diff --git a/Cargo.lock b/Cargo.lock index d27868a4..90c4f0b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,8 +1075,6 @@ checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59" [[package]] name = "bevy_pbr" version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab6944ffc6fd71604c0fbca68cc3e2a3654edfcdbfd232f9d8b88e3d20fdc0" dependencies = [ "bevy_app", "bevy_asset", diff --git a/Cargo.toml b/Cargo.toml index 987f0aa6..26df4ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ opt-level = 3 [profile.dev.package."*"] opt-level = 3 + +[patch.crates-io] +bevy_pbr = { path = "vendor/bevy_pbr" } diff --git a/crates/skate-game/src/performance.rs b/crates/skate-game/src/performance.rs index 0d0e29ec..4b4ba29e 100644 --- a/crates/skate-game/src/performance.rs +++ b/crates/skate-game/src/performance.rs @@ -95,9 +95,30 @@ impl Plugin for PerformancePlugin { .before(RenderSystems::Render), ) .add_systems(Render, render_finish.after(RenderSystems::PostCleanup)); + render_app + .init_resource::() + .add_systems( + Render, + mesh_binding_begin + .in_set(RenderSystems::PrepareBindGroups) + .before(bevy::pbr::prepare_mesh_bind_groups), + ) + .add_systems( + Render, + mesh_binding_end + .in_set(RenderSystems::PrepareBindGroups) + .after(bevy::pbr::prepare_mesh_bind_groups), + ); } // Benchmark measurements must not depend on whether the window has focus. app.insert_resource(bevy::winit::WinitSettings::continuous()); + app.add_systems(Startup, report_adapter); + if std::env::var_os("SKATE_PERF_CAMERA_SWEEP").is_some() { + app.add_systems( + Update, + sweep_camera.after(crate::app::FrameSet::Verification), + ); + } app.insert_resource(Performance { path: path.into(), start: None, @@ -112,6 +133,34 @@ impl Plugin for PerformancePlugin { .add_systems(Last, finish); } } + +#[derive(Resource, Default)] +struct MeshBindingTimer(Option); +fn mesh_binding_begin(mut timer: ResMut) { + timer.0 = Some(Scope::new("mesh_bind_groups")); +} +fn mesh_binding_end(mut timer: ResMut) { + timer.0.take(); +} +fn report_adapter( + device: Res, + adapter: Res, +) { + eprintln!( + "SKATE_GPU adapter={:?} features={:?} limits={:?}", + &**adapter, + device.features(), + device.limits() + ); +} + +// Rendering-only benchmark: exercise changing visibility without steering the +// skater or feeding synthetic inputs into the simulation camera. +fn sweep_camera(time: Res>, mut cameras: Query<&mut Transform, With>) { + for mut transform in &mut cameras { + transform.rotate_y(time.elapsed_secs() * std::f32::consts::TAU / 12.); + } +} fn begin(mut p: ResMut) { p.frame_start = Instant::now(); p.physics_ms = 0.; diff --git a/docs/performance.md b/docs/performance.md index c5e32f77..f9668234 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -59,3 +59,41 @@ Validation includes the full skate-core suite and game suite (43 passed, 21 exis ignored tests), differential collision queries against the unfiltered implementation across 420 varied poses, material/texture identity checks, and exact render triangle retention tests. Startup capture checks rendering/integration, not retail parity. + +## Follow-up: Bevy lightmap binding cache + +GPU diagnostics confirmed the RTX 5090 Vulkan adapter with binding arrays, +non-uniform indexing, and indirect drawing enabled. Local source inspection and +profiling found repeated lightmap bind-group creation in Bevy's +`prepare_mesh_bind_groups`. The [tracked Bevy patch](../vendor/README.md) caches +those bindings with resource-identity checks and bounded retention. + +Same stationary-spawn benchmark, before and after this patch: + +| Metric | Before cache | After cache | +| --- | ---: | ---: | +| Average FPS | 52.3 | 237.1 | +| Frame interval p95 | 94.5 ms | 5.15 ms | +| Render preparation per frame | 15.52 ms | 1.19 ms | +| Mesh binding interval per call, including warmup | 10.39 ms | 0.66 ms | + +Reports: `logs/perf-bindings.json` and `logs/perf-cache-repeat.json`. The first +cache trial averaged 158 FPS, but overlapped test compilation and is excluded +from the clean comparison. The normal window presentation settings are unchanged. +GPU telemetry during the clean repeat sampled roughly 23–33% usage, compared +with the earlier single-digit readings. Full utilization is not the goal: this +GPU can render this view at high FPS without approaching full utilization. + +An additional render-only camera sweep averaged 91.1 FPS, with a 24.9 ms p95 +frame interval (`logs/perf-cache-sweep.json`). This rotates through different +views to exercise changing visibility and newly extracted lightmaps; it does not +move the skater or alter collision behavior. Enable it alongside the usual +benchmark variable with `$env:SKATE_PERF_CAMERA_SWEEP = '1'`, and remove it +afterwards with `Remove-Item Env:SKATE_PERF_CAMERA_SWEEP`. It only runs when +`SKATE_PERF_REPORT` also enables the benchmark plugin. + +The sweep is a different workload, not an FPS comparison against the stationary +baseline. Exploration can still encounter heavier views and loading/shader spikes. +Validation: GPU cache regression test passed, game suite passed (43 passed, +21 existing ignored), and the staged executable completed a normal city startup +capture with benchmark settings disabled. diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 00000000..7cc07227 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,51 @@ +# Local Bevy patch + +`bevy_pbr` is the unmodified crates.io 0.18.1 source except for the changes +described below. Its original MIT and Apache licenses are included. Cargo selects +it through the workspace `[patch.crates-io]` entry; the Cargo registry is untouched. + +## Cached lightmap bindings + +Bevy 0.18.1 rebuilds a bind group for every allocated lightmap slab and every +render phase on each frame, even when all bound GPU resources are unchanged. +On San Vanelona, mesh binding preparation measured roughly 10 ms per frame. + +Local changes are restricted to: + +- `src/lightmap/mod.rs`: each slab retains at most eight bindings. Lookup compares + the actual wgpu buffer identity, binding offset and size, and layout identity. + Texture insertion/removal clears the cache. FIFO eviction bounds retained old + buffers when phases disappear or instance buffers resize. +- `src/render/mesh_bindings.rs`: reuse or create the binding through that cache. + The lightmap slab parameter is now mutable. +- `src/lightmap/cache_tests.rs`: explicit GPU regression test. + +Writing new instance data into an existing buffer does not require rebuilding a +bind group. Changing the buffer, bound range, layout, or texture does. The patch +does not skip extraction, uploads, animation, or visibility updates. It retains +the existing single-texture and binding-array rendering paths and shaders. + +Run the Vulkan test explicitly: + +```powershell +cargo test -p bevy_pbr --lib cache_tracks_binding_identity_and_texture_replacement --locked -- --ignored +``` + +It covers cloned versus replaced buffers, changed binding ranges/layouts, writes +to an existing buffer, texture insertion/removal, bounded eviction, and actual +bind-group construction/reuse. Regular game tests and a city screenshot/benchmark +also exercise the patched renderer. + +Upstream references reviewed September 6, 2026: + +- [Bevy issue 23595](https://github.com/bevyengine/bevy/issues/23595) reports + increasing `prepare_mesh_bind_groups` cost. That report points at mesh scanning; + local measurements and source inspection identified lightmap binding recreation + as the opportunity addressed here. +- [Bevy 0.19 rendering improvements](https://bevy.org/news/bevy-0-19/#render-big-scenes-faster) + explain further GPU batching and CPU overhead reductions. An engine migration + is separate from this small patch; 0.19.1 source still rebuilds lightmap groups. + +When upgrading Bevy, reassess this patch and rerun the GPU test, stationary and +moving-camera benchmarks, and visual comparison. Do not assume an unchanged +private renderer implementation across engine versions. diff --git a/vendor/bevy_pbr/Cargo.toml b/vendor/bevy_pbr/Cargo.toml new file mode 100644 index 00000000..814202f7 --- /dev/null +++ b/vendor/bevy_pbr/Cargo.toml @@ -0,0 +1,240 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "bevy_pbr" +version = "0.18.1" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Adds PBR rendering to Bevy Engine" +homepage = "https://bevy.org" +readme = "README.md" +keywords = ["bevy"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/bevyengine/bevy" +resolver = "2" + +[package.metadata.docs.rs] +rustdoc-args = [ + "-Zunstable-options", + "--generate-link-to-definition", +] +all-features = true +features = ["bevy_image/zstd_rust"] + +[features] +bluenoise_texture = [ + "bevy_image/ktx2", + "bevy_image/zstd", +] +experimental_pbr_pcss = ["bevy_light/experimental_pbr_pcss"] +meshlet = [ + "dep:lz4_flex", + "dep:range-alloc", + "dep:bevy_tasks", +] +meshlet_processor = [ + "meshlet", + "dep:meshopt", + "dep:metis", + "dep:itertools", + "dep:bitvec", +] +pbr_anisotropy_texture = [] +pbr_clustered_decals = [] +pbr_light_textures = [] +pbr_multi_layer_material_textures = [] +pbr_specular_textures = [] +pbr_transmission_textures = [] +shader_format_glsl = ["bevy_shader/shader_format_glsl"] +trace = ["bevy_render/trace"] +webgl = ["bevy_light/webgl"] +webgpu = ["bevy_light/webgpu"] + +[lib] +name = "bevy_pbr" +path = "src/lib.rs" + +[dependencies.bevy_app] +version = "0.18.0" + +[dependencies.bevy_asset] +version = "0.18.0" + +[dependencies.bevy_camera] +version = "0.18.0" + +[dependencies.bevy_color] +version = "0.18.0" + +[dependencies.bevy_core_pipeline] +version = "0.18.0" + +[dependencies.bevy_derive] +version = "0.18.0" + +[dependencies.bevy_diagnostic] +version = "0.18.0" + +[dependencies.bevy_ecs] +version = "0.18.0" + +[dependencies.bevy_image] +version = "0.18.0" + +[dependencies.bevy_light] +version = "0.18.0" + +[dependencies.bevy_log] +version = "0.18.0" + +[dependencies.bevy_math] +version = "0.18.0" + +[dependencies.bevy_mesh] +version = "0.18.0" +features = [ + "morph", + "bevy_mikktspace", +] + +[dependencies.bevy_platform] +version = "0.18.0" +features = ["std"] +default-features = false + +[dependencies.bevy_reflect] +version = "0.18.0" + +[dependencies.bevy_render] +version = "0.18.0" +features = ["morph"] + +[dependencies.bevy_shader] +version = "0.18.0" + +[dependencies.bevy_tasks] +version = "0.18.0" +optional = true + +[dependencies.bevy_transform] +version = "0.18.0" + +[dependencies.bevy_utils] +version = "0.18.0" + +[dependencies.bitflags] +version = "2.3" +features = ["bytemuck"] + +[dependencies.bitvec] +version = "1" +optional = true + +[dependencies.bytemuck] +version = "1" +features = [ + "derive", + "must_cast", +] + +[dependencies.derive_more] +version = "2" +features = ["from"] +default-features = false + +[dependencies.fixedbitset] +version = "0.5" + +[dependencies.itertools] +version = "0.14" +optional = true + +[dependencies.lz4_flex] +version = "0.12" +features = ["frame"] +optional = true +default-features = false + +[dependencies.meshopt] +version = "0.6.2" +optional = true + +[dependencies.metis] +version = "0.2.2" +optional = true + +[dependencies.nonmax] +version = "0.5" + +[dependencies.offset-allocator] +version = "0.2" + +[dependencies.range-alloc] +version = "0.1" +optional = true + +[dependencies.smallvec] +version = "1" +default-features = false + +[dependencies.static_assertions] +version = "1" + +[dependencies.thiserror] +version = "2" +default-features = false + +[dependencies.tracing] +version = "0.1" +features = ["std"] +default-features = false + +[lints.clippy] +alloc_instead_of_core = "warn" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" +doc_markdown = "warn" +manual_let_else = "warn" +match_same_arms = "warn" +needless_lifetimes = "allow" +nonstandard_macro_braces = "warn" +print_stderr = "warn" +print_stdout = "warn" +ptr_as_ptr = "warn" +ptr_cast_constness = "warn" +redundant_closure_for_method_calls = "warn" +redundant_else = "warn" +ref_as_ptr = "warn" +semicolon_if_nothing_returned = "warn" +std_instead_of_alloc = "warn" +std_instead_of_core = "warn" +too_long_first_doc_paragraph = "allow" +too_many_arguments = "allow" +type_complexity = "allow" +undocumented_unsafe_blocks = "warn" +unwrap_or_default = "warn" + +[lints.rust] +missing_docs = "warn" +unsafe_code = "deny" +unsafe_op_in_unsafe_fn = "warn" +unused_qualifications = "warn" + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = ["cfg(docsrs_dep)"] diff --git a/vendor/bevy_pbr/LICENSE-APACHE b/vendor/bevy_pbr/LICENSE-APACHE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/vendor/bevy_pbr/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/vendor/bevy_pbr/LICENSE-MIT b/vendor/bevy_pbr/LICENSE-MIT new file mode 100644 index 00000000..9cf10627 --- /dev/null +++ b/vendor/bevy_pbr/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/vendor/bevy_pbr/README.md b/vendor/bevy_pbr/README.md new file mode 100644 index 00000000..c65a6a11 --- /dev/null +++ b/vendor/bevy_pbr/README.md @@ -0,0 +1,7 @@ +# Bevy PBR + +[![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_pbr.svg)](https://crates.io/crates/bevy_pbr) +[![Downloads](https://img.shields.io/crates/d/bevy_pbr.svg)](https://crates.io/crates/bevy_pbr) +[![Docs](https://docs.rs/bevy_pbr/badge.svg)](https://docs.rs/bevy_pbr/latest/bevy_pbr/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/vendor/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl b/vendor/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl new file mode 100644 index 00000000..efacc3de --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl @@ -0,0 +1,64 @@ +#import bevy_pbr::{ + atmosphere::{ + bindings::settings, + functions::{ + sample_density_lut, sample_local_inscattering, uv_to_ray_direction, get_view_position, + MIDPOINT_RATIO, MIN_EXTINCTION, ABSORPTION_DENSITY, SCATTERING_DENSITY, + }, + } +} + + +@group(0) @binding(13) var aerial_view_lut_out: texture_storage_3d; + +@compute +@workgroup_size(16, 16, 1) +fn main(@builtin(global_invocation_id) idx: vec3) { + if any(idx.xy > settings.aerial_view_lut_size.xy) { return; } + + let uv = (vec2(idx.xy) + 0.5) / vec2(settings.aerial_view_lut_size.xy); + let ray_dir = uv_to_ray_direction(uv); + let world_pos = get_view_position(); + + let r = length(world_pos); + let t_max = settings.aerial_view_lut_max_distance; + + var prev_t = 0.0; + var total_inscattering = vec3(0.0); + var throughput = vec3(1.0); + + for (var slice_i: u32 = 0; slice_i < settings.aerial_view_lut_size.z; slice_i++) { + for (var step_i: u32 = 0; step_i < settings.aerial_view_lut_samples; step_i++) { + let t_i = t_max * (f32(slice_i) + ((f32(step_i) + MIDPOINT_RATIO) / f32(settings.aerial_view_lut_samples))) / f32(settings.aerial_view_lut_size.z); + let dt = (t_i - prev_t); + prev_t = t_i; + + let sample_pos = world_pos + ray_dir * t_i; + let local_r = length(sample_pos); + let local_up = normalize(sample_pos); + + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt; + let sample_transmittance = exp(-sample_optical_depth); + + // evaluate one segment of the integral + var inscattering = sample_local_inscattering(scattering, ray_dir, sample_pos); + + // Analytical integration of the single scattering term in the radiance transfer equation + let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); + total_inscattering += throughput * s_int; + + throughput *= sample_transmittance; + if all(throughput < vec3(0.001)) { + break; + } + } + + // Store in log space to allow linear interpolation of exponential values between slices + let log_inscattering = log(max(total_inscattering, vec3(1e-6))); + textureStore(aerial_view_lut_out, vec3(vec2(idx.xy), slice_i), vec4(log_inscattering, 0.0)); + } +} diff --git a/vendor/bevy_pbr/src/atmosphere/bindings.wgsl b/vendor/bevy_pbr/src/atmosphere/bindings.wgsl new file mode 100644 index 00000000..5c8fc811 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/bindings.wgsl @@ -0,0 +1,24 @@ +#define_import_path bevy_pbr::atmosphere::bindings + +#import bevy_render::view::View; + +#import bevy_pbr::{ + mesh_view_types::Lights, + atmosphere::types::{Atmosphere, AtmosphereSettings, AtmosphereTransforms} +} + +@group(0) @binding(0) var atmosphere: Atmosphere; +@group(0) @binding(1) var settings: AtmosphereSettings; +@group(0) @binding(2) var atmosphere_transforms: AtmosphereTransforms; +@group(0) @binding(3) var view: View; +@group(0) @binding(4) var lights: Lights; + +@group(0) @binding(5) var medium_density_lut: texture_2d; +@group(0) @binding(6) var medium_scattering_lut: texture_2d; +@group(0) @binding(7) var medium_sampler: sampler; + +@group(0) @binding(8) var transmittance_lut: texture_2d; +@group(0) @binding(9) var multiscattering_lut: texture_2d; +@group(0) @binding(10) var sky_view_lut: texture_2d; +@group(0) @binding(11) var aerial_view_lut: texture_3d; +@group(0) @binding(12) var atmosphere_lut_sampler: sampler; diff --git a/vendor/bevy_pbr/src/atmosphere/bruneton_functions.wgsl b/vendor/bevy_pbr/src/atmosphere/bruneton_functions.wgsl new file mode 100644 index 00000000..1e559ca9 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/bruneton_functions.wgsl @@ -0,0 +1,139 @@ +// Copyright (c) 2017 Eric Bruneton +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// Precomputed Atmospheric Scattering +// Copyright (c) 2008 INRIA +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +#define_import_path bevy_pbr::atmosphere::bruneton_functions + +#import bevy_pbr::atmosphere::{ + types::Atmosphere, + bindings::atmosphere, +} + +// Mapping from view height (r) and zenith cos angle (mu) to UV coordinates in the transmittance LUT +// Assuming r between ground and top atmosphere boundary, and mu= cos(zenith_angle) +// Chosen to increase precision near the ground and to work around a discontinuity at the horizon +// See Bruneton and Neyret 2008, "Precomputed Atmospheric Scattering" section 4 +fn transmittance_lut_r_mu_to_uv(atm: Atmosphere, r: f32, mu: f32) -> vec2 { + // Distance along a horizontal ray from the ground to the top atmosphere boundary + let H = sqrt(atm.top_radius * atm.top_radius - atm.bottom_radius * atm.bottom_radius); + + // Distance from a point at height r to the horizon + // ignore the case where r <= atmosphere.bottom_radius + let rho = sqrt(max(r * r - atm.bottom_radius * atm.bottom_radius, 0.0)); + + // Distance from a point at height r to the top atmosphere boundary at zenith angle mu + let d = distance_to_top_atmosphere_boundary(atm, r, mu); + + // Minimum and maximum distance to the top atmosphere boundary from a point at height r + let d_min = atm.top_radius - r; // length of the ray straight up to the top atmosphere boundary + let d_max = rho + H; // length of the ray to the top atmosphere boundary and grazing the horizon + + let u = (d - d_min) / (d_max - d_min); + let v = rho / H; + return vec2(u, v); +} + +// Inverse of the mapping above, mapping from UV coordinates in the transmittance LUT to view height (r) and zenith cos angle (mu) +fn transmittance_lut_uv_to_r_mu(uv: vec2) -> vec2 { + // Distance to top atmosphere boundary for a horizontal ray at ground level + let H = sqrt(atmosphere.top_radius * atmosphere.top_radius - atmosphere.bottom_radius * atmosphere.bottom_radius); + + // Distance to the horizon, from which we can compute r: + let rho = H * uv.y; + let r = sqrt(rho * rho + atmosphere.bottom_radius * atmosphere.bottom_radius); + + // Distance to the top atmosphere boundary for the ray (r,mu), and its minimum + // and maximum values over all mu- obtained for (r,1) and (r,mu_horizon) - + // from which we can recover mu: + let d_min = atmosphere.top_radius - r; + let d_max = rho + H; + let d = d_min + uv.x * (d_max - d_min); + + var mu: f32; + if d == 0.0 { + mu = 1.0; + } else { + mu = (H * H - rho * rho - d * d) / (2.0 * r * d); + } + + mu = clamp(mu, -1.0, 1.0); + + return vec2(r, mu); +} + +/// Simplified ray-sphere intersection +/// where: +/// Ray origin, o = [0,0,r] with r <= atmosphere.top_radius +/// mu is the cosine of spherical coordinate theta (-1.0 <= mu <= 1.0) +/// so ray direction in spherical coordinates is [1,acos(mu),0] which needs to be converted to cartesian +/// Direction of ray, u = [0,sqrt(1-mu*mu),mu] +/// Center of sphere, c = [0,0,0] +/// Radius of sphere, r = atmosphere.top_radius +/// This function solves the quadratic equation for line-sphere intersection simplified under these assumptions +fn distance_to_top_atmosphere_boundary(atm: Atmosphere, r: f32, mu: f32) -> f32 { + // ignore the case where r > atm.top_radius + let positive_discriminant = max(r * r * (mu * mu - 1.0) + atm.top_radius * atm.top_radius, 0.0); + return max(-r * mu + sqrt(positive_discriminant), 0.0); +} + +/// Simplified ray-sphere intersection +/// as above for intersections with the ground +fn distance_to_bottom_atmosphere_boundary(r: f32, mu: f32) -> f32 { + let positive_discriminant = max(r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0); + return max(-r * mu - sqrt(positive_discriminant), 0.0); +} + +fn ray_intersects_ground(r: f32, mu: f32) -> bool { + return mu < 0.0 && r * r * (mu * mu - 1.0) + atmosphere.bottom_radius * atmosphere.bottom_radius >= 0.0; +} diff --git a/vendor/bevy_pbr/src/atmosphere/environment.rs b/vendor/bevy_pbr/src/atmosphere/environment.rs new file mode 100644 index 00000000..29f9c7f3 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/environment.rs @@ -0,0 +1,333 @@ +use crate::{ + resources::{ + AtmosphereSampler, AtmosphereTextures, AtmosphereTransform, AtmosphereTransforms, + AtmosphereTransformsOffset, GpuAtmosphere, + }, + ExtractedAtmosphere, GpuAtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, +}; +use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryState, With, Without}, + resource::Resource, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::Image; +use bevy_light::{AtmosphereEnvironmentMapLight, GeneratedEnvironmentMapLight}; +use bevy_math::{Quat, UVec2}; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex, ExtractComponent}, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::{binding_types::*, *}, + renderer::{RenderContext, RenderDevice}, + texture::{CachedTexture, GpuImage}, + view::{ViewUniform, ViewUniformOffset, ViewUniforms}, +}; +use bevy_utils::default; +use tracing::warn; + +// Render world representation of an environment map light for the atmosphere +#[derive(Component, ExtractComponent, Clone)] +pub struct AtmosphereEnvironmentMap { + pub environment_map: Handle, + pub size: UVec2, +} + +#[derive(Component)] +pub struct AtmosphereProbeTextures { + pub environment: TextureView, + pub transmittance_lut: CachedTexture, + pub multiscattering_lut: CachedTexture, + pub sky_view_lut: CachedTexture, + pub aerial_view_lut: CachedTexture, +} + +#[derive(Component)] +pub(crate) struct AtmosphereProbeBindGroups { + pub environment: BindGroup, +} + +#[derive(Resource)] +pub struct AtmosphereProbeLayouts { + pub environment: BindGroupLayoutDescriptor, +} + +#[derive(Resource)] +pub struct AtmosphereProbePipeline { + pub environment: CachedComputePipelineId, +} + +pub fn init_atmosphere_probe_layout(mut commands: Commands) { + let environment = BindGroupLayoutDescriptor::new( + "environment_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + // uniforms + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (9, texture_2d(TextureSampleType::default())), // multiscattering + (10, texture_2d(TextureSampleType::default())), // sky view + (11, texture_3d(TextureSampleType::default())), // aerial view + (12, sampler(SamplerBindingType::Filtering)), + // output 2D array texture + ( + 13, + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + commands.insert_resource(AtmosphereProbeLayouts { environment }); +} + +pub(super) fn prepare_atmosphere_probe_bind_groups( + probes: Query<(Entity, &AtmosphereProbeTextures), With>, + render_device: Res, + layouts: Res, + atmosphere_sampler: Res, + view_uniforms: Res, + lights_uniforms: Res, + atmosphere_transforms: Res, + atmosphere_uniforms: Res>, + settings_uniforms: Res>, + pipeline_cache: Res, + mut commands: Commands, +) { + for (entity, textures) in &probes { + let environment = render_device.create_bind_group( + "environment_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.environment), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_uniforms.binding().unwrap()), + (1, settings_uniforms.binding().unwrap()), + (2, atmosphere_transforms.uniforms().binding().unwrap()), + (3, view_uniforms.uniforms.binding().unwrap()), + (4, lights_uniforms.view_gpu_lights.binding().unwrap()), + // atmosphere luts and sampler + (8, &textures.transmittance_lut.default_view), + (9, &textures.multiscattering_lut.default_view), + (10, &textures.sky_view_lut.default_view), + (11, &textures.aerial_view_lut.default_view), + (12, &**atmosphere_sampler), + // output 2D array texture + (13, &textures.environment), + )), + ); + + commands + .entity(entity) + .insert(AtmosphereProbeBindGroups { environment }); + } +} + +pub(super) fn prepare_probe_textures( + view_textures: Query<&AtmosphereTextures, With>, + probes: Query< + (Entity, &AtmosphereEnvironmentMap), + ( + With, + Without, + ), + >, + gpu_images: Res>, + mut commands: Commands, +) { + for (probe, render_env_map) in &probes { + let environment = gpu_images.get(&render_env_map.environment_map).unwrap(); + // create a cube view + let environment_view = environment.texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + // Get the first view entity's textures to borrow + if let Some(view_textures) = view_textures.iter().next() { + commands.entity(probe).insert(AtmosphereProbeTextures { + environment: environment_view, + transmittance_lut: view_textures.transmittance_lut.clone(), + multiscattering_lut: view_textures.multiscattering_lut.clone(), + sky_view_lut: view_textures.sky_view_lut.clone(), + aerial_view_lut: view_textures.aerial_view_lut.clone(), + }); + } + } +} + +pub fn init_atmosphere_probe_pipeline( + pipeline_cache: Res, + layouts: Res, + asset_server: Res, + mut commands: Commands, +) { + let environment = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("environment_pipeline".into()), + layout: vec![layouts.environment.clone()], + shader: load_embedded_asset!(asset_server.as_ref(), "environment.wgsl"), + ..default() + }); + commands.insert_resource(AtmosphereProbePipeline { environment }); +} + +// Ensure power-of-two dimensions to avoid edge update issues on cubemap faces +pub fn validate_environment_map_size(size: UVec2) -> UVec2 { + let new_size = UVec2::new( + size.x.max(1).next_power_of_two(), + size.y.max(1).next_power_of_two(), + ); + if new_size != size { + warn!( + "Non-power-of-two AtmosphereEnvironmentMapLight size {}, correcting to {new_size}", + size + ); + } + new_size +} + +pub fn prepare_atmosphere_probe_components( + probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without,)>, + mut commands: Commands, + mut images: ResMut>, +) { + for (entity, env_map_light) in &probes { + // Create a cubemap image in the main world that we can reference + let new_size = validate_environment_map_size(env_map_light.size); + let mut environment_image = Image::new_fill( + Extent3d { + width: new_size.x, + height: new_size.y, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + environment_image.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + environment_image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING + | TextureUsages::STORAGE_BINDING + | TextureUsages::COPY_SRC; + + // Add the image to assets to get a handle + let environment_handle = images.add(environment_image); + + commands.entity(entity).insert(AtmosphereEnvironmentMap { + environment_map: environment_handle.clone(), + size: new_size, + }); + + commands + .entity(entity) + .insert(GeneratedEnvironmentMapLight { + environment_map: environment_handle, + intensity: env_map_light.intensity, + rotation: Quat::IDENTITY, + affects_lightmapped_mesh_diffuse: env_map_light.affects_lightmapped_mesh_diffuse, + }); + } +} + +pub(super) struct EnvironmentNode { + main_view_query: QueryState<( + Read>, + Read>, + Read, + Read, + Read, + )>, + probe_query: QueryState<( + Read, + Read, + )>, +} + +impl FromWorld for EnvironmentNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + probe_query: QueryState::new(world), + } + } +} + +impl Node for EnvironmentNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.probe_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + let view_entity = graph.view_entity(); + + let Some(environment_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.environment) + else { + return Ok(()); + }; + + let (Ok(( + atmosphere_uniforms_offset, + settings_uniforms_offset, + atmosphere_transforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + )),) = (self.main_view_query.get_manual(world, view_entity),) + else { + return Ok(()); + }; + + for (bind_groups, env_map_light) in self.probe_query.iter_manual(world) { + let mut pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("environment_pass"), + timestamp_writes: None, + }); + + pass.set_pipeline(environment_pipeline); + pass.set_bind_group( + 0, + &bind_groups.environment, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + atmosphere_transforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + pass.dispatch_workgroups( + env_map_light.size.x / 8, + env_map_light.size.y / 8, + 6, // 6 cubemap faces + ); + } + + Ok(()) + } +} diff --git a/vendor/bevy_pbr/src/atmosphere/environment.wgsl b/vendor/bevy_pbr/src/atmosphere/environment.wgsl new file mode 100644 index 00000000..0ca53508 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/environment.wgsl @@ -0,0 +1,39 @@ +#import bevy_pbr::{ + atmosphere::{ + functions::{direction_world_to_atmosphere, sample_sky_view_lut, get_view_position}, + }, + utils::sample_cube_dir +} + +@group(0) @binding(13) var output: texture_storage_2d_array; + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let dimensions = textureDimensions(output); + let slice_index = global_id.z; + + if (global_id.x >= dimensions.x || global_id.y >= dimensions.y || slice_index >= 6u) { + return; + } + + // Calculate normalized UV coordinates for this pixel + let uv = vec2( + (f32(global_id.x) + 0.5) / f32(dimensions.x), + (f32(global_id.y) + 0.5) / f32(dimensions.y) + ); + + var ray_dir_ws = sample_cube_dir(uv, slice_index); + + // invert the z direction to account for cubemaps being lefthanded + ray_dir_ws.z = -ray_dir_ws.z; + + let world_pos = get_view_position(); + let r = length(world_pos); + let up = normalize(world_pos); + + let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz, up); + let inscattering = sample_sky_view_lut(r, ray_dir_as); + let color = vec4(inscattering, 1.0); + + textureStore(output, vec2(global_id.xy), i32(slice_index), color); +} diff --git a/vendor/bevy_pbr/src/atmosphere/functions.wgsl b/vendor/bevy_pbr/src/atmosphere/functions.wgsl new file mode 100644 index 00000000..0c034031 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/functions.wgsl @@ -0,0 +1,510 @@ +#define_import_path bevy_pbr::atmosphere::functions + +#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2, ray_sphere_intersect} + +#import bevy_pbr::atmosphere::{ + types::Atmosphere, + bindings::{ + atmosphere, settings, view, lights, transmittance_lut, atmosphere_lut_sampler, + multiscattering_lut, sky_view_lut, aerial_view_lut, atmosphere_transforms, + medium_density_lut, medium_scattering_lut, medium_sampler, + }, + bruneton_functions::{ + transmittance_lut_r_mu_to_uv, ray_intersects_ground, + distance_to_top_atmosphere_boundary, distance_to_bottom_atmosphere_boundary + }, +} + +// NOTE FOR CONVENTIONS: +// r: +// radius, or distance from planet center +// +// altitude: +// distance from planet **surface** +// +// mu: +// cosine of the zenith angle of a ray with +// respect to the planet normal +// +// atmosphere space: +// abbreviated as "as" (contrast with vs, cs, ws), this space is similar +// to view space, but with the camera positioned horizontally on the planet +// surface, so the horizon is a horizontal line centered vertically in the +// frame. This enables the non-linear latitude parametrization the paper uses +// to concentrate detail near the horizon + + +// CONSTANTS +const FRAC_PI: f32 = 0.3183098862; // 1 / π +const FRAC_2_PI: f32 = 0.15915494309; // 1 / (2π) +const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π) +const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π) +const ROOT_2: f32 = 1.41421356; // √2 +const EPSILON: f32 = 1.0; // 1 meter +const MIN_EXTINCTION: vec3 = vec3(1e-12); + +// During raymarching, each segment is sampled at a single point. This constant determines +// where in the segment that sample is taken (0.0 = start, 0.5 = middle, 1.0 = end). +// We use 0.3 to sample closer to the start of each segment, which better approximates +// the exponential falloff of atmospheric density. +const MIDPOINT_RATIO: f32 = 0.3; + +// LUT UV PARAMETERIZATIONS + +fn unit_to_sub_uvs(val: vec2, resolution: vec2) -> vec2 { + return (val + 0.5f / resolution) * (resolution / (resolution + 1.0f)); +} + +fn sub_uvs_to_unit(val: vec2, resolution: vec2) -> vec2 { + return (val - 0.5f / resolution) * (resolution / (resolution - 1.0f)); +} + +fn multiscattering_lut_r_mu_to_uv(r: f32, mu: f32) -> vec2 { + let u = 0.5 + 0.5 * mu; + let v = saturate((r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius)); //TODO + return unit_to_sub_uvs(vec2(u, v), vec2(settings.multiscattering_lut_size)); +} + +fn multiscattering_lut_uv_to_r_mu(uv: vec2) -> vec2 { + let adj_uv = sub_uvs_to_unit(uv, vec2(settings.multiscattering_lut_size)); + let r = mix(atmosphere.bottom_radius, atmosphere.top_radius, adj_uv.y); + let mu = adj_uv.x * 2 - 1; + return vec2(r, mu); +} + +fn sky_view_lut_r_mu_azimuth_to_uv(r: f32, mu: f32, azimuth: f32) -> vec2 { + let u = (azimuth * FRAC_2_PI) + 0.5; + + let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius); + let cos_beta = v_horizon / r; + // Using fast_acos_4 for better precision at small angles + // to avoid artifacts at the horizon + let beta = fast_acos_4(cos_beta); + let horizon_zenith = PI - beta; + let view_zenith = fast_acos_4(mu); + + // Apply non-linear transformation to compress more texels + // near the horizon where high-frequency details matter most + // l is latitude in [-π/2, π/2] and v is texture coordinate in [0,1] + let l = view_zenith - horizon_zenith; + let abs_l = abs(l); + + let v = 0.5 + 0.5 * sign(l) * sqrt(abs_l / HALF_PI); + + return unit_to_sub_uvs(vec2(u, v), vec2(settings.sky_view_lut_size)); +} + +fn sky_view_lut_uv_to_zenith_azimuth(r: f32, uv: vec2) -> vec2 { + let adj_uv = sub_uvs_to_unit(vec2(uv.x, 1.0 - uv.y), vec2(settings.sky_view_lut_size)); + let azimuth = (adj_uv.x - 0.5) * PI_2; + + // Horizon parameters + let v_horizon = sqrt(r * r - atmosphere.bottom_radius * atmosphere.bottom_radius); + let cos_beta = v_horizon / r; + let beta = fast_acos_4(cos_beta); + let horizon_zenith = PI - beta; + + // Inverse of horizon-detail mapping to recover original latitude from texture coordinate + let t = abs(2.0 * (adj_uv.y - 0.5)); + let l = sign(adj_uv.y - 0.5) * HALF_PI * t * t; + + return vec2(horizon_zenith - l, azimuth); +} + +// LUT SAMPLING + +fn sample_transmittance_lut(r: f32, mu: f32) -> vec3 { + let uv = transmittance_lut_r_mu_to_uv(atmosphere, r, mu); + return textureSampleLevel(transmittance_lut, atmosphere_lut_sampler, uv, 0.0).rgb; +} + +// NOTICE: This function is copyrighted by Eric Bruneton and INRIA, and falls +// under the license reproduced in bruneton_functions.wgsl (variant of MIT license) +// +// FIXME: this function should be in bruneton_functions.wgsl, but because naga_oil doesn't +// support cyclic imports it's stuck here +fn sample_transmittance_lut_segment(r: f32, mu: f32, t: f32) -> vec3 { + let r_t = get_local_r(r, mu, t); + let mu_t = clamp((r * mu + t) / r_t, -1.0, 1.0); + + if ray_intersects_ground(r, mu) { + return min( + sample_transmittance_lut(r_t, -mu_t) / sample_transmittance_lut(r, -mu), + vec3(1.0) + ); + } else { + return min( + sample_transmittance_lut(r, mu) / sample_transmittance_lut(r_t, mu_t), vec3(1.0) + ); + } +} + +fn sample_multiscattering_lut(r: f32, mu: f32) -> vec3 { + let uv = multiscattering_lut_r_mu_to_uv(r, mu); + return textureSampleLevel(multiscattering_lut, atmosphere_lut_sampler, uv, 0.0).rgb; +} + +fn sample_sky_view_lut(r: f32, ray_dir_as: vec3) -> vec3 { + let mu = ray_dir_as.y; + let azimuth = fast_atan2(ray_dir_as.x, -ray_dir_as.z); + let uv = sky_view_lut_r_mu_azimuth_to_uv(r, mu, azimuth); + return textureSampleLevel(sky_view_lut, atmosphere_lut_sampler, uv, 0.0).rgb; +} + +fn ndc_to_camera_dist(ndc: vec3) -> f32 { + let view_pos = view.view_from_clip * vec4(ndc, 1.0); + let t = length(view_pos.xyz / view_pos.w) * settings.scene_units_to_m; + return t; +} + +// RGB channels: total inscattered light along the camera ray to the current sample. +// A channel: average transmittance across all wavelengths to the current sample. +fn sample_aerial_view_lut(uv: vec2, t: f32) -> vec3 { + let t_max = settings.aerial_view_lut_max_distance; + let num_slices = f32(settings.aerial_view_lut_size.z); + // Each texel stores the value of the scattering integral over the whole slice, + // which requires us to offset the w coordinate by half a slice. For + // example, if we wanted the value of the integral at the boundary between slices, + // we'd need to sample at the center of the previous slice, and vice-versa for + // sampling in the center of a slice. + let uvw = vec3(uv, saturate(t / t_max - 0.5 / num_slices)); + let sample = textureSampleLevel(aerial_view_lut, atmosphere_lut_sampler, uvw, 0.0); + // Since sampling anywhere between w=0 and w=t_slice will clamp to the first slice, + // we need to do a linear step over the first slice towards zero at the camera's + // position to recover the correct integral value. + let t_slice = t_max / num_slices; + let fade = saturate(t / t_slice); + // Recover the values from log space + return exp(sample.rgb) * fade; +} + +// ATMOSPHERE SAMPLING + +const ABSORPTION_DENSITY: f32 = 0.0; +const SCATTERING_DENSITY: f32 = 1.0; + +// samples from the atmosphere density LUT. +// +// calling with `component = 0.0` will return the atmosphere's absorption density, +// while calling with `component = 1.0` will return the atmosphere's scattering density. +fn sample_density_lut(r: f32, component: f32) -> vec3 { + // sampler clamps to [0, 1] anyways, no need to clamp the altitude + let normalized_altitude = (r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius); + let uv = vec2(1.0 - normalized_altitude, component); + return textureSampleLevel(medium_density_lut, medium_sampler, uv, 0.0).xyz; +} + +// samples from the atmosphere scattering LUT. `neg_LdotV` is the dot product +// of the light direction and the incoming view vector. +fn sample_scattering_lut(r: f32, neg_LdotV: f32) -> vec3 { + let normalized_altitude = (r - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius); + let uv = vec2(1.0 - normalized_altitude, neg_LdotV * 0.5 + 0.5); + return textureSampleLevel(medium_scattering_lut, medium_sampler, uv, 0.0).xyz; +} + +/// evaluates L_scat, equation 3 in the paper, which gives the total single-order scattering towards the view at a single point +fn sample_local_inscattering(local_scattering: vec3, ray_dir: vec3, world_pos: vec3) -> vec3 { + let local_r = length(world_pos); + let local_up = normalize(world_pos); + var inscattering = vec3(0.0); + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + + let mu_light = dot((*light).direction_to_light, local_up); + + // -(L . V) == (L . -V). -V here is our ray direction, which points away from the view + // instead of towards it (as is the convention for V) + let neg_LdotV = dot((*light).direction_to_light, ray_dir); + + let transmittance_to_light = sample_transmittance_lut(local_r, mu_light); + let shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light)); + let scattering_coeff = sample_scattering_lut(local_r, neg_LdotV); + + // Transmittance from scattering event to light source + let scattering_factor = shadow_factor * scattering_coeff; + + // Additive factor from the multiscattering LUT + let psi_ms = sample_multiscattering_lut(local_r, mu_light); + let multiscattering_factor = psi_ms * local_scattering; + + inscattering += (*light).color.rgb * (scattering_factor + multiscattering_factor); + } + return inscattering; +} + +fn sample_sun_radiance(ray_dir_ws: vec3) -> vec3 { + let view_pos = get_view_position(); + let r = length(view_pos); + let up = normalize(view_pos); + let mu_view = dot(ray_dir_ws, up); + let shadow_factor = f32(!ray_intersects_ground(r, mu_view)); + var sun_radiance = vec3(0.0); + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws); + let angle_to_sun = fast_acos(clamp(neg_LdotV, -1.0, 1.0)); + let w = max(0.5 * fwidth(angle_to_sun), 1e-6); + let sun_angular_size = (*light).sun_disk_angular_size; + let sun_intensity = (*light).sun_disk_intensity; + if sun_angular_size > 0.0 && sun_intensity > 0.0 { + let factor = 1 - smoothstep(sun_angular_size * 0.5 - w, sun_angular_size * 0.5 + w, angle_to_sun); + let sun_solid_angle = (sun_angular_size * sun_angular_size) * 0.25 * PI; + sun_radiance += ((*light).color.rgb / sun_solid_angle) * sun_intensity * factor * shadow_factor; + } + } + return sun_radiance; +} + +fn calculate_visible_sun_ratio(atmosphere: Atmosphere, r: f32, mu: f32, sun_angular_size: f32) -> f32 { + let bottom_radius = atmosphere.bottom_radius; + // Calculate the angle between horizon and sun center + // Invert the horizon angle calculation to fix shading direction + let horizon_cos = -sqrt(1.0 - (bottom_radius * bottom_radius) / (r * r)); + let horizon_angle = fast_acos_4(horizon_cos); + let sun_zenith_angle = fast_acos_4(mu); + + // If sun is completely above horizon + if sun_zenith_angle + sun_angular_size * 0.5 <= horizon_angle { + return 1.0; + } + + // If sun is completely below horizon + if sun_zenith_angle - sun_angular_size * 0.5 >= horizon_angle { + return 0.0; + } + + // Calculate partial visibility using circular segment area formula + let d = (horizon_angle - sun_zenith_angle) / (sun_angular_size * 0.5); + let visible_ratio = 0.5 + d * 0.5; + return clamp(visible_ratio, 0.0, 1.0); +} + +// TRANSFORM UTILITIES + +/// Clamp a position to the planet surface (with a small epsilon) to avoid underground artifacts. +fn clamp_to_surface(atmosphere: Atmosphere, position: vec3) -> vec3 { + let min_radius = atmosphere.bottom_radius + EPSILON; + let r = length(position); + if r < min_radius { + let up = normalize(position); + return up * min_radius; + } + return position; +} + +fn max_atmosphere_distance(r: f32, mu: f32) -> f32 { + let t_top = distance_to_top_atmosphere_boundary(atmosphere, r, mu); + let t_bottom = distance_to_bottom_atmosphere_boundary(r, mu); + let hits = ray_intersects_ground(r, mu); + return mix(t_top, t_bottom, f32(hits)); +} + +/// Returns the observer's position in the atmosphere +fn get_view_position() -> vec3 { + var world_pos = view.world_position * settings.scene_units_to_m + vec3(0.0, atmosphere.bottom_radius, 0.0); + return clamp_to_surface(atmosphere, world_pos); +} + +// We assume the `up` vector at the view position is the y axis, since the world is locally flat/level. +// t = distance along view ray in atmosphere space +// NOTE: this means that if your world is actually spherical, this will be wrong. +fn get_local_up(r: f32, t: f32, ray_dir: vec3) -> vec3 { + return normalize(vec3(0.0, r, 0.0) + t * ray_dir); +} + +// Given a ray starting at radius r, with mu = cos(zenith angle), +// and a t = distance along the ray, gives the new radius at point t +fn get_local_r(r: f32, mu: f32, t: f32) -> f32 { + return sqrt(t * t + 2.0 * r * mu * t + r * r); +} + +// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Converts a direction in world space to atmosphere space +fn direction_world_to_atmosphere(dir_ws: vec3, up: vec3) -> vec3 { + // Camera forward in world space (-Z in view to world transform) + let forward_ws = (view.world_from_view * vec4(0.0, 0.0, -1.0, 0.0)).xyz; + let tangent_z = normalize(up * dot(forward_ws, up) - forward_ws); + let tangent_x = cross(up, tangent_z); + return vec3( + dot(dir_ws, tangent_x), + dot(dir_ws, up), + dot(dir_ws, tangent_z), + ); +} + +/// Converts a direction in atmosphere space to world space +fn direction_atmosphere_to_world(dir_as: vec3) -> vec3 { + let dir_ws = atmosphere_transforms.world_from_atmosphere * vec4(dir_as, 0.0); + return dir_ws.xyz; +} + +// Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport. +// Returns a normalized ray direction in world space. +fn uv_to_ray_direction(uv: vec2) -> vec3 { + // Using world positions of the fragment and camera to calculate a ray direction + // breaks down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.view_from_clip * vec4( + uv_to_ndc(uv), + 1.0, + 1.0, + ); + + let view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; + // Transforming the view space ray direction by the inverse view matrix, transforms the + // direction to world space. Note that the w element is set to 0.0, as this is a + // vector direction, not a position, That causes the matrix multiplication to ignore + // the translations from the view matrix. + let ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz; + + return normalize(ray_direction); +} + +fn zenith_azimuth_to_ray_dir(zenith: f32, azimuth: f32) -> vec3 { + let sin_zenith = sin(zenith); + let mu = cos(zenith); + let sin_azimuth = sin(azimuth); + let cos_azimuth = cos(azimuth); + return vec3(sin_azimuth * sin_zenith, mu, -cos_azimuth * sin_zenith); +} + +struct RaymarchSegment { + start: f32, + end: f32, +} + +fn get_raymarch_segment(r: f32, mu: f32) -> RaymarchSegment { + // Get both intersection points with atmosphere + let atmosphere_intersections = ray_sphere_intersect(r, mu, atmosphere.top_radius); + let ground_intersections = ray_sphere_intersect(r, mu, atmosphere.bottom_radius); + + var segment: RaymarchSegment; + + if r < atmosphere.bottom_radius { + // Inside planet - start from bottom of atmosphere + segment.start = ground_intersections.y; // Use second intersection point with ground + segment.end = atmosphere_intersections.y; + } else if r < atmosphere.top_radius { + // Inside atmosphere + segment.start = 0.0; + segment.end = select(atmosphere_intersections.y, ground_intersections.x, ray_intersects_ground(r, mu)); + } else { + // Outside atmosphere + if atmosphere_intersections.x < 0.0 { + // No intersection with atmosphere + return segment; + } + // Start at atmosphere entry, end at exit or ground + segment.start = atmosphere_intersections.x; + segment.end = select(atmosphere_intersections.y, ground_intersections.x, ray_intersects_ground(r, mu)); + } + + return segment; +} + +struct RaymarchResult { + inscattering: vec3, + transmittance: vec3, +} + +fn raymarch_atmosphere( + pos: vec3, + ray_dir: vec3, + t_max: f32, + max_samples: u32, + uv: vec2, + ground: bool +) -> RaymarchResult { + let r = length(pos); + let up = normalize(pos); + let mu = dot(ray_dir, up); + + // Optimization: Reduce sample count at close proximity to the scene + let sample_count = mix(1.0, f32(max_samples), saturate(t_max * 0.01)); + + let segment = get_raymarch_segment(r, mu); + let t_start = segment.start; + var t_end = segment.end; + + t_end = min(t_end, t_max); + let t_total = t_end - t_start; + + var result: RaymarchResult; + result.inscattering = vec3(0.0); + result.transmittance = vec3(1.0); + + // Skip if invalid segment + if t_total <= 0.0 { + return result; + } + + var prev_t = t_start; + var optical_depth = vec3(0.0); + for (var s = 0.0; s < sample_count; s += 1.0) { + // Linear distribution from atmosphere entry to exit/ground + let t_i = t_start + t_total * (s + MIDPOINT_RATIO) / sample_count; + let dt_i = (t_i - prev_t); + prev_t = t_i; + + let sample_pos = pos + ray_dir * t_i; + let local_r = length(sample_pos); + let local_up = normalize(sample_pos); + + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt_i; + optical_depth += sample_optical_depth; + let sample_transmittance = exp(-sample_optical_depth); + + let inscattering = sample_local_inscattering( + scattering, + ray_dir, + sample_pos + ); + + let s_int = (inscattering - inscattering * sample_transmittance) / max(extinction, MIN_EXTINCTION); + result.inscattering += result.transmittance * s_int; + + result.transmittance *= sample_transmittance; + if all(result.transmittance < vec3(0.001)) { + break; + } + } + + // include reflected luminance from planet ground + if ground && ray_intersects_ground(r, mu) { + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + let light_dir = (*light).direction_to_light; + let light_color = (*light).color.rgb; + let transmittance_to_ground = exp(-optical_depth); + // position on the sphere and get the sphere normal (up) + let sphere_point = pos + ray_dir * t_end; + let sphere_normal = normalize(sphere_point); + let mu_light = dot(light_dir, sphere_normal); + let transmittance_to_light = sample_transmittance_lut(0.0, mu_light); + let light_luminance = transmittance_to_light * max(mu_light, 0.0) * light_color; + // Normalized Lambert BRDF + let ground_luminance = transmittance_to_ground * atmosphere.ground_albedo / PI; + result.inscattering += ground_luminance * light_luminance; + } + } + + return result; +} diff --git a/vendor/bevy_pbr/src/atmosphere/mod.rs b/vendor/bevy_pbr/src/atmosphere/mod.rs new file mode 100644 index 00000000..959baf68 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/mod.rs @@ -0,0 +1,449 @@ +//! Procedural Atmospheric Scattering. +//! +//! This plugin implements [Hillaire's 2020 paper](https://sebh.github.io/publications/egsr2020.pdf) +//! on real-time atmospheric scattering. While it *will* work simply as a +//! procedural skybox, it also does much more. It supports dynamic time-of- +//! -day, multiple directional lights, and since it's applied as a post-processing +//! effect *on top* of the existing skybox, a starry skybox would automatically +//! show based on the time of day. Scattering in front of terrain (similar +//! to distance fog, but more complex) is handled as well, and takes into +//! account the directional light color and direction. +//! +//! Adding the [`Atmosphere`] component to a 3d camera will enable the effect, +//! which by default is set to look similar to Earth's atmosphere. See the +//! documentation on the component itself for information regarding its fields. +//! +//! Performance-wise, the effect should be fairly cheap since the LUTs (Look +//! Up Tables) that encode most of the data are small, and take advantage of the +//! fact that the atmosphere is symmetric. Performance is also proportional to +//! the number of directional lights in the scene. In order to tune +//! performance more finely, the [`AtmosphereSettings`] camera component +//! manages the size of each LUT and the sample count for each ray. +//! +//! Given how similar it is to [`crate::volumetric_fog`], it might be expected +//! that these two modules would work together well. However for now using both +//! at once is untested, and might not be physically accurate. These may be +//! integrated into a single module in the future. +//! +//! On web platforms, atmosphere rendering will look slightly different. Specifically, when calculating how light travels +//! through the atmosphere, we use a simpler averaging technique instead of the more +//! complex blending operations. This difference will be resolved for WebGPU in a future release. +//! +//! [Shadertoy]: https://www.shadertoy.com/view/slSXRW +//! +//! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere + +mod environment; +mod node; +pub mod resources; + +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{embedded_asset, AssetId, Handle}; +use bevy_camera::Camera3d; +use bevy_core_pipeline::core_3d::graph::Node3d; +use bevy_ecs::{ + component::Component, + query::{Changed, QueryItem, With}, + schedule::IntoScheduleConfigs, + system::{lifetimeless::Read, Query}, +}; +use bevy_math::{UVec2, UVec3, Vec3}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + extract_component::UniformComponentPlugin, + render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, + view::Hdr, + RenderStartup, +}; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::{TextureFormat, TextureUsages}, + renderer::RenderAdapter, + Render, RenderApp, RenderSystems, +}; + +use bevy_core_pipeline::core_3d::graph::Core3d; +use bevy_shader::load_shader_library; +use environment::{ + init_atmosphere_probe_layout, init_atmosphere_probe_pipeline, + prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components, + prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode, +}; +use resources::{ + prepare_atmosphere_transforms, prepare_atmosphere_uniforms, queue_render_sky_pipelines, + AtmosphereTransforms, GpuAtmosphere, RenderSkyBindGroupLayouts, +}; +use tracing::warn; + +use crate::{ + medium::ScatteringMedium, + resources::{init_atmosphere_buffer, write_atmosphere_buffer}, +}; + +use self::{ + node::{AtmosphereLutsNode, AtmosphereNode, RenderSkyNode}, + resources::{ + prepare_atmosphere_bind_groups, prepare_atmosphere_textures, AtmosphereBindGroupLayouts, + AtmosphereLutPipelines, AtmosphereSampler, + }, +}; + +#[doc(hidden)] +pub struct AtmospherePlugin; + +impl Plugin for AtmospherePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "types.wgsl"); + load_shader_library!(app, "functions.wgsl"); + load_shader_library!(app, "bruneton_functions.wgsl"); + load_shader_library!(app, "bindings.wgsl"); + + embedded_asset!(app, "transmittance_lut.wgsl"); + embedded_asset!(app, "multiscattering_lut.wgsl"); + embedded_asset!(app, "sky_view_lut.wgsl"); + embedded_asset!(app, "aerial_view_lut.wgsl"); + embedded_asset!(app, "render_sky.wgsl"); + embedded_asset!(app, "environment.wgsl"); + + app.add_plugins(( + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )) + .add_systems(Update, prepare_atmosphere_probe_components); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let render_adapter = render_app.world().resource::(); + + if !render_adapter + .get_downlevel_capabilities() + .flags + .contains(DownlevelFlags::COMPUTE_SHADERS) + { + warn!("AtmospherePlugin not loaded. GPU lacks support for compute shaders."); + return; + } + + if !render_adapter + .get_texture_format_features(TextureFormat::Rgba16Float) + .allowed_usages + .contains(TextureUsages::STORAGE_BINDING) + { + warn!("AtmospherePlugin not loaded. GPU lacks support: TextureFormat::Rgba16Float does not support TextureUsages::STORAGE_BINDING."); + return; + } + + render_app + .insert_resource(AtmosphereBindGroupLayouts::new()) + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems( + RenderStartup, + ( + init_atmosphere_probe_layout, + init_atmosphere_probe_pipeline, + init_atmosphere_buffer, + ) + .chain(), + ) + .add_systems( + Render, + ( + configure_camera_depth_usages.in_set(RenderSystems::ManageViews), + queue_render_sky_pipelines.in_set(RenderSystems::Queue), + prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources), + prepare_probe_textures + .in_set(RenderSystems::PrepareResources) + .after(prepare_atmosphere_textures), + prepare_atmosphere_uniforms + .before(RenderSystems::PrepareResources) + .after(RenderSystems::PrepareAssets), + prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), + prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), + write_atmosphere_buffer.in_set(RenderSystems::PrepareResources), + ), + ) + .add_render_graph_node::>( + Core3d, + AtmosphereNode::RenderLuts, + ) + .add_render_graph_edges( + Core3d, + ( + // END_PRE_PASSES -> RENDER_LUTS -> MAIN_PASS + Node3d::EndPrepasses, + AtmosphereNode::RenderLuts, + Node3d::StartMainPass, + ), + ) + .add_render_graph_node::>( + Core3d, + AtmosphereNode::RenderSky, + ) + .add_render_graph_node::(Core3d, AtmosphereNode::Environment) + .add_render_graph_edges( + Core3d, + ( + Node3d::MainOpaquePass, + AtmosphereNode::RenderSky, + Node3d::MainTransparentPass, + ), + ); + } +} + +/// Enables atmospheric scattering for an HDR camera. +#[derive(Clone, Component)] +#[require(AtmosphereSettings, Hdr)] +pub struct Atmosphere { + /// Radius of the planet + /// + /// units: m + pub bottom_radius: f32, + + /// Radius at which we consider the atmosphere to 'end' for our + /// calculations (from center of planet) + /// + /// units: m + pub top_radius: f32, + + /// An approximation of the average albedo (or color, roughly) of the + /// planet's surface. This is used when calculating multiscattering. + /// + /// units: N/A + pub ground_albedo: Vec3, + + /// A handle to a [`ScatteringMedium`], which describes the substance + /// of the atmosphere and how it scatters light. + pub medium: Handle, +} + +impl Atmosphere { + pub fn earthlike(medium: Handle) -> Self { + const EARTH_BOTTOM_RADIUS: f32 = 6_360_000.0; + const EARTH_TOP_RADIUS: f32 = 6_460_000.0; + const EARTH_ALBEDO: Vec3 = Vec3::splat(0.3); + Self { + bottom_radius: EARTH_BOTTOM_RADIUS, + top_radius: EARTH_TOP_RADIUS, + ground_albedo: EARTH_ALBEDO, + medium, + } + } +} + +impl ExtractComponent for Atmosphere { + type QueryData = Read; + + type QueryFilter = With; + + type Out = ExtractedAtmosphere; + + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(ExtractedAtmosphere { + bottom_radius: item.bottom_radius, + top_radius: item.top_radius, + ground_albedo: item.ground_albedo, + medium: item.medium.id(), + }) + } +} + +/// The render-world representation of an `Atmosphere`, but which +/// hasn't been converted into shader uniforms yet. +#[derive(Clone, Component)] +pub struct ExtractedAtmosphere { + pub bottom_radius: f32, + pub top_radius: f32, + pub ground_albedo: Vec3, + pub medium: AssetId, +} + +/// This component controls the resolution of the atmosphere LUTs, and +/// how many samples are used when computing them. +/// +/// The transmittance LUT stores the transmittance from a point in the +/// atmosphere to the outer edge of the atmosphere in any direction, +/// parametrized by the point's radius and the cosine of the zenith angle +/// of the ray. +/// +/// The multiscattering LUT stores the factor representing luminance scattered +/// towards the camera with scattering order >2, parametrized by the point's radius +/// and the cosine of the zenith angle of the sun. +/// +/// The sky-view lut is essentially the actual skybox, storing the light scattered +/// towards the camera in every direction with a cubemap. +/// +/// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance +/// scattered towards the camera at each point (RGB channels), alongside the average +/// transmittance to that point (A channel). +#[derive(Clone, Component, Reflect)] +#[reflect(Clone, Default)] +pub struct AtmosphereSettings { + /// The size of the transmittance LUT + pub transmittance_lut_size: UVec2, + + /// The size of the multiscattering LUT + pub multiscattering_lut_size: UVec2, + + /// The size of the sky-view LUT. + pub sky_view_lut_size: UVec2, + + /// The size of the aerial-view LUT. + pub aerial_view_lut_size: UVec3, + + /// The number of points to sample along each ray when + /// computing the transmittance LUT + pub transmittance_lut_samples: u32, + + /// The number of rays to sample when computing each + /// pixel of the multiscattering LUT + pub multiscattering_lut_dirs: u32, + + /// The number of points to sample when integrating along each + /// multiscattering ray + pub multiscattering_lut_samples: u32, + + /// The number of points to sample along each ray when + /// computing the sky-view LUT. + pub sky_view_lut_samples: u32, + + /// The number of points to sample for each slice along the z-axis + /// of the aerial-view LUT. + pub aerial_view_lut_samples: u32, + + /// The maximum distance from the camera to evaluate the + /// aerial view LUT. The slices along the z-axis of the + /// texture will be distributed linearly from the camera + /// to this value. + /// + /// units: m + pub aerial_view_lut_max_distance: f32, + + /// A conversion factor between scene units and meters, used to + /// ensure correctness at different length scales. + pub scene_units_to_m: f32, + + /// The number of points to sample for each fragment when the using + /// ray marching to render the sky + pub sky_max_samples: u32, + + /// The rendering method to use for the atmosphere. + pub rendering_method: AtmosphereMode, +} + +impl Default for AtmosphereSettings { + fn default() -> Self { + Self { + transmittance_lut_size: UVec2::new(256, 128), + transmittance_lut_samples: 40, + multiscattering_lut_size: UVec2::new(32, 32), + multiscattering_lut_dirs: 64, + multiscattering_lut_samples: 20, + sky_view_lut_size: UVec2::new(400, 200), + sky_view_lut_samples: 16, + aerial_view_lut_size: UVec3::new(32, 32, 32), + aerial_view_lut_samples: 10, + aerial_view_lut_max_distance: 3.2e4, + scene_units_to_m: 1.0, + sky_max_samples: 16, + rendering_method: AtmosphereMode::LookupTexture, + } + } +} + +#[derive(Clone, Component, Reflect, ShaderType)] +#[reflect(Default)] +pub struct GpuAtmosphereSettings { + pub transmittance_lut_size: UVec2, + pub multiscattering_lut_size: UVec2, + pub sky_view_lut_size: UVec2, + pub aerial_view_lut_size: UVec3, + pub transmittance_lut_samples: u32, + pub multiscattering_lut_dirs: u32, + pub multiscattering_lut_samples: u32, + pub sky_view_lut_samples: u32, + pub aerial_view_lut_samples: u32, + pub aerial_view_lut_max_distance: f32, + pub scene_units_to_m: f32, + pub sky_max_samples: u32, + pub rendering_method: u32, +} + +impl Default for GpuAtmosphereSettings { + fn default() -> Self { + AtmosphereSettings::default().into() + } +} + +impl From for GpuAtmosphereSettings { + fn from(s: AtmosphereSettings) -> Self { + Self { + transmittance_lut_size: s.transmittance_lut_size, + multiscattering_lut_size: s.multiscattering_lut_size, + sky_view_lut_size: s.sky_view_lut_size, + aerial_view_lut_size: s.aerial_view_lut_size, + transmittance_lut_samples: s.transmittance_lut_samples, + multiscattering_lut_dirs: s.multiscattering_lut_dirs, + multiscattering_lut_samples: s.multiscattering_lut_samples, + sky_view_lut_samples: s.sky_view_lut_samples, + aerial_view_lut_samples: s.aerial_view_lut_samples, + aerial_view_lut_max_distance: s.aerial_view_lut_max_distance, + scene_units_to_m: s.scene_units_to_m, + sky_max_samples: s.sky_max_samples, + rendering_method: s.rendering_method as u32, + } + } +} + +impl ExtractComponent for GpuAtmosphereSettings { + type QueryData = Read; + + type QueryFilter = (With, With); + + type Out = GpuAtmosphereSettings; + + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(item.clone().into()) + } +} + +fn configure_camera_depth_usages( + mut cameras: Query<&mut Camera3d, (Changed, With)>, +) { + for mut camera in &mut cameras { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} + +/// Selects how the atmosphere is rendered. Choose based on scene scale and +/// volumetric shadow quality, and based on performance needs. +#[repr(u32)] +#[derive(Clone, Default, Reflect, Copy)] +pub enum AtmosphereMode { + /// High-performance solution tailored to scenes that are mostly inside of the atmosphere. + /// Uses a set of lookup textures to approximate scattering integration. + /// Slightly less accurate for very long-distance/space views (lighting precision + /// tapers as the camera moves far from the scene origin) and for sharp volumetric + /// (cloud/fog) shadows. + #[default] + LookupTexture = 0, + /// Slower, more accurate rendering method for any type of scene. + /// Integrates the scattering numerically with raymarching and produces sharp volumetric + /// (cloud/fog) shadows. + /// Best for cinematic shots, planets seen from orbit, and scenes requiring + /// accurate long-distance lighting. + Raymarched = 1, +} diff --git a/vendor/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl b/vendor/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl new file mode 100644 index 00000000..132bf4f0 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/multiscattering_lut.wgsl @@ -0,0 +1,136 @@ +#import bevy_pbr::{ + atmosphere::{ + bindings::{atmosphere, settings}, + functions::{ + multiscattering_lut_uv_to_r_mu, sample_transmittance_lut, + get_local_r, get_local_up, sample_density_lut, FRAC_4_PI, + max_atmosphere_distance, + MIN_EXTINCTION, ABSORPTION_DENSITY, SCATTERING_DENSITY, + }, + bruneton_functions::ray_intersects_ground + } +} + +#import bevy_render::maths::{PI, PI_2} + +const PHI_2: vec2 = vec2(1.3247179572447460259609088, 1.7548776662466927600495087); + +@group(0) @binding(13) var multiscattering_lut_out: texture_storage_2d; + +fn s2_sequence(n: u32) -> vec2 { + return fract(0.5 + f32(n) * PHI_2); +} + +// Lambert equal-area projection. +fn uv_to_sphere(uv: vec2) -> vec3 { + let phi = PI_2 * uv.y; + let sin_lambda = 2 * uv.x - 1; + let cos_lambda = sqrt(1 - sin_lambda * sin_lambda); + + return vec3(cos_lambda * cos(phi), cos_lambda * sin(phi), sin_lambda); +} + +// Shared memory arrays for workgroup communication +var multi_scat_shared_mem: array, 64>; +var l_shared_mem: array, 64>; + +@compute +@workgroup_size(1, 1, 64) +fn main(@builtin(global_invocation_id) global_id: vec3) { + var uv = (vec2(global_id.xy) + 0.5) / vec2(settings.multiscattering_lut_size); + + let r_mu = multiscattering_lut_uv_to_r_mu(uv); + let light_dir = normalize(vec3(0.0, r_mu.y, -1.0)); + + let ray_dir = uv_to_sphere(s2_sequence(global_id.z)); + let ms_sample = sample_multiscattering_dir(r_mu.x, ray_dir, light_dir); + + // Calculate the contribution for this sample + let sphere_solid_angle = 4.0 * PI; + let sample_weight = sphere_solid_angle / 64.0; + multi_scat_shared_mem[global_id.z] = ms_sample.f_ms * sample_weight; + l_shared_mem[global_id.z] = ms_sample.l_2 * sample_weight; + + workgroupBarrier(); + + // Parallel reduction bitshift to the right to divide by 2 each step + for (var step = 32u; step > 0u; step >>= 1u) { + if global_id.z < step { + multi_scat_shared_mem[global_id.z] += multi_scat_shared_mem[global_id.z + step]; + l_shared_mem[global_id.z] += l_shared_mem[global_id.z + step]; + } + workgroupBarrier(); + } + + if global_id.z > 0u { + return; + } + + // Apply isotropic phase function + let f_ms = multi_scat_shared_mem[0] * FRAC_4_PI; + let l_2 = l_shared_mem[0] * FRAC_4_PI; + + // Equation 10 from the paper: Geometric series for infinite scattering + let psi_ms = l_2 / (1.0 - f_ms); + textureStore(multiscattering_lut_out, global_id.xy, vec4(psi_ms, 1.0)); +} + +struct MultiscatteringSample { + l_2: vec3, + f_ms: vec3, +}; + +fn sample_multiscattering_dir(r: f32, ray_dir: vec3, light_dir: vec3) -> MultiscatteringSample { + // get the cosine of the zenith angle of the view direction with respect to the light direction + let mu_view = ray_dir.y; + let t_max = max_atmosphere_distance(r, mu_view); + + let dt = t_max / f32(settings.multiscattering_lut_samples); + var optical_depth = vec3(0.0); + + var l_2 = vec3(0.0); + var f_ms = vec3(0.0); + var throughput = vec3(1.0); + for (var i: u32 = 0u; i < settings.multiscattering_lut_samples; i++) { + let t_i = dt * (f32(i) + 0.5); + let local_r = get_local_r(r, mu_view, t_i); + let local_up = get_local_up(r, t_i, ray_dir); + + let absorption = sample_density_lut(local_r, ABSORPTION_DENSITY); + let scattering = sample_density_lut(local_r, SCATTERING_DENSITY); + let extinction = absorption + scattering; + + let sample_optical_depth = extinction * dt; + let sample_transmittance = exp(-sample_optical_depth); + optical_depth += sample_optical_depth; + + let ms = scattering; + let ms_int = (ms - ms * sample_transmittance) / max(extinction, MIN_EXTINCTION); + f_ms += throughput * ms_int; + + let mu_light = dot(light_dir, local_up); + let transmittance_to_light = sample_transmittance_lut(local_r, mu_light); + let shadow_factor = transmittance_to_light * f32(!ray_intersects_ground(local_r, mu_light)); + + let s = scattering * shadow_factor * FRAC_4_PI; + let s_int = (s - s * sample_transmittance) / max(extinction, MIN_EXTINCTION); + l_2 += throughput * s_int; + + throughput *= sample_transmittance; + if all(throughput < vec3(0.001)) { + break; + } + } + + //include reflected luminance from planet ground + if ray_intersects_ground(r, mu_view) { + let transmittance_to_ground = exp(-optical_depth); + let local_up = get_local_up(r, t_max, ray_dir); + let mu_light = dot(light_dir, local_up); + let transmittance_to_light = sample_transmittance_lut(0.0, mu_light); + let ground_luminance = transmittance_to_light * transmittance_to_ground * max(mu_light, 0.0) * atmosphere.ground_albedo; + l_2 += ground_luminance; + } + + return MultiscatteringSample(l_2, f_ms); +} diff --git a/vendor/bevy_pbr/src/atmosphere/node.rs b/vendor/bevy_pbr/src/atmosphere/node.rs new file mode 100644 index 00000000..9f640b52 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/node.rs @@ -0,0 +1,242 @@ +use bevy_camera::{MainPassResolutionOverride, Viewport}; +use bevy_ecs::{query::QueryItem, system::lifetimeless::Read, world::World}; +use bevy_math::{UVec2, Vec3Swizzles}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + extract_component::DynamicUniformIndex, + render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode}, + render_resource::{ComputePass, ComputePassDescriptor, PipelineCache, RenderPassDescriptor}, + renderer::RenderContext, + view::{ViewTarget, ViewUniformOffset}, +}; + +use crate::{resources::GpuAtmosphere, ViewLightsUniformOffset}; + +use super::{ + resources::{ + AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereTransformsOffset, + RenderSkyPipelineId, + }, + GpuAtmosphereSettings, +}; + +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] +pub enum AtmosphereNode { + RenderLuts, + RenderSky, + Environment, +} + +#[derive(Default)] +pub(super) struct AtmosphereLutsNode {} + +impl ViewNode for AtmosphereLutsNode { + type ViewQuery = ( + Read, + Read, + Read>, + Read>, + Read, + Read, + Read, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + settings, + bind_groups, + atmosphere_uniforms_offset, + settings_uniforms_offset, + atmosphere_transforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let ( + Some(transmittance_lut_pipeline), + Some(multiscattering_lut_pipeline), + Some(sky_view_lut_pipeline), + Some(aerial_view_lut_pipeline), + ) = ( + pipeline_cache.get_compute_pipeline(pipelines.transmittance_lut), + pipeline_cache.get_compute_pipeline(pipelines.multiscattering_lut), + pipeline_cache.get_compute_pipeline(pipelines.sky_view_lut), + pipeline_cache.get_compute_pipeline(pipelines.aerial_view_lut), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let command_encoder = render_context.command_encoder(); + + let mut luts_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("atmosphere_luts"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut luts_pass, "atmosphere_luts"); + + fn dispatch_2d(compute_pass: &mut ComputePass, size: UVec2) { + const WORKGROUP_SIZE: u32 = 16; + let workgroups_x = size.x.div_ceil(WORKGROUP_SIZE); + let workgroups_y = size.y.div_ceil(WORKGROUP_SIZE); + compute_pass.dispatch_workgroups(workgroups_x, workgroups_y, 1); + } + + // Transmittance LUT + + luts_pass.set_pipeline(transmittance_lut_pipeline); + luts_pass.set_bind_group( + 0, + &bind_groups.transmittance_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + ], + ); + + dispatch_2d(&mut luts_pass, settings.transmittance_lut_size); + + // Multiscattering LUT + + luts_pass.set_pipeline(multiscattering_lut_pipeline); + luts_pass.set_bind_group( + 0, + &bind_groups.multiscattering_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + ], + ); + + luts_pass.dispatch_workgroups( + settings.multiscattering_lut_size.x, + settings.multiscattering_lut_size.y, + 1, + ); + + // Sky View LUT + + luts_pass.set_pipeline(sky_view_lut_pipeline); + luts_pass.set_bind_group( + 0, + &bind_groups.sky_view_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + atmosphere_transforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + dispatch_2d(&mut luts_pass, settings.sky_view_lut_size); + + // Aerial View LUT + + luts_pass.set_pipeline(aerial_view_lut_pipeline); + luts_pass.set_bind_group( + 0, + &bind_groups.aerial_view_lut, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + dispatch_2d(&mut luts_pass, settings.aerial_view_lut_size.xy()); + + pass_span.end(&mut luts_pass); + + Ok(()) + } +} + +#[derive(Default)] +pub(super) struct RenderSkyNode; + +impl ViewNode for RenderSkyNode { + type ViewQuery = ( + Read, + Read, + Read, + Read>, + Read>, + Read, + Read, + Read, + Read, + Option>, + ); + + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + camera, + atmosphere_bind_groups, + view_target, + atmosphere_uniforms_offset, + settings_uniforms_offset, + atmosphere_transforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + render_sky_pipeline_id, + resolution_override, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let Some(render_sky_pipeline) = + pipeline_cache.get_render_pipeline(render_sky_pipeline_id.0) + else { + return Ok(()); + }; //TODO: warning + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_sky_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("render_sky"), + color_attachments: &[Some(view_target.get_color_attachment())], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_sky_pass, "render_sky"); + + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_sky_pass.set_camera_viewport(&viewport); + } + + render_sky_pass.set_render_pipeline(render_sky_pipeline); + render_sky_pass.set_bind_group( + 0, + &atmosphere_bind_groups.render_sky, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + atmosphere_transforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + render_sky_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_sky_pass); + + Ok(()) + } +} diff --git a/vendor/bevy_pbr/src/atmosphere/render_sky.wgsl b/vendor/bevy_pbr/src/atmosphere/render_sky.wgsl new file mode 100644 index 00000000..7893fb07 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -0,0 +1,80 @@ +enable dual_source_blending; + +#import bevy_pbr::atmosphere::{ + bindings::{view, settings}, + functions::{ + sample_transmittance_lut, sample_transmittance_lut_segment, + sample_sky_view_lut, direction_world_to_atmosphere, + uv_to_ray_direction, uv_to_ndc, sample_aerial_view_lut, + sample_sun_radiance, ndc_to_camera_dist, raymarch_atmosphere, + get_view_position, max_atmosphere_distance + }, +}; + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +#ifdef MULTISAMPLED +@group(0) @binding(13) var depth_texture: texture_depth_multisampled_2d; +#else +@group(0) @binding(13) var depth_texture: texture_depth_2d; +#endif + +struct RenderSkyOutput { +#ifdef DUAL_SOURCE_BLENDING + @location(0) @blend_src(0) inscattering: vec4, + @location(0) @blend_src(1) transmittance: vec4, +#else + @location(0) inscattering: vec4, +#endif +} + +@fragment +fn main(in: FullscreenVertexOutput) -> RenderSkyOutput { + let depth = textureLoad(depth_texture, vec2(in.position.xy), 0); + + let ray_dir_ws = uv_to_ray_direction(in.uv); + let world_pos = get_view_position(); + let r = length(world_pos); + let up = normalize(world_pos); + let mu = dot(ray_dir_ws, up); + let max_samples = settings.sky_max_samples; + let should_raymarch = settings.rendering_method == 1u; + + var transmittance: vec3; + var inscattering: vec3; + + let sun_radiance = sample_sun_radiance(ray_dir_ws); + + if depth == 0.0 { + let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws, up); + transmittance = sample_transmittance_lut(r, mu); + inscattering = sample_sky_view_lut(r, ray_dir_as); + if should_raymarch { + let t_max = max_atmosphere_distance(r, mu); + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t_max, max_samples, in.uv, true); + inscattering = result.inscattering; + transmittance = result.transmittance; + } + inscattering += sun_radiance * transmittance; + } else { + let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth)); + inscattering = sample_aerial_view_lut(in.uv, t); + transmittance = sample_transmittance_lut_segment(r, mu, t); + if should_raymarch { + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t, max_samples, in.uv, false); + inscattering = result.inscattering; + transmittance = result.transmittance; + } + } + + // exposure compensation + inscattering *= view.exposure; + +#ifdef DUAL_SOURCE_BLENDING + return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0)); +#else + let mean_transmittance = (transmittance.r + transmittance.g + transmittance.b) / 3.0; + return RenderSkyOutput(vec4(inscattering, mean_transmittance)); +#endif + +} diff --git a/vendor/bevy_pbr/src/atmosphere/resources.rs b/vendor/bevy_pbr/src/atmosphere/resources.rs new file mode 100644 index 00000000..1d2657d0 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/resources.rs @@ -0,0 +1,809 @@ +use crate::{ + ExtractedAtmosphere, GpuLights, GpuScatteringMedium, LightMeta, ScatteringMedium, + ScatteringMediumSampler, +}; +use bevy_asset::{load_embedded_asset, AssetId, Handle}; +use bevy_camera::{Camera, Camera3d}; +use bevy_core_pipeline::FullscreenShader; +use bevy_derive::Deref; +use bevy_ecs::{ + component::Component, + entity::Entity, + error::BevyError, + query::With, + resource::Resource, + system::{Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::ToExtents; +use bevy_math::{Affine3A, Mat4, Vec3, Vec3A}; +use bevy_render::{ + extract_component::ComponentUniforms, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewUniform, ViewUniforms}, +}; +use bevy_shader::Shader; +use bevy_utils::default; + +use super::GpuAtmosphereSettings; + +#[derive(Resource)] +pub(crate) struct AtmosphereBindGroupLayouts { + pub transmittance_lut: BindGroupLayoutDescriptor, + pub multiscattering_lut: BindGroupLayoutDescriptor, + pub sky_view_lut: BindGroupLayoutDescriptor, + pub aerial_view_lut: BindGroupLayoutDescriptor, +} + +#[derive(Resource)] +pub(crate) struct RenderSkyBindGroupLayouts { + pub render_sky: BindGroupLayoutDescriptor, + pub render_sky_msaa: BindGroupLayoutDescriptor, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, +} + +impl AtmosphereBindGroupLayouts { + pub fn new() -> Self { + let transmittance_lut = BindGroupLayoutDescriptor::new( + "transmittance_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // transmittance lut storage texture + ( + 13, + texture_storage_2d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let multiscattering_lut = BindGroupLayoutDescriptor::new( + "multiscattering_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (12, sampler(SamplerBindingType::Filtering)), + // multiscattering lut storage texture + ( + 13, + texture_storage_2d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let sky_view_lut = BindGroupLayoutDescriptor::new( + "sky_view_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (9, texture_2d(TextureSampleType::default())), // multiscattering + (12, sampler(SamplerBindingType::Filtering)), + // sky view lut storage texture + ( + 13, + texture_storage_2d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + let aerial_view_lut = BindGroupLayoutDescriptor::new( + "aerial_view_lut_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::COMPUTE, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (9, texture_2d(TextureSampleType::default())), // multiscattering + (12, sampler(SamplerBindingType::Filtering)), + // eerial view lut storage texture + ( + 13, + texture_storage_3d( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ), + ); + + Self { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + } + } +} + +impl FromWorld for RenderSkyBindGroupLayouts { + fn from_world(world: &mut World) -> Self { + let render_sky = BindGroupLayoutDescriptor::new( + "render_sky_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (9, texture_2d(TextureSampleType::default())), // multiscattering + (10, texture_2d(TextureSampleType::default())), // sky view + (11, texture_3d(TextureSampleType::default())), // aerial view + (12, sampler(SamplerBindingType::Filtering)), + // view depth texture + (13, texture_2d(TextureSampleType::Depth)), + ), + ), + ); + + let render_sky_msaa = BindGroupLayoutDescriptor::new( + "render_sky_msaa_bind_group_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(true)), + (2, uniform_buffer::(true)), + (3, uniform_buffer::(true)), + (4, uniform_buffer::(true)), + // scattering medium luts and sampler + (5, texture_2d(TextureSampleType::default())), + (6, texture_2d(TextureSampleType::default())), + (7, sampler(SamplerBindingType::Filtering)), + // atmosphere luts and sampler + (8, texture_2d(TextureSampleType::default())), // transmittance + (9, texture_2d(TextureSampleType::default())), // multiscattering + (10, texture_2d(TextureSampleType::default())), // sky view + (11, texture_3d(TextureSampleType::default())), // aerial view + (12, sampler(SamplerBindingType::Filtering)), + // view depth texture + (13, texture_2d_multisampled(TextureSampleType::Depth)), + ), + ), + ); + + Self { + render_sky, + render_sky_msaa, + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "render_sky.wgsl"), + } + } +} + +#[derive(Resource, Deref)] +pub struct AtmosphereSampler(Sampler); + +impl FromWorld for AtmosphereSampler { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let sampler = render_device.create_sampler(&SamplerDescriptor { + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..Default::default() + }); + + Self(sampler) + } +} + +#[derive(Resource)] +pub(crate) struct AtmosphereLutPipelines { + pub transmittance_lut: CachedComputePipelineId, + pub multiscattering_lut: CachedComputePipelineId, + pub sky_view_lut: CachedComputePipelineId, + pub aerial_view_lut: CachedComputePipelineId, +} + +impl FromWorld for AtmosphereLutPipelines { + fn from_world(world: &mut World) -> Self { + let pipeline_cache = world.resource::(); + let layouts = world.resource::(); + + let transmittance_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("transmittance_lut_pipeline".into()), + layout: vec![layouts.transmittance_lut.clone()], + shader: load_embedded_asset!(world, "transmittance_lut.wgsl"), + ..default() + }); + + let multiscattering_lut = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("multi_scattering_lut_pipeline".into()), + layout: vec![layouts.multiscattering_lut.clone()], + shader: load_embedded_asset!(world, "multiscattering_lut.wgsl"), + ..default() + }); + + let sky_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("sky_view_lut_pipeline".into()), + layout: vec![layouts.sky_view_lut.clone()], + shader: load_embedded_asset!(world, "sky_view_lut.wgsl"), + ..default() + }); + + let aerial_view_lut = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("aerial_view_lut_pipeline".into()), + layout: vec![layouts.aerial_view_lut.clone()], + shader: load_embedded_asset!(world, "aerial_view_lut.wgsl"), + ..default() + }); + + Self { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + } + } +} + +#[derive(Component)] +pub(crate) struct RenderSkyPipelineId(pub CachedRenderPipelineId); + +#[derive(Copy, Clone, Hash, PartialEq, Eq)] +pub(crate) struct RenderSkyPipelineKey { + pub msaa_samples: u32, + pub dual_source_blending: bool, +} + +impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { + type Key = RenderSkyPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + + if key.msaa_samples > 1 { + shader_defs.push("MULTISAMPLED".into()); + } + if key.dual_source_blending { + shader_defs.push("DUAL_SOURCE_BLENDING".into()); + } + + let dst_factor = if key.dual_source_blending { + BlendFactor::Src1 + } else { + BlendFactor::SrcAlpha + }; + + RenderPipelineDescriptor { + label: Some(format!("render_sky_pipeline_{}", key.msaa_samples).into()), + layout: vec![if key.msaa_samples == 1 { + self.render_sky.clone() + } else { + self.render_sky_msaa.clone() + }], + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: TextureFormat::Rgba16Float, + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::One, + dst_factor, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::Zero, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + })], + ..default() + }), + multisample: MultisampleState { + count: key.msaa_samples, + ..default() + }, + ..default() + } + } +} + +pub(super) fn queue_render_sky_pipelines( + views: Query<(Entity, &Msaa), (With, With)>, + pipeline_cache: Res, + layouts: Res, + mut specializer: ResMut>, + render_device: Res, + mut commands: Commands, +) { + for (entity, msaa) in &views { + let id = specializer.specialize( + &pipeline_cache, + &layouts, + RenderSkyPipelineKey { + msaa_samples: msaa.samples(), + dual_source_blending: render_device + .features() + .contains(WgpuFeatures::DUAL_SOURCE_BLENDING), + }, + ); + commands.entity(entity).insert(RenderSkyPipelineId(id)); + } +} + +#[derive(Component)] +pub struct AtmosphereTextures { + pub transmittance_lut: CachedTexture, + pub multiscattering_lut: CachedTexture, + pub sky_view_lut: CachedTexture, + pub aerial_view_lut: CachedTexture, +} + +pub(super) fn prepare_atmosphere_textures( + views: Query<(Entity, &GpuAtmosphereSettings), With>, + render_device: Res, + mut texture_cache: ResMut, + mut commands: Commands, +) { + for (entity, lut_settings) in &views { + let transmittance_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("transmittance_lut"), + size: lut_settings.transmittance_lut_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let multiscattering_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("multiscattering_lut"), + size: lut_settings.multiscattering_lut_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let sky_view_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("sky_view_lut"), + size: lut_settings.sky_view_lut_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let aerial_view_lut = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("aerial_view_lut"), + size: lut_settings.aerial_view_lut_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D3, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + commands.entity(entity).insert({ + AtmosphereTextures { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + } + }); + } +} + +#[derive(Copy, Clone, Debug, thiserror::Error)] +#[error("ScatteringMedium missing with id {0:?}: make sure the asset was not removed.")] +struct ScatteringMediumMissingError(AssetId); + +/// The shader-uniform representation of an Atmosphere. +#[derive(Clone, Component, ShaderType)] +pub struct GpuAtmosphere { + //TODO: rename to Planet later? + pub ground_albedo: Vec3, + pub bottom_radius: f32, + pub top_radius: f32, +} + +pub fn prepare_atmosphere_uniforms( + mut commands: Commands, + atmospheres: Query<(Entity, &ExtractedAtmosphere)>, +) -> Result<(), BevyError> { + for (entity, atmosphere) in atmospheres { + commands.entity(entity).insert(GpuAtmosphere { + ground_albedo: atmosphere.ground_albedo, + bottom_radius: atmosphere.bottom_radius, + top_radius: atmosphere.top_radius, + }); + } + Ok(()) +} + +#[derive(Resource, Default)] +pub struct AtmosphereTransforms { + uniforms: DynamicUniformBuffer, +} + +impl AtmosphereTransforms { + #[inline] + pub fn uniforms(&self) -> &DynamicUniformBuffer { + &self.uniforms + } +} + +#[derive(ShaderType)] +pub struct AtmosphereTransform { + world_from_atmosphere: Mat4, +} + +#[derive(Component)] +pub struct AtmosphereTransformsOffset { + index: u32, +} + +impl AtmosphereTransformsOffset { + #[inline] + pub fn index(&self) -> u32 { + self.index + } +} + +pub(super) fn prepare_atmosphere_transforms( + views: Query<(Entity, &ExtractedView), (With, With)>, + render_device: Res, + render_queue: Res, + mut atmo_uniforms: ResMut, + mut commands: Commands, +) { + let atmo_count = views.iter().len(); + let Some(mut writer) = + atmo_uniforms + .uniforms + .get_writer(atmo_count, &render_device, &render_queue) + else { + return; + }; + + for (entity, view) in &views { + let world_from_view = view.world_from_view.affine(); + let camera_z = world_from_view.matrix3.z_axis; + let camera_y = world_from_view.matrix3.y_axis; + let atmo_z = camera_z + .with_y(0.0) + .try_normalize() + .unwrap_or_else(|| camera_y.with_y(0.0).normalize()); + let atmo_y = Vec3A::Y; + let atmo_x = atmo_y.cross(atmo_z).normalize(); + let world_from_atmosphere = + Affine3A::from_cols(atmo_x, atmo_y, atmo_z, world_from_view.translation); + + let world_from_atmosphere = Mat4::from(world_from_atmosphere); + + commands.entity(entity).insert(AtmosphereTransformsOffset { + index: writer.write(&AtmosphereTransform { + world_from_atmosphere, + }), + }); + } +} + +#[derive(Component)] +pub(crate) struct AtmosphereBindGroups { + pub transmittance_lut: BindGroup, + pub multiscattering_lut: BindGroup, + pub sky_view_lut: BindGroup, + pub aerial_view_lut: BindGroup, + pub render_sky: BindGroup, +} + +#[derive(Copy, Clone, Debug, thiserror::Error)] +enum AtmosphereBindGroupError { + #[error("Failed to prepare atmosphere bind groups. Atmosphere uniform buffer missing")] + Atmosphere, + #[error( + "Failed to prepare atmosphere bind groups. AtmosphereTransforms uniform buffer missing" + )] + Transforms, + #[error("Failed to prepare atmosphere bind groups. AtmosphereSettings uniform buffer missing")] + Settings, + #[error("Failed to prepare atmosphere bind groups. View uniform buffer missing")] + ViewUniforms, + #[error("Failed to prepare atmosphere bind groups. Light uniform buffer missing")] + LightUniforms, +} + +pub(super) fn prepare_atmosphere_bind_groups( + views: Query< + ( + Entity, + &ExtractedAtmosphere, + &AtmosphereTextures, + &ViewDepthTexture, + &Msaa, + ), + (With, With), + >, + render_device: Res, + layouts: Res, + render_sky_layouts: Res, + atmosphere_sampler: Res, + view_uniforms: Res, + lights_uniforms: Res, + atmosphere_transforms: Res, + atmosphere_uniforms: Res>, + settings_uniforms: Res>, + gpu_media: Res>, + medium_sampler: Res, + pipeline_cache: Res, + mut commands: Commands, +) -> Result<(), BevyError> { + if views.iter().len() == 0 { + return Ok(()); + } + + let atmosphere_binding = atmosphere_uniforms + .binding() + .ok_or(AtmosphereBindGroupError::Atmosphere)?; + + let transforms_binding = atmosphere_transforms + .uniforms() + .binding() + .ok_or(AtmosphereBindGroupError::Transforms)?; + + let settings_binding = settings_uniforms + .binding() + .ok_or(AtmosphereBindGroupError::Settings)?; + + let view_binding = view_uniforms + .uniforms + .binding() + .ok_or(AtmosphereBindGroupError::ViewUniforms)?; + + let lights_binding = lights_uniforms + .view_gpu_lights + .binding() + .ok_or(AtmosphereBindGroupError::LightUniforms)?; + + for (entity, atmosphere, textures, view_depth_texture, msaa) in &views { + let gpu_medium = gpu_media + .get(atmosphere.medium) + .ok_or(ScatteringMediumMissingError(atmosphere.medium))?; + + let transmittance_lut = render_device.create_bind_group( + "transmittance_lut_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.transmittance_lut), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + // scattering medium luts and sampler + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + // transmittance lut storage texture + (13, &textures.transmittance_lut.default_view), + )), + ); + + let multiscattering_lut = render_device.create_bind_group( + "multiscattering_lut_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.multiscattering_lut), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + // scattering medium luts and sampler + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + // atmosphere luts and sampler + (8, &textures.transmittance_lut.default_view), + (12, &**atmosphere_sampler), + // multiscattering lut storage texture + (13, &textures.multiscattering_lut.default_view), + )), + ); + + let sky_view_lut = render_device.create_bind_group( + "sky_view_lut_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.sky_view_lut), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (2, transforms_binding.clone()), + (3, view_binding.clone()), + (4, lights_binding.clone()), + // scattering medium luts and sampler + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + // atmosphere luts and sampler + (8, &textures.transmittance_lut.default_view), + (9, &textures.multiscattering_lut.default_view), + (12, &**atmosphere_sampler), + // sky view lut storage texture + (13, &textures.sky_view_lut.default_view), + )), + ); + + let aerial_view_lut = render_device.create_bind_group( + "sky_view_lut_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.aerial_view_lut), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (3, view_binding.clone()), + (4, lights_binding.clone()), + // scattering medium luts and sampler + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + // atmosphere luts and sampler + (8, &textures.transmittance_lut.default_view), + (9, &textures.multiscattering_lut.default_view), + (12, &**atmosphere_sampler), + // aerial view lut storage texture + (13, &textures.aerial_view_lut.default_view), + )), + ); + + let render_sky = render_device.create_bind_group( + "render_sky_bind_group", + &pipeline_cache.get_bind_group_layout(if *msaa == Msaa::Off { + &render_sky_layouts.render_sky + } else { + &render_sky_layouts.render_sky_msaa + }), + &BindGroupEntries::with_indices(( + // uniforms + (0, atmosphere_binding.clone()), + (1, settings_binding.clone()), + (2, transforms_binding.clone()), + (3, view_binding.clone()), + (4, lights_binding.clone()), + // scattering medium luts and sampler + (5, &gpu_medium.density_lut_view), + (6, &gpu_medium.scattering_lut_view), + (7, medium_sampler.sampler()), + // atmosphere luts and sampler + (8, &textures.transmittance_lut.default_view), + (9, &textures.multiscattering_lut.default_view), + (10, &textures.sky_view_lut.default_view), + (11, &textures.aerial_view_lut.default_view), + (12, &**atmosphere_sampler), + // view depth texture + (13, view_depth_texture.view()), + )), + ); + + commands.entity(entity).insert(AtmosphereBindGroups { + transmittance_lut, + multiscattering_lut, + sky_view_lut, + aerial_view_lut, + render_sky, + }); + } + + Ok(()) +} + +#[derive(ShaderType)] +#[repr(C)] +pub(crate) struct AtmosphereData { + pub atmosphere: GpuAtmosphere, + pub settings: GpuAtmosphereSettings, +} + +pub fn init_atmosphere_buffer(mut commands: Commands) { + commands.insert_resource(AtmosphereBuffer { + buffer: StorageBuffer::from(AtmosphereData { + atmosphere: GpuAtmosphere { + ground_albedo: Vec3::ZERO, + bottom_radius: 0.0, + top_radius: 0.0, + }, + settings: GpuAtmosphereSettings::default(), + }), + }); +} + +#[derive(Resource)] +pub struct AtmosphereBuffer { + pub(crate) buffer: StorageBuffer, +} + +pub(crate) fn write_atmosphere_buffer( + device: Res, + queue: Res, + atmosphere_entity: Query<(&GpuAtmosphere, &GpuAtmosphereSettings), With>, + mut atmosphere_buffer: ResMut, +) { + let Ok((atmosphere, settings)) = atmosphere_entity.single() else { + return; + }; + + atmosphere_buffer.buffer.set(AtmosphereData { + atmosphere: atmosphere.clone(), + settings: settings.clone(), + }); + atmosphere_buffer.buffer.write_buffer(&device, &queue); +} diff --git a/vendor/bevy_pbr/src/atmosphere/sky_view_lut.wgsl b/vendor/bevy_pbr/src/atmosphere/sky_view_lut.wgsl new file mode 100644 index 00000000..5e23e78e --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/sky_view_lut.wgsl @@ -0,0 +1,40 @@ +#import bevy_pbr::{ + atmosphere::{ + bindings::settings, + functions::{ + get_view_position, raymarch_atmosphere, + max_atmosphere_distance, direction_atmosphere_to_world, + sky_view_lut_uv_to_zenith_azimuth, zenith_azimuth_to_ray_dir, + }, + } +} + +#import bevy_render::{ + view::View, + maths::HALF_PI, +} +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(13) var sky_view_lut_out: texture_storage_2d; + +@compute +@workgroup_size(16, 16, 1) +fn main(@builtin(global_invocation_id) idx: vec3) { + let uv = vec2(idx.xy) / vec2(settings.sky_view_lut_size); + + let cam_pos = get_view_position(); + let r = length(cam_pos); + var zenith_azimuth = sky_view_lut_uv_to_zenith_azimuth(r, uv); + + let ray_dir_as = zenith_azimuth_to_ray_dir(zenith_azimuth.x, zenith_azimuth.y); + let ray_dir_ws = direction_atmosphere_to_world(ray_dir_as); + + let world_pos = vec3(0.0, r, 0.0); + let up = normalize(world_pos); + let mu = dot(ray_dir_ws, up); + let t_max = max_atmosphere_distance(r, mu); + + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t_max, settings.sky_view_lut_samples, uv, true); + + textureStore(sky_view_lut_out, idx.xy, vec4(result.inscattering, 1.0)); +} diff --git a/vendor/bevy_pbr/src/atmosphere/transmittance_lut.wgsl b/vendor/bevy_pbr/src/atmosphere/transmittance_lut.wgsl new file mode 100644 index 00000000..e7187bc6 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/transmittance_lut.wgsl @@ -0,0 +1,51 @@ +#import bevy_pbr::atmosphere::{ + bindings::settings, + functions::{ + sample_density_lut, get_local_r, max_atmosphere_distance, + MIDPOINT_RATIO, ABSORPTION_DENSITY, SCATTERING_DENSITY + }, + bruneton_functions::transmittance_lut_uv_to_r_mu, +} + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(13) var transmittance_lut_out: texture_storage_2d; + +@compute +@workgroup_size(16, 16, 1) +fn main(@builtin(global_invocation_id) idx: vec3) { + let uv: vec2 = (vec2(idx.xy) + 0.5) / vec2(settings.transmittance_lut_size); + // map UV coordinates to view height (r) and zenith cos angle (mu) + let r_mu = transmittance_lut_uv_to_r_mu(uv); + + // compute the optical depth from view height r to the top atmosphere boundary + let optical_depth = ray_optical_depth(r_mu.x, r_mu.y, settings.transmittance_lut_samples); + let transmittance = exp(-optical_depth); + + textureStore(transmittance_lut_out, idx.xy, vec4(transmittance, 1.0)); +} + +/// Compute the optical depth of the atmosphere from the ground to the top atmosphere boundary +/// at a given view height (r) and zenith cos angle (mu) +fn ray_optical_depth(r: f32, mu: f32, sample_count: u32) -> vec3 { + let t_max = max_atmosphere_distance(r, mu); + var optical_depth = vec3(0.0f); + var prev_t = 0.0f; + + for (var i = 0u; i < sample_count; i++) { + let t_i = t_max * (f32(i) + MIDPOINT_RATIO) / f32(sample_count); + let dt = t_i - prev_t; + prev_t = t_i; + + let r_i = get_local_r(r, mu, t_i); + + // PERF: A possible later optimization would be to sample at `component = 0.5` + // (getting the average of the two rows) and then multiplying by 2 to find the sum. + let absorption = sample_density_lut(r_i, ABSORPTION_DENSITY); + let scattering = sample_density_lut(r_i, SCATTERING_DENSITY); + let extinction = absorption + scattering; + optical_depth += extinction * dt; + } + + return optical_depth; +} diff --git a/vendor/bevy_pbr/src/atmosphere/types.wgsl b/vendor/bevy_pbr/src/atmosphere/types.wgsl new file mode 100644 index 00000000..d74a2ce4 --- /dev/null +++ b/vendor/bevy_pbr/src/atmosphere/types.wgsl @@ -0,0 +1,36 @@ +#define_import_path bevy_pbr::atmosphere::types + +struct Atmosphere { + ground_albedo: vec3, + // Radius of the planet + bottom_radius: f32, // units: m + // Radius at which we consider the atmosphere to 'end' for out calculations (from center of planet) + top_radius: f32, // units: m +} + +struct AtmosphereSettings { + transmittance_lut_size: vec2, + multiscattering_lut_size: vec2, + sky_view_lut_size: vec2, + aerial_view_lut_size: vec3, + transmittance_lut_samples: u32, + multiscattering_lut_dirs: u32, + multiscattering_lut_samples: u32, + sky_view_lut_samples: u32, + aerial_view_lut_samples: u32, + aerial_view_lut_max_distance: f32, + scene_units_to_m: f32, + sky_max_samples: u32, + rendering_method: u32, +} + +// "Atmosphere space" is just the view position with y=0 and oriented horizontally, +// so the horizon stays a horizontal line in our luts +struct AtmosphereTransforms { + world_from_atmosphere: mat4x4, +} + +struct AtmosphereData { + atmosphere: Atmosphere, + settings: AtmosphereSettings, +} \ No newline at end of file diff --git a/vendor/bevy_pbr/src/bluenoise/stbn.ktx2 b/vendor/bevy_pbr/src/bluenoise/stbn.ktx2 new file mode 100644 index 00000000..bd5ca539 Binary files /dev/null and b/vendor/bevy_pbr/src/bluenoise/stbn.ktx2 differ diff --git a/vendor/bevy_pbr/src/cluster.rs b/vendor/bevy_pbr/src/cluster.rs new file mode 100644 index 00000000..ca7db235 --- /dev/null +++ b/vendor/bevy_pbr/src/cluster.rs @@ -0,0 +1,580 @@ +use core::num::NonZero; + +use bevy_camera::Camera; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_light::cluster::{ClusterableObjectCounts, Clusters, GlobalClusterSettings}; +use bevy_math::{uvec4, UVec3, UVec4, Vec4}; +use bevy_render::{ + render_resource::{ + BindingResource, BufferBindingType, ShaderSize, ShaderType, StorageBuffer, UniformBuffer, + }, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + Extract, +}; +use tracing::warn; + +use crate::MeshPipeline; + +// NOTE: this must be kept in sync with the same constants in +// `mesh_view_types.wgsl`. +pub const MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS: usize = 204; +// Make sure that the clusterable object buffer doesn't overflow the maximum +// size of a UBO on WebGL 2. +const _: () = + assert!(size_of::() * MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS <= 16384); + +// NOTE: Clustered-forward rendering requires 3 storage buffer bindings so check that +// at least that many are supported using this constant and SupportedBindingType::from_device() +pub const CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT: u32 = 3; + +// this must match CLUSTER_COUNT_SIZE in pbr.wgsl +// and must be large enough to contain MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS +const CLUSTER_COUNT_SIZE: u32 = 9; + +const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1; +const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1; + +pub(crate) fn make_global_cluster_settings(world: &World) -> GlobalClusterSettings { + let device = world.resource::(); + let adapter = world.resource::(); + let clustered_decals_are_usable = + crate::decal::clustered::clustered_decals_are_usable(device, adapter); + let supports_storage_buffers = matches!( + device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + BufferBindingType::Storage { .. } + ); + GlobalClusterSettings { + supports_storage_buffers, + clustered_decals_are_usable, + max_uniform_buffer_clusterable_objects: MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + view_cluster_bindings_max_indices: ViewClusterBindings::MAX_INDICES, + } +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuClusterableObject { + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + // For spot lights: 2 components of the direction (x,z), spot_scale and spot_offset + pub(crate) light_custom_data: Vec4, + pub(crate) color_inverse_square_range: Vec4, + pub(crate) position_radius: Vec4, + pub(crate) flags: u32, + pub(crate) shadow_depth_bias: f32, + pub(crate) shadow_normal_bias: f32, + pub(crate) spot_light_tan_angle: f32, + pub(crate) soft_shadow_size: f32, + pub(crate) shadow_map_near_z: f32, + pub(crate) decal_index: u32, + pub(crate) pad: f32, +} + +#[derive(Resource)] +pub struct GlobalClusterableObjectMeta { + pub gpu_clusterable_objects: GpuClusterableObjects, + pub entity_to_index: EntityHashMap, +} + +pub enum GpuClusterableObjects { + Uniform(UniformBuffer), + Storage(StorageBuffer), +} + +#[derive(ShaderType)] +pub struct GpuClusterableObjectsUniform { + data: Box<[GpuClusterableObject; MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS]>, +} + +#[derive(ShaderType, Default)] +pub struct GpuClusterableObjectsStorage { + #[shader(size(runtime))] + data: Vec, +} + +#[derive(Component)] +pub struct ExtractedClusterConfig { + /// Special near value for cluster calculations + pub(crate) near: f32, + pub(crate) far: f32, + /// Number of clusters in `X` / `Y` / `Z` in the view frustum + pub(crate) dimensions: UVec3, +} + +enum ExtractedClusterableObjectElement { + ClusterHeader(ClusterableObjectCounts), + ClusterableObjectEntity(Entity), +} + +#[derive(Component)] +pub struct ExtractedClusterableObjects { + data: Vec, +} + +#[derive(ShaderType)] +struct GpuClusterOffsetsAndCountsUniform { + data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>, +} + +#[derive(ShaderType, Default)] +struct GpuClusterableObjectIndexListsStorage { + #[shader(size(runtime))] + data: Vec, +} + +#[derive(ShaderType, Default)] +struct GpuClusterOffsetsAndCountsStorage { + /// The starting offset, followed by the number of point lights, spot + /// lights, reflection probes, and irradiance volumes in each cluster, in + /// that order. The remaining fields are filled with zeroes. + #[shader(size(runtime))] + data: Vec<[UVec4; 2]>, +} + +enum ViewClusterBuffers { + Uniform { + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + clusterable_object_index_lists: UniformBuffer, + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + cluster_offsets_and_counts: UniformBuffer, + }, + Storage { + clusterable_object_index_lists: StorageBuffer, + cluster_offsets_and_counts: StorageBuffer, + }, +} + +#[derive(Component)] +pub struct ViewClusterBindings { + n_indices: usize, + n_offsets: usize, + buffers: ViewClusterBuffers, +} + +pub fn init_global_clusterable_object_meta( + mut commands: Commands, + render_device: Res, +) { + commands.insert_resource(GlobalClusterableObjectMeta::new( + render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + )); +} + +impl GlobalClusterableObjectMeta { + pub fn new(buffer_binding_type: BufferBindingType) -> Self { + Self { + gpu_clusterable_objects: GpuClusterableObjects::new(buffer_binding_type), + entity_to_index: EntityHashMap::default(), + } + } +} + +impl GpuClusterableObjects { + fn new(buffer_binding_type: BufferBindingType) -> Self { + match buffer_binding_type { + BufferBindingType::Storage { .. } => Self::storage(), + BufferBindingType::Uniform => Self::uniform(), + } + } + + fn uniform() -> Self { + Self::Uniform(UniformBuffer::default()) + } + + fn storage() -> Self { + Self::Storage(StorageBuffer::default()) + } + + pub(crate) fn set(&mut self, mut clusterable_objects: Vec) { + match self { + GpuClusterableObjects::Uniform(buffer) => { + let len = clusterable_objects + .len() + .min(MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS); + let src = &clusterable_objects[..len]; + let dst = &mut buffer.get_mut().data[..len]; + dst.copy_from_slice(src); + } + GpuClusterableObjects::Storage(buffer) => { + buffer.get_mut().data.clear(); + buffer.get_mut().data.append(&mut clusterable_objects); + } + } + } + + pub(crate) fn write_buffer( + &mut self, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + match self { + GpuClusterableObjects::Uniform(buffer) => { + buffer.write_buffer(render_device, render_queue); + } + GpuClusterableObjects::Storage(buffer) => { + buffer.write_buffer(render_device, render_queue); + } + } + } + + pub fn binding(&self) -> Option> { + match self { + GpuClusterableObjects::Uniform(buffer) => buffer.binding(), + GpuClusterableObjects::Storage(buffer) => buffer.binding(), + } + } + + pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterableObjectsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterableObjectsUniform::min_size(), + } + } +} + +impl Default for GpuClusterableObjectsUniform { + fn default() -> Self { + Self { + data: Box::new( + [GpuClusterableObject::default(); MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS], + ), + } + } +} + +/// Extracts clusters from the main world from the render world. +pub fn extract_clusters( + mut commands: Commands, + views: Extract>, + mapper: Extract>, +) { + for (entity, clusters, camera) in &views { + let mut entity_commands = commands + .get_entity(entity) + .expect("Clusters entity wasn't synced."); + if !camera.is_active { + entity_commands.remove::<(ExtractedClusterableObjects, ExtractedClusterConfig)>(); + continue; + } + + let entity_count: usize = clusters + .clusterable_objects + .iter() + .map(|l| l.entities.len()) + .sum(); + let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + entity_count); + for cluster_objects in &clusters.clusterable_objects { + data.push(ExtractedClusterableObjectElement::ClusterHeader( + cluster_objects.counts, + )); + for clusterable_entity in &cluster_objects.entities { + if let Ok(entity) = mapper.get(*clusterable_entity) { + data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( + entity, + )); + } + } + } + + entity_commands.insert(( + ExtractedClusterableObjects { data }, + ExtractedClusterConfig { + near: clusters.near, + far: clusters.far, + dimensions: clusters.dimensions, + }, + )); + } +} + +pub fn prepare_clusters( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mesh_pipeline: Res, + global_clusterable_object_meta: Res, + views: Query<(Entity, &ExtractedClusterableObjects)>, +) { + let render_device = render_device.into_inner(); + let supports_storage_buffers = matches!( + mesh_pipeline.clustered_forward_buffer_binding_type, + BufferBindingType::Storage { .. } + ); + for (entity, extracted_clusters) in &views { + let mut view_clusters_bindings = + ViewClusterBindings::new(mesh_pipeline.clustered_forward_buffer_binding_type); + view_clusters_bindings.clear(); + + for record in &extracted_clusters.data { + match record { + ExtractedClusterableObjectElement::ClusterHeader(counts) => { + let offset = view_clusters_bindings.n_indices(); + view_clusters_bindings.push_offset_and_counts(offset, counts); + } + ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => { + if let Some(clusterable_object_index) = + global_clusterable_object_meta.entity_to_index.get(entity) + { + if view_clusters_bindings.n_indices() >= ViewClusterBindings::MAX_INDICES + && !supports_storage_buffers + { + warn!( + "Clusterable object index lists are full! The clusterable \ + objects in the view are present in too many clusters." + ); + break; + } + view_clusters_bindings.push_index(*clusterable_object_index); + } + } + } + } + + view_clusters_bindings.write_buffers(render_device, &render_queue); + + commands.entity(entity).insert(view_clusters_bindings); + } +} + +impl ViewClusterBindings { + pub const MAX_OFFSETS: usize = 16384 / 4; + const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4; + pub const MAX_INDICES: usize = 16384; + + pub fn new(buffer_binding_type: BufferBindingType) -> Self { + Self { + n_indices: 0, + n_offsets: 0, + buffers: ViewClusterBuffers::new(buffer_binding_type), + } + } + + pub fn clear(&mut self) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + *clusterable_object_index_lists.get_mut().data = + [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]; + *cluster_offsets_and_counts.get_mut().data = [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]; + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + cluster_offsets_and_counts, + .. + } => { + clusterable_object_index_lists.get_mut().data.clear(); + cluster_offsets_and_counts.get_mut().data.clear(); + } + } + } + + fn push_offset_and_counts(&mut self, offset: usize, counts: &ClusterableObjectCounts) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + cluster_offsets_and_counts, + .. + } => { + let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4 + if array_index >= Self::MAX_UNIFORM_ITEMS { + warn!("cluster offset and count out of bounds!"); + return; + } + let component = self.n_offsets & ((1 << 2) - 1); + let packed = + pack_offset_and_counts(offset, counts.point_lights, counts.spot_lights); + + cluster_offsets_and_counts.get_mut().data[array_index][component] = packed; + } + ViewClusterBuffers::Storage { + cluster_offsets_and_counts, + .. + } => { + cluster_offsets_and_counts.get_mut().data.push([ + uvec4( + offset as u32, + counts.point_lights, + counts.spot_lights, + counts.reflection_probes, + ), + uvec4(counts.irradiance_volumes, counts.decals, 0, 0), + ]); + } + } + + self.n_offsets += 1; + } + + pub fn n_indices(&self) -> usize { + self.n_indices + } + + pub fn push_index(&mut self, index: usize) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + .. + } => { + let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16 + let component = (self.n_indices >> 2) & ((1 << 2) - 1); + let sub_index = self.n_indices & ((1 << 2) - 1); + let index = index as u32; + + clusterable_object_index_lists.get_mut().data[array_index][component] |= + index << (8 * sub_index); + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + .. + } => { + clusterable_object_index_lists + .get_mut() + .data + .push(index as u32); + } + } + + self.n_indices += 1; + } + + pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + clusterable_object_index_lists.write_buffer(render_device, render_queue); + cluster_offsets_and_counts.write_buffer(render_device, render_queue); + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + clusterable_object_index_lists.write_buffer(render_device, render_queue); + cluster_offsets_and_counts.write_buffer(render_device, render_queue); + } + } + } + + pub fn clusterable_object_index_lists_binding(&self) -> Option> { + match &self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + .. + } => clusterable_object_index_lists.binding(), + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + .. + } => clusterable_object_index_lists.binding(), + } + } + + pub fn offsets_and_counts_binding(&self) -> Option> { + match &self.buffers { + ViewClusterBuffers::Uniform { + cluster_offsets_and_counts, + .. + } => cluster_offsets_and_counts.binding(), + ViewClusterBuffers::Storage { + cluster_offsets_and_counts, + .. + } => cluster_offsets_and_counts.binding(), + } + } + + pub fn min_size_clusterable_object_index_lists( + buffer_binding_type: BufferBindingType, + ) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterableObjectIndexListsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterableObjectIndexListsUniform::min_size(), + } + } + + pub fn min_size_cluster_offsets_and_counts( + buffer_binding_type: BufferBindingType, + ) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(), + } + } +} + +impl ViewClusterBuffers { + fn new(buffer_binding_type: BufferBindingType) -> Self { + match buffer_binding_type { + BufferBindingType::Storage { .. } => Self::storage(), + BufferBindingType::Uniform => Self::uniform(), + } + } + + fn uniform() -> Self { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists: UniformBuffer::default(), + cluster_offsets_and_counts: UniformBuffer::default(), + } + } + + fn storage() -> Self { + ViewClusterBuffers::Storage { + clusterable_object_index_lists: StorageBuffer::default(), + cluster_offsets_and_counts: StorageBuffer::default(), + } + } +} + +// Compresses the offset and counts of point and spot lights so that they fit in +// a UBO. +// +// This function is only used if storage buffers are unavailable on this +// platform: typically, on WebGL 2. +// +// NOTE: With uniform buffer max binding size as 16384 bytes +// that means we can fit 204 clusterable objects in one uniform +// buffer, which means the count can be at most 204 so it +// needs 9 bits. +// The array of indices can also use u8 and that means the +// offset in to the array of indices needs to be able to address +// 16384 values. log2(16384) = 14 bits. +// We use 32 bits to store the offset and counts so +// we pack the offset into the upper 14 bits of a u32, +// the point light count into bits 9-17, and the spot light count into bits 0-8. +// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] +// [ offset | point light count | spot light count ] +// +// NOTE: This assumes CPU and GPU endianness are the same which is true +// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs +// +// NOTE: On platforms that use this function, we don't cluster light probes, so +// the number of light probes is irrelevant. +fn pack_offset_and_counts(offset: usize, point_count: u32, spot_count: u32) -> u32 { + ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2)) + | ((point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE) + | (spot_count & CLUSTER_COUNT_MASK) +} + +#[derive(ShaderType)] +struct GpuClusterableObjectIndexListsUniform { + data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>, +} + +// NOTE: Assert at compile time that GpuClusterableObjectIndexListsUniform +// fits within the maximum uniform buffer binding size +const _: () = assert!(GpuClusterableObjectIndexListsUniform::SHADER_SIZE.get() <= 16384); + +impl Default for GpuClusterableObjectIndexListsUniform { + fn default() -> Self { + Self { + data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]), + } + } +} + +impl Default for GpuClusterOffsetsAndCountsUniform { + fn default() -> Self { + Self { + data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]), + } + } +} diff --git a/vendor/bevy_pbr/src/components.rs b/vendor/bevy_pbr/src/components.rs new file mode 100644 index 00000000..bdb5a36d --- /dev/null +++ b/vendor/bevy_pbr/src/components.rs @@ -0,0 +1,46 @@ +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; +use bevy_ecs::entity::{Entity, EntityHashMap}; +use bevy_ecs::reflect::ReflectComponent; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::sync_world::MainEntity; + +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Default, Clone)] +pub struct RenderVisibleMeshEntities { + #[reflect(ignore, clone)] + pub entities: Vec<(Entity, MainEntity)>, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct RenderCubemapVisibleEntities { + #[reflect(ignore, clone)] + pub(crate) data: [RenderVisibleMeshEntities; 6], +} + +impl RenderCubemapVisibleEntities { + pub fn get(&self, i: usize) -> &RenderVisibleMeshEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut RenderVisibleMeshEntities { + &mut self.data[i] + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct RenderCascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore, clone)] + pub entities: EntityHashMap>, +} diff --git a/vendor/bevy_pbr/src/decal/clustered.rs b/vendor/bevy_pbr/src/decal/clustered.rs new file mode 100644 index 00000000..1acbd2fb --- /dev/null +++ b/vendor/bevy_pbr/src/decal/clustered.rs @@ -0,0 +1,555 @@ +//! Clustered decals, bounding regions that project textures onto surfaces. +//! +//! A *clustered decal* is a bounding box that projects a texture onto any +//! surface within its bounds along the positive Z axis. In Bevy, clustered +//! decals use the *clustered forward* rendering technique. +//! +//! Clustered decals are the highest-quality types of decals that Bevy supports, +//! but they require bindless textures. This means that they presently can't be +//! used on WebGL 2 or WebGPU. Bevy's clustered decals can be used +//! with forward or deferred rendering and don't require a prepass. +//! +//! Each clustered decal may contain up to 4 textures. By default, the 4 +//! textures correspond to the base color, a normal map, a metallic-roughness +//! map, and an emissive map respectively. However, with a custom shader, you +//! can use these 4 textures for whatever you wish. Additionally, you can use +//! the built-in *tag* field to store additional application-specific data; by +//! reading the tag in the shader, you can modify the appearance of a clustered +//! decal arbitrarily. See the documentation in `clustered.wgsl` for more +//! information and the `clustered_decals` example for an example of use. + +use core::{num::NonZero, ops::Deref}; + +use bevy_app::{App, Plugin}; +use bevy_asset::{AssetId, Handle}; +use bevy_camera::visibility::ViewVisibility; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + entity::{Entity, EntityHashMap}, + query::With, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{Commands, Local, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture}; +use bevy_math::Mat4; +use bevy_platform::collections::HashMap; +use bevy_render::{ + render_asset::RenderAssets, + render_resource::{ + binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler, + SamplerBindingType, ShaderType, TextureSampleType, TextureView, + }, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + settings::WgpuFeatures, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{FallbackImage, GpuImage}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_transform::components::GlobalTransform; +use bytemuck::{Pod, Zeroable}; + +use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; + +/// The number of textures that can be associated with each clustered decal. +const IMAGES_PER_DECAL: usize = 4; + +/// A plugin that adds support for clustered decals. +/// +/// In environments where bindless textures aren't available, clustered decals +/// can still be added to a scene, but they won't project any decals. +pub struct ClusteredDecalPlugin; + +/// Stores information about all the clustered decals in the scene. +#[derive(Resource, Default)] +pub struct RenderClusteredDecals { + /// Maps an index in the shader binding array to the associated decal image. + /// + /// [`Self::texture_to_binding_index`] holds the inverse mapping. + binding_index_to_textures: Vec>, + /// Maps a decal image to the shader binding array. + /// + /// [`Self::binding_index_to_textures`] holds the inverse mapping. + texture_to_binding_index: HashMap, i32>, + /// The information concerning each decal that we provide to the shader. + decals: Vec, + /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the + /// index of that decal in the [`Self::decals`] list. + entity_to_decal_index: EntityHashMap, +} + +impl RenderClusteredDecals { + /// Clears out this [`RenderClusteredDecals`] in preparation for a new + /// frame. + fn clear(&mut self) { + self.binding_index_to_textures.clear(); + self.texture_to_binding_index.clear(); + self.decals.clear(); + self.entity_to_decal_index.clear(); + } + + pub fn insert_decal( + &mut self, + entity: Entity, + images: [Option>; IMAGES_PER_DECAL], + local_from_world: Mat4, + tag: u32, + ) { + let image_indices = images.map(|maybe_image_id| match maybe_image_id { + Some(ref image_id) => self.get_or_insert_image(image_id), + None => -1, + }); + let decal_index = self.decals.len(); + self.decals.push(RenderClusteredDecal { + local_from_world, + image_indices, + tag, + pad_a: 0, + pad_b: 0, + pad_c: 0, + }); + self.entity_to_decal_index.insert(entity, decal_index); + } + + pub fn get(&self, entity: Entity) -> Option { + self.entity_to_decal_index.get(&entity).copied() + } +} + +/// The per-view bind group entries pertaining to decals. +pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> { + /// The list of decals, corresponding to `mesh_view_bindings::decals` in the + /// shader. + pub(crate) decals: &'a Buffer, + /// The list of textures, corresponding to + /// `mesh_view_bindings::decal_textures` in the shader. + pub(crate) texture_views: Vec<&'a ::Target>, + /// The sampler that the shader uses to sample decals, corresponding to + /// `mesh_view_bindings::decal_sampler` in the shader. + pub(crate) sampler: &'a Sampler, +} + +/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready +/// to upload to the GPU. +#[derive(Resource, Deref, DerefMut)] +pub struct DecalsBuffer(RawBufferVec); + +impl Default for DecalsBuffer { + fn default() -> Self { + DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE)) + } +} + +impl Plugin for ClusteredDecalPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "clustered.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, (extract_decals, extract_clustered_decal)) + .add_systems( + Render, + prepare_decals + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + ) + .add_systems( + Render, + upload_decals.in_set(RenderSystems::PrepareResources), + ); + } +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type ClusteredDecal +fn extract_clustered_decal( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +/// The GPU data structure that stores information about each decal. +#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)] +#[repr(C)] +pub struct RenderClusteredDecal { + /// The inverse of the model matrix. + /// + /// The shader uses this in order to back-transform world positions into + /// model space. + local_from_world: Mat4, + /// The index of each decal texture in the binding array. + /// + /// These are in the order of the base color texture, the normal map + /// texture, the metallic-roughness map texture, and finally the emissive + /// texture. + /// + /// If the decal doesn't have a texture assigned to a slot, the index at + /// that slot will be -1. + image_indices: [i32; 4], + /// A custom tag available for application-defined purposes. + tag: u32, + /// Padding. + pad_a: u32, + /// Padding. + pad_b: u32, + /// Padding. + pad_c: u32, +} + +/// Extracts decals from the main world into the render world. +pub fn extract_decals( + decals: Extract< + Query<( + RenderEntity, + &ClusteredDecal, + &GlobalTransform, + &ViewVisibility, + )>, + >, + spot_light_textures: Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + point_light_textures: Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + directional_light_textures: Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + mut render_decals: ResMut, +) { + // Clear out the `RenderDecals` in preparation for a new frame. + render_decals.clear(); + + extract_clustered_decals(&decals, &mut render_decals); + extract_spot_light_textures(&spot_light_textures, &mut render_decals); + extract_point_light_textures(&point_light_textures, &mut render_decals); + extract_directional_light_textures(&directional_light_textures, &mut render_decals); +} + +/// Extracts all clustered decals and light textures from the scene and transfers +/// them to the render world. +fn extract_clustered_decals( + decals: &Extract< + Query<( + RenderEntity, + &ClusteredDecal, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + // Loop over each decal. + for (decal_entity, clustered_decal, global_transform, view_visibility) in decals { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + // Insert the decal, grabbing the ID of every associated texture as we + // do. + render_decals.insert_decal( + decal_entity, + [ + clustered_decal.base_color_texture.as_ref().map(Handle::id), + clustered_decal.normal_map_texture.as_ref().map(Handle::id), + clustered_decal + .metallic_roughness_texture + .as_ref() + .map(Handle::id), + clustered_decal.emissive_texture.as_ref().map(Handle::id), + ], + global_transform.affine().inverse().into(), + clustered_decal.tag, + ); + } +} + +/// Extracts all textures from spot lights from the main world to the render +/// world as clustered decals. +fn extract_spot_light_textures( + spot_light_textures: &Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in spot_light_textures { + // If the texture is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + [Some(texture.image.id()), None, None, None], + global_transform.affine().inverse().into(), + 0, + ); + } +} + +/// Extracts all textures from point lights from the main world to the render +/// world as clustered decals. +fn extract_point_light_textures( + point_light_textures: &Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in point_light_textures { + // If the texture is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + [Some(texture.image.id()), None, None, None], + global_transform.affine().inverse().into(), + texture.cubemap_layout as u32, + ); + } +} + +/// Extracts all textures from directional lights from the main world to the +/// render world as clustered decals. +fn extract_directional_light_textures( + directional_light_textures: &Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in directional_light_textures { + // If the texture is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + [Some(texture.image.id()), None, None, None], + global_transform.affine().inverse().into(), + if texture.tiled { 1 } else { 0 }, + ); + } +} + +/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table. +fn prepare_decals( + decals: Query>, + mut global_clusterable_object_meta: ResMut, + render_decals: Res, +) { + for decal_entity in &decals { + if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) { + global_clusterable_object_meta + .entity_to_index + .insert(decal_entity, *index); + } + } +} + +/// Returns the layout for the clustered-decal-related bind group entries for a +/// single view. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> Option<[BindGroupLayoutEntryBuilder; 3]> { + // If binding arrays aren't supported on the current platform, we have no + // bind group layout entries. + if !clustered_decals_are_usable(render_device, render_adapter) { + return None; + } + + Some([ + // `decals` + binding_types::storage_buffer_read_only::(false), + // `decal_textures` + binding_types::texture_2d(TextureSampleType::Float { filterable: true }) + .count(NonZero::::new(max_view_decals(render_device)).unwrap()), + // `decal_sampler` + binding_types::sampler(SamplerBindingType::Filtering), + ]) +} + +impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> { + /// Creates and returns the bind group entries for clustered decals for a + /// single view. + pub(crate) fn get( + render_decals: &RenderClusteredDecals, + decals_buffer: &'a DecalsBuffer, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> Option> { + // Skip the entries if decals are unsupported on the current platform. + if !clustered_decals_are_usable(render_device, render_adapter) { + return None; + } + + // We use the first sampler among all the images. This assumes that all + // images use the same sampler, which is a documented restriction. If + // there's no sampler, we just use the one from the fallback image. + let sampler = match render_decals + .binding_index_to_textures + .iter() + .filter_map(|image_id| images.get(*image_id)) + .next() + { + Some(gpu_image) => &gpu_image.sampler, + None => &fallback_image.d2.sampler, + }; + + // Gather up the decal textures. + let mut texture_views = vec![]; + for image_id in &render_decals.binding_index_to_textures { + match images.get(*image_id) { + None => texture_views.push(&*fallback_image.d2.texture_view), + Some(gpu_image) => texture_views.push(&*gpu_image.texture_view), + } + } + + // If required on this platform, pad out the binding array to its + // maximum length. + if !render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + let max_view_decals = max_view_decals(render_device); + while texture_views.len() < max_view_decals as usize { + texture_views.push(&*fallback_image.d2.texture_view); + } + } else if texture_views.is_empty() { + texture_views.push(&*fallback_image.d2.texture_view); + } + + Some(RenderViewClusteredDecalBindGroupEntries { + decals: decals_buffer.buffer()?, + texture_views, + sampler, + }) + } +} + +impl RenderClusteredDecals { + /// Returns the index of the given image in the decal texture binding array, + /// adding it to the list if necessary. + fn get_or_insert_image(&mut self, image_id: &AssetId) -> i32 { + *self + .texture_to_binding_index + .entry(*image_id) + .or_insert_with(|| { + let index = self.binding_index_to_textures.len() as i32; + self.binding_index_to_textures.push(*image_id); + index + }) + } +} + +/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the +/// GPU. +fn upload_decals( + render_decals: Res, + mut decals_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + decals_buffer.clear(); + + for &decal in &render_decals.decals { + decals_buffer.push(decal); + } + + // Make sure the buffer is non-empty. + // Otherwise there won't be a buffer to bind. + if decals_buffer.is_empty() { + decals_buffer.push(RenderClusteredDecal::default()); + } + + decals_buffer.write_buffer(&render_device, &render_queue); +} + +/// Returns true if clustered decals are usable on the current platform or false +/// otherwise. +/// +/// Clustered decals are currently disabled on macOS and iOS due to insufficient +/// texture bindings and limited bindless support in `wgpu`. +pub fn clustered_decals_are_usable( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> bool { + // Disable binding arrays on Metal. There aren't enough texture bindings available. + // See issue #17553. + // Re-enable this when `wgpu` has first-class bindless. + binding_arrays_are_usable(render_device, render_adapter) + && cfg!(feature = "pbr_clustered_decals") +} + +/// Returns the maximum number of decals that can be in the scene, taking +/// platform limitations into account. +fn max_view_decals(render_device: &RenderDevice) -> u32 { + // If the current `wgpu` platform doesn't support partially-bound binding + // arrays, limit the number of decals to a low number. If we didn't do this, + // then on such platforms we'd pay the maximum overhead even if there are no + // decals are in the scene. + if render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + // This number was determined arbitrarily as a reasonable value that + // would encompass most use cases (e.g. bullet holes in walls) while + // offering a failsafe to prevent shaders becoming too slow if there are + // extremely large numbers of decals. + 1024 + } else { + 8 + } +} diff --git a/vendor/bevy_pbr/src/decal/clustered.wgsl b/vendor/bevy_pbr/src/decal/clustered.wgsl new file mode 100644 index 00000000..a6404ecd --- /dev/null +++ b/vendor/bevy_pbr/src/decal/clustered.wgsl @@ -0,0 +1,265 @@ +// Support code for clustered decals. +// +// This module provides an iterator API, which you may wish to use in your own +// shaders if you want clustered decals to provide textures other than the base +// color. The iterator API allows you to iterate over all decals affecting the +// current fragment. Use `clustered_decal_iterator_new()` and +// `clustered_decal_iterator_next()` as follows: +// +// let view_z = get_view_z(vec4(world_position, 1.0)); +// let is_orthographic = view_is_orthographic(); +// +// let cluster_index = +// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); +// var clusterable_object_index_ranges = +// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); +// +// var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); +// while (clustered_decal_iterator_next(&iterator)) { +// ... sample from the texture at iterator.texture_index at iterator.uv ... +// } +// +// In this way, in conjunction with a custom material, you can provide your own +// texture arrays that mirror `mesh_view_bindings::clustered_decal_textures` in +// order to support decals with normal maps, etc. +// +// Note that the order in which decals are returned is currently unpredictable, +// though generally stable from frame to frame. + +#define_import_path bevy_pbr::decal::clustered + +#import bevy_pbr::clustered_forward +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::pbr_functions +#import bevy_pbr::pbr_types::PbrInput +#import bevy_pbr::utils::porter_duff_over +#import bevy_render::maths + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else +#import bevy_pbr::forward_io::VertexOutput +#endif + +// An object that allows stepping through all clustered decals that affect a +// single fragment. +struct ClusteredDecalIterator { + // Public fields follow: + // The index of the decal base color texture in the binding array. + base_color_texture_index: i32, + // The index of the decal normal map texture in the binding array. + normal_map_texture_index: i32, + // The index of the decal metallic-roughness texture in the binding array. + metallic_roughness_texture_index: i32, + // The index of the decal emissive texture in the binding array. + emissive_texture_index: i32, + // The UV coordinates at which to sample that decal texture. + uv: vec2, + // A custom tag you can use for your own purposes. + tag: u32, + + // Private fields follow: + // The current offset of the index in the `ClusterableObjectIndexRanges` list. + decal_index_offset: i32, + // The end offset of the index in the `ClusterableObjectIndexRanges` list. + end_offset: i32, + // The world-space position of the fragment. + world_position: vec3, +} + +#ifdef CLUSTERED_DECALS_ARE_USABLE + +// Creates a new iterator over the decals at the current fragment. +// +// You can retrieve `clusterable_object_index_ranges` as follows: +// +// let view_z = get_view_z(world_position); +// let is_orthographic = view_is_orthographic(); +// +// let cluster_index = +// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); +// var clusterable_object_index_ranges = +// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); +fn clustered_decal_iterator_new( + world_position: vec3, + clusterable_object_index_ranges: ptr +) -> ClusteredDecalIterator { + return ClusteredDecalIterator( + -1, + -1, + -1, + -1, + vec2(0.0), + 0u, + // We subtract 1 because the first thing `decal_iterator_next` does is + // add 1. + i32((*clusterable_object_index_ranges).first_decal_offset) - 1, + i32((*clusterable_object_index_ranges).last_clusterable_object_index_offset), + world_position, + ); +} + +// Populates the `iterator.texture_index` and `iterator.uv` fields for the next +// decal overlapping the current world position. +// +// Returns true if another decal was found or false if no more decals were found +// for this position. +fn clustered_decal_iterator_next(iterator: ptr) -> bool { + if ((*iterator).decal_index_offset == (*iterator).end_offset) { + return false; + } + + (*iterator).decal_index_offset += 1; + + while ((*iterator).decal_index_offset < (*iterator).end_offset) { + let decal_index = i32(clustered_forward::get_clusterable_object_id( + u32((*iterator).decal_index_offset) + )); + let decal_space_vector = + (mesh_view_bindings::clustered_decals.decals[decal_index].local_from_world * + vec4((*iterator).world_position, 1.0)).xyz; + + if (all(decal_space_vector >= vec3(-0.5)) && all(decal_space_vector <= vec3(0.5))) { + (*iterator).base_color_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].base_color_texture_index + ); + (*iterator).normal_map_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].normal_map_texture_index + ); + (*iterator).metallic_roughness_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[ + decal_index + ].metallic_roughness_texture_index + ); + (*iterator).emissive_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].emissive_texture_index + ); + (*iterator).uv = decal_space_vector.xy * vec2(1.0, -1.0) + vec2(0.5); + (*iterator).tag = + mesh_view_bindings::clustered_decals.decals[decal_index].tag; + return true; + } + + (*iterator).decal_index_offset += 1; + } + + return false; +} + +#endif // CLUSTERED_DECALS_ARE_USABLE + +// Returns the view-space Z coordinate for the given world position. +fn get_view_z(world_position: vec3) -> f32 { + return dot(vec4( + mesh_view_bindings::view.view_from_world[0].z, + mesh_view_bindings::view.view_from_world[1].z, + mesh_view_bindings::view.view_from_world[2].z, + mesh_view_bindings::view.view_from_world[3].z + ), vec4(world_position, 1.0)); +} + +// Returns true if the current view describes an orthographic projection or +// false otherwise. +fn view_is_orthographic() -> bool { + return mesh_view_bindings::view.clip_from_view[3].w == 1.0; +} + +fn apply_decals(pbr_input: ptr) { + let world_position = (*pbr_input).world_position.xyz; + let world_normal = (*pbr_input).world_normal; + let frag_coord = (*pbr_input).frag_coord.xy; + + var base_color = (*pbr_input).material.base_color; + var emissive = (*pbr_input).material.emissive; + var Nt = (*pbr_input).N; + var metallic = (*pbr_input).material.metallic; + var perceptual_roughness = (*pbr_input).material.perceptual_roughness; + +#ifdef CLUSTERED_DECALS_ARE_USABLE + // Fetch the clusterable object index ranges for this world position. + + let view_z = get_view_z(world_position); + let is_orthographic = view_is_orthographic(); + + let cluster_index = + clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + + // Iterate over decals. + + var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); + while (clustered_decal_iterator_next(&iterator)) { + // Apply base color and metallic/roughness. + if (iterator.base_color_texture_index >= 0) { + let decal_base_color = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.base_color_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + + // Apply the metallic (blue channel) and the roughness (green channel) map. + if (iterator.metallic_roughness_texture_index >= 0) { + let metallic_roughness_sampler = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[ + iterator.metallic_roughness_texture_index + ], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + // Use OVER compositing using the base color alpha. + metallic = mix( + metallic * base_color.a, + metallic_roughness_sampler.b, + decal_base_color.a + ); + perceptual_roughness = mix( + perceptual_roughness * base_color.a, + metallic_roughness_sampler.g, + decal_base_color.a + ); + } + + // Apply base color with the standard OVER compositing operator. + base_color = porter_duff_over(base_color, decal_base_color); + } + +#ifdef VERTEX_TANGENTS + if (iterator.normal_map_texture_index >= 0) { + let Nd = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.normal_map_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ).rgb * 2.0 - 1.0; + // This is the *Whiteout* normal map blending operator from [1]. + // + // [1]: https://blog.selfshadow.com/publications/blending-in-detail/ + Nt = vec3(Nt.xy + Nd.xy, Nt.z * Nd.z); + } +#endif // VERTEX_TANGENTS + + // Apply emissive. + if (iterator.emissive_texture_index >= 0) { + let decal_emissive = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.emissive_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + emissive += vec4(decal_emissive.rgb, 0.0); + } + } +#endif // CLUSTERED_DECALS_ARE_USABLE + + (*pbr_input).material.base_color = base_color; + (*pbr_input).material.emissive = emissive; + (*pbr_input).N = normalize(Nt); + (*pbr_input).material.metallic = metallic; + (*pbr_input).material.perceptual_roughness = perceptual_roughness; +} diff --git a/vendor/bevy_pbr/src/decal/forward.rs b/vendor/bevy_pbr/src/decal/forward.rs new file mode 100644 index 00000000..c90c1f00 --- /dev/null +++ b/vendor/bevy_pbr/src/decal/forward.rs @@ -0,0 +1,167 @@ +use crate::{ + ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline, + MaterialPlugin, StandardMaterial, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, Assets, Handle}; +use bevy_ecs::{ + component::Component, lifecycle::HookContext, resource::Resource, world::DeferredWorld, +}; +use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; +use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}; +use bevy_reflect::{Reflect, TypePath}; +use bevy_render::{ + alpha::AlphaMode, + render_asset::RenderAssets, + render_resource::{ + AsBindGroup, AsBindGroupShaderType, CompareFunction, RenderPipelineDescriptor, ShaderType, + SpecializedMeshPipelineError, + }, + texture::GpuImage, + RenderDebugFlags, +}; +use bevy_shader::load_shader_library; + +/// Plugin to render [`ForwardDecal`]s. +pub struct ForwardDecalPlugin; + +impl Plugin for ForwardDecalPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "forward_decal.wgsl"); + + let mesh = app.world_mut().resource_mut::>().add( + Rectangle::from_size(Vec2::ONE) + .mesh() + .build() + .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) + .with_generated_tangents() + .unwrap(), + ); + + app.insert_resource(ForwardDecalMesh(mesh)); + + app.add_plugins(MaterialPlugin::> { + debug_flags: RenderDebugFlags::default(), + ..Default::default() + }); + } +} + +/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying +/// geometry towards the edges. +/// +/// Because forward decals are meshes, you can use arbitrary materials to control their appearance. +/// +/// # Usage Notes +/// +/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`]. +/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::prepass::DepthPrepass`] component. +/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's +/// texture with extra transparent pixels on the edges. +/// * On Wasm, requires using WebGPU and disabling `Msaa` on your camera. +#[derive(Component, Reflect)] +#[require(Mesh3d)] +#[component(on_add=forward_decal_set_mesh)] +pub struct ForwardDecal; + +/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension. +/// +/// Make sure to register the [`MaterialPlugin`] for this material in your app setup. +/// +/// [`StandardMaterial`] comes with out of the box support for forward decals. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type ForwardDecalMaterial = ExtendedMaterial; + +/// Material extension for a [`ForwardDecal`]. +/// +/// In addition to wrapping your material type with this extension, your shader must use +/// the `bevy_pbr::decal::forward::get_forward_decal_info` function. +/// +/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate +/// the forward decal code behind an ifdef. +#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)] +#[uniform(200, ForwardDecalMaterialExtUniform)] +pub struct ForwardDecalMaterialExt { + /// Controls the distance threshold for decal blending with surfaces. + /// + /// This parameter determines how far away a surface can be before the decal no longer blends + /// with it and instead renders with full opacity. + /// + /// Lower values cause the decal to only blend with close surfaces, while higher values allow + /// blending with more distant surfaces. + /// + /// Units are in meters. + pub depth_fade_factor: f32, +} + +#[derive(Clone, Default, ShaderType)] +pub struct ForwardDecalMaterialExtUniform { + pub inv_depth_fade_factor: f32, +} + +impl AsBindGroupShaderType for ForwardDecalMaterialExt { + fn as_bind_group_shader_type( + &self, + _images: &RenderAssets, + ) -> ForwardDecalMaterialExtUniform { + ForwardDecalMaterialExtUniform { + inv_depth_fade_factor: 1.0 / self.depth_fade_factor.max(0.001), + } + } +} + +impl MaterialExtension for ForwardDecalMaterialExt { + fn alpha_mode() -> Option { + Some(AlphaMode::Blend) + } + + fn enable_shadows() -> bool { + false + } + + fn specialize( + _pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: MaterialExtensionKey, + ) -> Result<(), SpecializedMeshPipelineError> { + descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always; + + descriptor.vertex.shader_defs.push("FORWARD_DECAL".into()); + + if let Some(fragment) = &mut descriptor.fragment { + fragment.shader_defs.push("FORWARD_DECAL".into()); + } + + if let Some(label) = &mut descriptor.label { + *label = format!("forward_decal_{label}").into(); + } + + Ok(()) + } +} + +impl Default for ForwardDecalMaterialExt { + fn default() -> Self { + Self { + depth_fade_factor: 8.0, + } + } +} + +#[derive(Resource)] +struct ForwardDecalMesh(Handle); + +// Note: We need to use a hook here instead of required components since we cannot access resources +// with required components, and we can't otherwise get a handle to the asset from a required +// component constructor, since the constructor must be a function pointer, and we intentionally do +// not want to use `uuid_handle!`. +fn forward_decal_set_mesh(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { + let decal_mesh = world.resource::().0.clone(); + let mut entity = world.entity_mut(entity); + let mut entity_mesh = entity.get_mut::().unwrap(); + // Only replace the mesh handle if the mesh handle is defaulted. + if **entity_mesh == Handle::default() { + entity_mesh.0 = decal_mesh; + } +} diff --git a/vendor/bevy_pbr/src/decal/forward_decal.wgsl b/vendor/bevy_pbr/src/decal/forward_decal.wgsl new file mode 100644 index 00000000..ffbb5f95 --- /dev/null +++ b/vendor/bevy_pbr/src/decal/forward_decal.wgsl @@ -0,0 +1,52 @@ +#define_import_path bevy_pbr::decal::forward + +#import bevy_pbr::{ + forward_io::VertexOutput, + mesh_functions::get_world_from_local, + mesh_view_bindings::view, + pbr_functions::calculate_tbn_mikktspace, + prepass_utils::prepass_depth, + view_transformations::depth_ndc_to_view_z, +} +#import bevy_render::maths::project_onto + +@group(#{MATERIAL_BIND_GROUP}) @binding(200) +var inv_depth_fade_factor: f32; + +struct ForwardDecalInformation { + world_position: vec4, + uv: vec2, + alpha: f32, +} + +fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation { + let world_from_local = get_world_from_local(in.instance_index); + let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz; + let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w); + + let V = normalize(view.world_position - in.world_position.xyz); + + // Transform V from fragment to camera in world space to tangent space. + let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); + + let frag_depth = depth_ndc_to_view_z(in.position.z); + let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u)); + let diff_depth = frag_depth - depth_pass_depth; + let diff_depth_abs = abs(diff_depth); + + // Apply UV parallax + let contact_on_decal = project_onto(V * diff_depth, in.world_normal); + let normal_depth = length(contact_on_decal); + let view_steepness = abs(Vt.z); + let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + let uv = in.uv + delta_uv; + + let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w); + let alpha = saturate(1.0 - (normal_depth * inv_depth_fade_factor)); + + return ForwardDecalInformation(world_position, uv, alpha); +} diff --git a/vendor/bevy_pbr/src/decal/mod.rs b/vendor/bevy_pbr/src/decal/mod.rs new file mode 100644 index 00000000..1b921317 --- /dev/null +++ b/vendor/bevy_pbr/src/decal/mod.rs @@ -0,0 +1,11 @@ +//! Decal rendering. +//! +//! Decals are a material that render on top of the surface that they're placed above. +//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces. + +// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs. + +pub mod clustered; +mod forward; + +pub use forward::*; diff --git a/vendor/bevy_pbr/src/deferred/deferred_lighting.wgsl b/vendor/bevy_pbr/src/deferred/deferred_lighting.wgsl new file mode 100644 index 00000000..7c14eea4 --- /dev/null +++ b/vendor/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -0,0 +1,88 @@ +#import bevy_pbr::{ + prepass_utils, + pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, + pbr_functions, + pbr_deferred_functions::pbr_input_from_deferred_gbuffer, + pbr_deferred_types::unpack_unorm3x4_plus_unorm_20_, + lighting, + mesh_view_bindings::deferred_prepass_texture, +} + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION +#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture +#import bevy_pbr::ssao_utils::ssao_multibounce +#endif + +struct FullscreenVertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, +}; + +struct PbrDeferredLightingDepthId { + depth_id: u32, // limited to u8 +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _webgl2_padding_0: f32, + _webgl2_padding_1: f32, + _webgl2_padding_2: f32, +#endif +} +@group(2) @binding(0) +var depth_id: PbrDeferredLightingDepthId; + +@vertex +fn vertex(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput { + // See the full screen vertex shader for explanation above for how this works. + let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; + // Depth is stored as unorm, so we are dividing the u8 depth_id by 255.0 here. + let clip_position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), f32(depth_id.depth_id) / 255.0, 1.0); + + return FullscreenVertexOutput(clip_position, uv); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + var frag_coord = vec4(in.position.xy, 0.0, 0.0); + + let deferred_data = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + +#ifdef WEBGL2 + frag_coord.z = unpack_unorm3x4_plus_unorm_20_(deferred_data.b).w; +#else +#ifdef DEPTH_PREPASS + frag_coord.z = prepass_utils::prepass_depth(in.position, 0u); +#endif +#endif + + var pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, deferred_data); + var output_color = vec4(0.0); + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION + let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); + pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce); + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + var perceptual_roughness: f32 = pbr_input.material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + pbr_input.specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); +#endif // SCREEN_SPACE_AMBIENT_OCCLUSION + + output_color = pbr_functions::apply_pbr_lighting(pbr_input); + } else { + output_color = pbr_input.material.base_color; + } + + output_color = pbr_functions::main_pass_post_lighting_processing(pbr_input, output_color); + + return output_color; +} + diff --git a/vendor/bevy_pbr/src/deferred/mod.rs b/vendor/bevy_pbr/src/deferred/mod.rs new file mode 100644 index 00000000..41766372 --- /dev/null +++ b/vendor/bevy_pbr/src/deferred/mod.rs @@ -0,0 +1,585 @@ +use crate::{ + graph::NodePbr, MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, + ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, + ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset, + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, +}; +use crate::{ + DistanceFog, ExtractedAtmosphere, MeshPipelineKey, ViewFogUniformOffset, + ViewLightsUniformOffset, +}; +use bevy_app::prelude::*; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + deferred::{ + copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + }, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::BevyDefault as _; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, ShadowFilteringMethod}; +use bevy_render::RenderStartup; +use bevy_render::{ + diagnostic::RecordDiagnostics, + extract_component::{ + ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, + }, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_resource::{binding_types::uniform_buffer, *}, + renderer::RenderContext, + view::{ExtractedView, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::{Shader, ShaderDefVal}; +use bevy_utils::default; + +pub struct DeferredPbrLightingPlugin; + +pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1; + +/// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass. +/// +/// Will be automatically added to entities with the [`DeferredPrepass`] component that don't already have a [`PbrDeferredLightingDepthId`]. +#[derive(Component, Clone, Copy, ExtractComponent, ShaderType)] +pub struct PbrDeferredLightingDepthId { + depth_id: u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: f32, +} + +impl PbrDeferredLightingDepthId { + pub fn new(value: u8) -> PbrDeferredLightingDepthId { + PbrDeferredLightingDepthId { + depth_id: value as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: 0.0, + } + } + + pub fn set(&mut self, value: u8) { + self.depth_id = value as u32; + } + + pub fn get(&self) -> u8 { + self.depth_id as u8 + } +} + +impl Default for PbrDeferredLightingDepthId { + fn default() -> Self { + PbrDeferredLightingDepthId { + depth_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: 0.0, + } + } +} + +impl Plugin for DeferredPbrLightingPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )) + .add_systems(PostUpdate, insert_deferred_lighting_pass_id_component); + + embedded_asset!(app, "deferred_lighting.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .add_systems(RenderStartup, init_deferred_lighting_layout) + .add_systems( + Render, + (prepare_deferred_lighting_pipelines.in_set(RenderSystems::Prepare),), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::DeferredLightingPass, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::StartMainPass, + NodePbr::DeferredLightingPass, + Node3d::MainOpaquePass, + ), + ); + } +} + +#[derive(Default)] +pub struct DeferredOpaquePass3dPbrLightingNode; + +impl ViewNode for DeferredOpaquePass3dPbrLightingNode { + type ViewQuery = ( + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, + &'static MeshViewBindGroup, + &'static ViewTarget, + &'static DeferredLightingIdDepthTexture, + &'static DeferredLightingPipeline, + ); + + fn run( + &self, + _graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + mesh_view_bind_group, + target, + deferred_lighting_id_depth_texture, + deferred_lighting_pipeline, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let deferred_lighting_layout = world.resource::(); + + let Some(pipeline) = + pipeline_cache.get_render_pipeline(deferred_lighting_pipeline.pipeline_id) + else { + return Ok(()); + }; + + let deferred_lighting_pass_id = + world.resource::>(); + let Some(deferred_lighting_pass_id_binding) = + deferred_lighting_pass_id.uniforms().binding() + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let bind_group_2 = render_context.render_device().create_bind_group( + "deferred_lighting_layout_group_2", + &pipeline_cache.get_bind_group_layout(&deferred_lighting_layout.bind_group_layout_2), + &BindGroupEntries::single(deferred_lighting_pass_id_binding), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("deferred_lighting"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &deferred_lighting_id_depth_texture.texture.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Discard, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "deferred_lighting"); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, &bind_group_2, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +#[derive(Resource)] +pub struct DeferredLightingLayout { + mesh_pipeline: MeshPipeline, + bind_group_layout_2: BindGroupLayoutDescriptor, + deferred_lighting_shader: Handle, +} + +#[derive(Component)] +pub struct DeferredLightingPipeline { + pub pipeline_id: CachedRenderPipelineId, +} + +impl SpecializedRenderPipeline for DeferredLightingLayout { + type Key = MeshPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + + // Let the shader code know that it's running in a deferred pipeline. + shader_defs.push("DEFERRED_LIGHTING_PIPELINE".into()); + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, + )); + + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) { + shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into()); + } + + if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if key.contains(MeshPipelineKey::IRRADIANCE_VOLUME) { + shader_defs.push("IRRADIANCE_VOLUME".into()); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::SCREEN_SPACE_REFLECTIONS) { + shader_defs.push("SCREEN_SPACE_REFLECTIONS".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + + if key.contains(MeshPipelineKey::DISTANCE_FOG) { + shader_defs.push("DISTANCE_FOG".into()); + } + if key.contains(MeshPipelineKey::ATMOSPHERE) { + shader_defs.push("ATMOSPHERE".into()); + } + + // Always true, since we're in the deferred lighting pipeline + shader_defs.push("DEFERRED_PREPASS".into()); + + let shadow_filter_method = + key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); + if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { + shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); + } + if self.mesh_pipeline.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); + + let layout = self.mesh_pipeline.get_view_layout(key.into()); + RenderPipelineDescriptor { + label: Some("deferred_lighting_pipeline".into()), + layout: vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout_2.clone(), + ], + vertex: VertexState { + shader: self.deferred_lighting_shader.clone(), + shader_defs: shader_defs.clone(), + ..default() + }, + fragment: Some(FragmentState { + shader: self.deferred_lighting_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + depth_stencil: Some(DepthStencilState { + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + ..default() + } + } +} + +pub fn init_deferred_lighting_layout( + mut commands: Commands, + mesh_pipeline: Res, + asset_server: Res, +) { + let layout = BindGroupLayoutDescriptor::new( + "deferred_lighting_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(false), + ), + ); + commands.insert_resource(DeferredLightingLayout { + mesh_pipeline: mesh_pipeline.clone(), + bind_group_layout_2: layout, + deferred_lighting_shader: load_embedded_asset!( + asset_server.as_ref(), + "deferred_lighting.wgsl" + ), + }); +} + +pub fn insert_deferred_lighting_pass_id_component( + mut commands: Commands, + views: Query, Without)>, +) { + for entity in views.iter() { + commands + .entity(entity) + .insert(PbrDeferredLightingDepthId::default()); + } +} + +pub fn prepare_deferred_lighting_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + deferred_lighting_layout: Res, + views: Query<( + Entity, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + ( + Has, + Has, + Has, + ), + ( + Has, + Has, + Has, + Has, + ), + Has>, + Has>, + Has, + Has, + )>, +) { + for ( + entity, + view, + tonemapping, + dither, + shadow_filter_method, + (ssao, ssr, distance_fog), + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + has_environment_maps, + has_irradiance_volumes, + skip_deferred_lighting, + has_atmosphere, + ) in &views + { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { + commands.entity(entity).remove::(); + continue; + } + + let mut view_key = MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if has_atmosphere { + view_key |= MeshPipelineKey::ATMOSPHERE; + } + + if view.invert_culling { + view_key |= MeshPipelineKey::INVERT_CULLING; + } + + // Always true, since we're in the deferred lighting pipeline + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if ssr { + view_key |= MeshPipelineKey::SCREEN_SPACE_REFLECTIONS; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + + // We don't need to check to see whether the environment map is loaded + // because [`gather_light_probes`] already checked that for us before + // adding the [`RenderViewEnvironmentMaps`] component. + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &deferred_lighting_layout, view_key); + + commands + .entity(entity) + .insert(DeferredLightingPipeline { pipeline_id }); + } +} + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; diff --git a/vendor/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/vendor/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl new file mode 100644 index 00000000..e6254b11 --- /dev/null +++ b/vendor/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -0,0 +1,153 @@ +#define_import_path bevy_pbr::pbr_deferred_functions + +#import bevy_pbr::{ + pbr_types::{PbrInput, pbr_input_new, STANDARD_MATERIAL_FLAGS_UNLIT_BIT}, + pbr_deferred_types as deferred_types, + pbr_functions, + rgb9e5, + mesh_view_bindings::view, + utils::{octahedral_encode, octahedral_decode}, + prepass_io::FragmentOutput, + view_transformations::{position_ndc_to_world, frag_coord_to_ndc}, +} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else +#import bevy_pbr::prepass_io::VertexOutput +#endif + +#ifdef MOTION_VECTOR_PREPASS + #import bevy_pbr::pbr_prepass_functions::calculate_motion_vector +#endif + +// Creates the deferred gbuffer from a PbrInput. +fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { + // Only monochrome occlusion supported. May not be worth including at all. + // Some models have baked occlusion, GLTF only supports monochrome. + // Real time occlusion is applied in the deferred lighting pass. + // Deriving luminance via Rec. 709. coefficients + // https://en.wikipedia.org/wiki/Rec._709 + let rec_709_coeffs = vec3(0.2126, 0.7152, 0.0722); + let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs); + // Only monochrome specular supported. + let reflectance = dot(in.material.reflectance, rec_709_coeffs); +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( + reflectance, + in.material.metallic, + diffuse_occlusion, + in.frag_coord.z)); +#else + var props = deferred_types::pack_unorm4x8_(vec4( + reflectance, // could be fewer bits + in.material.metallic, // could be fewer bits + diffuse_occlusion, // is this worth including? + 0.0)); // spare +#endif // WEBGL2 + let flags = deferred_types::deferred_flags_from_mesh_material_flags(in.flags, in.material.flags); + let octahedral_normal = octahedral_encode(normalize(in.N)); + var base_color_srgb = vec3(0.0); + var emissive = in.material.emissive.rgb; + if ((in.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + // Material is unlit, use emissive component of gbuffer for color data. + // Unlit materials are effectively emissive. + emissive = in.material.base_color.rgb; + } else { + base_color_srgb = pow(in.material.base_color.rgb, vec3(1.0 / 2.2)); + } + + // Utilize the emissive channel to transmit the lightmap data. To ensure + // it matches the output in forward shading, pre-multiply it with the + // calculated diffuse color. + let base_color = in.material.base_color.rgb; + let metallic = in.material.metallic; + let specular_transmission = in.material.specular_transmission; + let diffuse_transmission = in.material.diffuse_transmission; + let diffuse_color = pbr_functions::calculate_diffuse_color( + base_color, + metallic, + specular_transmission, + diffuse_transmission + ); + emissive += in.lightmap_light * diffuse_color * view.exposure; + + let deferred = vec4( + deferred_types::pack_unorm4x8_(vec4(base_color_srgb, in.material.perceptual_roughness)), + rgb9e5::vec3_to_rgb9e5_(emissive), + props, + deferred_types::pack_24bit_normal_and_flags(octahedral_normal, flags), + ); + return deferred; +} + +// Creates a PbrInput from the deferred gbuffer. +fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> PbrInput { + var pbr = pbr_input_new(); + + let flags = deferred_types::unpack_flags(gbuffer.a); + let deferred_flags = deferred_types::mesh_material_flags_from_deferred_flags(flags); + pbr.flags = deferred_flags.x; + pbr.material.flags = deferred_flags.y; + + let base_rough = deferred_types::unpack_unorm4x8_(gbuffer.r); + pbr.material.perceptual_roughness = base_rough.a; + let emissive = rgb9e5::rgb9e5_to_vec3_(gbuffer.g); + if ((pbr.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + pbr.material.base_color = vec4(emissive, 1.0); + pbr.material.emissive = vec4(vec3(0.0), 0.0); + } else { + pbr.material.base_color = vec4(pow(base_rough.rgb, vec3(2.2)), 1.0); + pbr.material.emissive = vec4(emissive, 0.0); + } +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); + // Bias to 0.5 since that's the value for almost all materials. + pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333)); +#else + let props = deferred_types::unpack_unorm4x8_(gbuffer.b); + pbr.material.reflectance = vec3(props.r); +#endif // WEBGL2 + pbr.material.metallic = props.g; + pbr.diffuse_occlusion = vec3(props.b); + let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a); + let N = octahedral_decode(octahedral_normal); + + let world_position = vec4(position_ndc_to_world(frag_coord_to_ndc(frag_coord)), 1.0); + let is_orthographic = view.clip_from_view[3].w == 1.0; + let V = pbr_functions::calculate_view(world_position, is_orthographic); + + pbr.frag_coord = frag_coord; + pbr.world_normal = N; + pbr.world_position = world_position; + pbr.N = N; + pbr.V = V; + pbr.is_orthographic = is_orthographic; + + return pbr; +} + +#ifdef PREPASS_PIPELINE +fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput { + var out: FragmentOutput; + + // gbuffer + out.deferred = deferred_gbuffer_from_pbr_input(pbr_input); + // lighting pass id (used to determine which lighting shader to run for the fragment) + out.deferred_lighting_pass_id = pbr_input.material.deferred_lighting_pass_id; + // normal if required +#ifdef NORMAL_PREPASS + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + // motion vectors if required +#ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else + out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position); +#endif +#endif + + return out; +} +#endif diff --git a/vendor/bevy_pbr/src/deferred/pbr_deferred_types.wgsl b/vendor/bevy_pbr/src/deferred/pbr_deferred_types.wgsl new file mode 100644 index 00000000..fb4def94 --- /dev/null +++ b/vendor/bevy_pbr/src/deferred/pbr_deferred_types.wgsl @@ -0,0 +1,89 @@ +#define_import_path bevy_pbr::pbr_deferred_types + +#import bevy_pbr::{ + mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT, + pbr_types::{STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT}, +} + +// Maximum of 8 bits available +const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u << 0u; +const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 1u; +const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 2u; + +fn deferred_flags_from_mesh_material_flags(mesh_flags: u32, mat_flags: u32) -> u32 { + var flags = 0u; + flags |= u32((mesh_flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) * DEFERRED_FLAGS_FOG_ENABLED_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) * DEFERRED_FLAGS_UNLIT_BIT; + return flags; +} + +fn mesh_material_flags_from_deferred_flags(deferred_flags: u32) -> vec2 { + var mat_flags = 0u; + var mesh_flags = 0u; + mesh_flags |= u32((deferred_flags & DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * MESH_FLAGS_SHADOW_RECEIVER_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_FOG_ENABLED_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_UNLIT_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_UNLIT_BIT; + return vec2(mesh_flags, mat_flags); +} + +const U12MAXF = 4095.0; +const U16MAXF = 65535.0; +const U20MAXF = 1048575.0; + +// Storing normals as oct24. +// Flags are stored in the remaining 8 bits. +// https://jcgt.org/published/0003/02/01/paper.pdf +// Could possibly go down to oct20 if the space is needed. + +fn pack_24bit_normal_and_flags(octahedral_normal: vec2, flags: u32) -> u32 { + let unorm1 = u32(saturate(octahedral_normal.x) * U12MAXF + 0.5); + let unorm2 = u32(saturate(octahedral_normal.y) * U12MAXF + 0.5); + return (unorm1 & 0xFFFu) | ((unorm2 & 0xFFFu) << 12u) | ((flags & 0xFFu) << 24u); +} + +fn unpack_24bit_normal(packed: u32) -> vec2 { + let unorm1 = packed & 0xFFFu; + let unorm2 = (packed >> 12u) & 0xFFFu; + return vec2(f32(unorm1) / U12MAXF, f32(unorm2) / U12MAXF); +} + +fn unpack_flags(packed: u32) -> u32 { + return (packed >> 24u) & 0xFFu; +} + +// The builtin one didn't work in webgl. +// "'unpackUnorm4x8' : no matching overloaded function found" +// https://github.com/gfx-rs/naga/issues/2006 +fn unpack_unorm4x8_(v: u32) -> vec4 { + return vec4( + f32(v & 0xFFu), + f32((v >> 8u) & 0xFFu), + f32((v >> 16u) & 0xFFu), + f32((v >> 24u) & 0xFFu) + ) / 255.0; +} + +// 'packUnorm4x8' : no matching overloaded function found +// https://github.com/gfx-rs/naga/issues/2006 +fn pack_unorm4x8_(values: vec4) -> u32 { + let v = vec4(saturate(values) * 255.0 + 0.5); + return (v.w << 24u) | (v.z << 16u) | (v.y << 8u) | v.x; +} + +// Pack 3x 4bit unorm + 1x 20bit +fn pack_unorm3x4_plus_unorm_20_(v: vec4) -> u32 { + let sm = vec3(saturate(v.xyz) * 15.0 + 0.5); + let bg = u32(saturate(v.w) * U20MAXF + 0.5); + return (bg << 12u) | (sm.z << 8u) | (sm.y << 4u) | sm.x; +} + +// Unpack 3x 4bit unorm + 1x 20bit +fn unpack_unorm3x4_plus_unorm_20_(v: u32) -> vec4 { + return vec4( + f32(v & 0xfu) / 15.0, + f32((v >> 4u) & 0xFu) / 15.0, + f32((v >> 8u) & 0xFu) / 15.0, + f32((v >> 12u) & 0xFFFFFFu) / U20MAXF, + ); +} diff --git a/vendor/bevy_pbr/src/diagnostic.rs b/vendor/bevy_pbr/src/diagnostic.rs new file mode 100644 index 00000000..79affaae --- /dev/null +++ b/vendor/bevy_pbr/src/diagnostic.rs @@ -0,0 +1,124 @@ +use core::{ + any::{type_name, Any, TypeId}, + marker::PhantomData, +}; + +use bevy_app::{Plugin, PreUpdate}; +use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}; +use bevy_ecs::{resource::Resource, system::Res}; +use bevy_platform::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use bevy_render::{Extract, ExtractSchedule, RenderApp}; + +use crate::{Material, MaterialBindGroupAllocators}; + +pub struct MaterialAllocatorDiagnosticPlugin { + suffix: &'static str, + _phantom: PhantomData, +} + +impl MaterialAllocatorDiagnosticPlugin { + pub fn new(suffix: &'static str) -> Self { + Self { + suffix, + _phantom: PhantomData, + } + } +} + +impl Default for MaterialAllocatorDiagnosticPlugin { + fn default() -> Self { + Self { + suffix: " materials", + _phantom: PhantomData, + } + } +} + +impl MaterialAllocatorDiagnosticPlugin { + /// Get the [`DiagnosticPath`] for slab count + pub fn slabs_diagnostic_path() -> DiagnosticPath { + DiagnosticPath::from_components(["material_allocator_slabs", type_name::()]) + } + /// Get the [`DiagnosticPath`] for total slabs size + pub fn slabs_size_diagnostic_path() -> DiagnosticPath { + DiagnosticPath::from_components(["material_allocator_slabs_size", type_name::()]) + } + /// Get the [`DiagnosticPath`] for material allocations + pub fn allocations_diagnostic_path() -> DiagnosticPath { + DiagnosticPath::from_components(["material_allocator_allocations", type_name::()]) + } +} + +impl Plugin for MaterialAllocatorDiagnosticPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.register_diagnostic( + Diagnostic::new(Self::slabs_diagnostic_path()).with_suffix(" slabs"), + ) + .register_diagnostic( + Diagnostic::new(Self::slabs_size_diagnostic_path()).with_suffix(" bytes"), + ) + .register_diagnostic( + Diagnostic::new(Self::allocations_diagnostic_path()).with_suffix(self.suffix), + ) + .init_resource::>() + .add_systems(PreUpdate, add_material_allocator_measurement::); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(ExtractSchedule, measure_allocator::); + } + } +} + +#[derive(Debug, Resource)] +struct MaterialAllocatorMeasurements { + slabs: AtomicUsize, + slabs_size: AtomicUsize, + allocations: AtomicU64, + _phantom: PhantomData, +} + +impl Default for MaterialAllocatorMeasurements { + fn default() -> Self { + Self { + slabs: AtomicUsize::default(), + slabs_size: AtomicUsize::default(), + allocations: AtomicU64::default(), + _phantom: PhantomData, + } + } +} + +fn add_material_allocator_measurement( + mut diagnostics: Diagnostics, + measurements: Res>, +) { + diagnostics.add_measurement( + &MaterialAllocatorDiagnosticPlugin::::slabs_diagnostic_path(), + || measurements.slabs.load(Ordering::Relaxed) as f64, + ); + diagnostics.add_measurement( + &MaterialAllocatorDiagnosticPlugin::::slabs_size_diagnostic_path(), + || measurements.slabs_size.load(Ordering::Relaxed) as f64, + ); + diagnostics.add_measurement( + &MaterialAllocatorDiagnosticPlugin::::allocations_diagnostic_path(), + || measurements.allocations.load(Ordering::Relaxed) as f64, + ); +} + +fn measure_allocator( + measurements: Extract>>, + allocators: Res, +) { + if let Some(allocator) = allocators.get(&TypeId::of::()) { + measurements + .slabs + .store(allocator.slab_count(), Ordering::Relaxed); + measurements + .slabs_size + .store(allocator.slabs_size(), Ordering::Relaxed); + measurements + .allocations + .store(allocator.allocations(), Ordering::Relaxed); + } +} diff --git a/vendor/bevy_pbr/src/extended_material.rs b/vendor/bevy_pbr/src/extended_material.rs new file mode 100644 index 00000000..732648bf --- /dev/null +++ b/vendor/bevy_pbr/src/extended_material.rs @@ -0,0 +1,436 @@ +use alloc::borrow::Cow; + +use bevy_asset::Asset; +use bevy_ecs::system::SystemParamItem; +use bevy_mesh::MeshVertexBufferLayoutRef; +use bevy_platform::{collections::HashSet, hash::FixedHasher}; +use bevy_reflect::{impl_type_path, Reflect}; +use bevy_render::{ + alpha::AlphaMode, + render_resource::{ + AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, + BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, + SpecializedMeshPipelineError, UnpreparedBindGroup, + }, + renderer::RenderDevice, +}; +use bevy_shader::ShaderRef; + +use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey}; + +pub struct MaterialExtensionPipeline { + pub mesh_pipeline: MeshPipeline, +} + +pub struct MaterialExtensionKey { + pub mesh_key: MeshPipelineKey, + pub bind_group_data: E::Data, +} + +/// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`. +/// +/// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct. +pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader + /// will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + // Returns this material’s AlphaMode. If None is returned, the base material alpha mode will be used. + fn alpha_mode() -> Option { + None + } + + /// Controls if the prepass is enabled for the Material. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + #[inline] + fn enable_prepass() -> bool { + true + } + + /// Controls if shadows are enabled for the Material. + #[inline] + fn enable_shadows() -> bool { + true + } + + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader + /// will be used. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader + /// will be used. + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader + /// will be used. + fn deferred_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader + /// will be used. + fn deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's + /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. + /// Specialization for the base material is applied before this function is called. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize( + pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialExtensionKey, + ) -> Result<(), SpecializedMeshPipelineError> { + Ok(()) + } +} + +/// A material that extends a base [`Material`] with additional shaders and data. +/// +/// The data from both materials will be combined and made available to the shader +/// so that shader functions built for the base material (and referencing the base material +/// bindings) will work as expected, and custom alterations based on custom data can also be used. +/// +/// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base +/// material's vertex shader. +/// +/// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base +/// fragment shader. +/// +/// When used with `StandardMaterial` as the base, all the standard material fields are +/// present, so the `pbr_fragment` shader functions can be called from the extension shader (see +/// the `extended_material` example). +#[derive(Asset, Clone, Debug, Reflect)] +#[reflect(type_path = false)] +#[reflect(Clone)] +pub struct ExtendedMaterial { + pub base: B, + pub extension: E, +} + +impl Default for ExtendedMaterial +where + B: Material + Default, + E: MaterialExtension + Default, +{ + fn default() -> Self { + Self { + base: B::default(), + extension: E::default(), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, packed)] +pub struct MaterialExtensionBindGroupData { + pub base: B, + pub extension: E, +} + +// We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` +// causes the `TypePath` derive to not generate an implementation. +impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); + +impl AsBindGroup for ExtendedMaterial { + type Data = MaterialExtensionBindGroupData; + type Param = (::Param, ::Param); + + fn bindless_slot_count() -> Option { + // We only enable bindless if both the base material and its extension + // are bindless. If we do enable bindless, we choose the smaller of the + // two slab size limits. + match (B::bindless_slot_count()?, E::bindless_slot_count()?) { + (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Auto) => { + Some(BindlessSlabResourceLimit::Auto) + } + (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Custom(limit)) + | (BindlessSlabResourceLimit::Custom(limit), BindlessSlabResourceLimit::Auto) => { + Some(BindlessSlabResourceLimit::Custom(limit)) + } + ( + BindlessSlabResourceLimit::Custom(base_limit), + BindlessSlabResourceLimit::Custom(extended_limit), + ) => Some(BindlessSlabResourceLimit::Custom( + base_limit.min(extended_limit), + )), + } + } + + fn bindless_supported(render_device: &RenderDevice) -> bool { + B::bindless_supported(render_device) && E::bindless_supported(render_device) + } + + fn label() -> &'static str { + E::label() + } + + fn bind_group_data(&self) -> Self::Data { + MaterialExtensionBindGroupData { + base: self.base.bind_group_data(), + extension: self.extension.bind_group_data(), + } + } + + fn unprepared_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, + mut force_non_bindless: bool, + ) -> Result { + force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); + + // add together the bindings of the base material and the extension + let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( + &self.base, + layout, + render_device, + base_param, + force_non_bindless, + )?; + let UnpreparedBindGroup { + bindings: extension_bindings, + } = E::unprepared_bind_group( + &self.extension, + layout, + render_device, + extended_param, + force_non_bindless, + )?; + + bindings.extend(extension_bindings.0); + + Ok(UnpreparedBindGroup { bindings }) + } + + fn bind_group_layout_entries( + render_device: &RenderDevice, + mut force_non_bindless: bool, + ) -> Vec + where + Self: Sized, + { + force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); + + // Add together the bindings of the standard material and the user + // material, skipping duplicate bindings. Duplicate bindings will occur + // when bindless mode is on, because of the common bindless resource + // arrays, and we need to eliminate the duplicates or `wgpu` will + // complain. + let base_entries = B::bind_group_layout_entries(render_device, force_non_bindless); + let extension_entries = E::bind_group_layout_entries(render_device, force_non_bindless); + + let mut seen_bindings = HashSet::::with_hasher(FixedHasher); + + base_entries + .into_iter() + .chain(extension_entries) + .filter(|entry| seen_bindings.insert(entry.binding)) + .collect() + } + + fn bindless_descriptor() -> Option { + // We're going to combine the two bindless descriptors. + let base_bindless_descriptor = B::bindless_descriptor()?; + let extended_bindless_descriptor = E::bindless_descriptor()?; + + // Combining the buffers and index tables is straightforward. + + let mut buffers = base_bindless_descriptor.buffers.to_vec(); + let mut index_tables = base_bindless_descriptor.index_tables.to_vec(); + + buffers.extend(extended_bindless_descriptor.buffers.iter().cloned()); + index_tables.extend(extended_bindless_descriptor.index_tables.iter().cloned()); + + // Combining the resources is a little trickier because the resource + // array is indexed by bindless index, so we have to merge the two + // arrays, not just concatenate them. + let max_bindless_index = base_bindless_descriptor + .resources + .len() + .max(extended_bindless_descriptor.resources.len()); + let mut resources = Vec::with_capacity(max_bindless_index); + for bindless_index in 0..max_bindless_index { + // In the event of a conflicting bindless index, we choose the + // base's binding. + match base_bindless_descriptor.resources.get(bindless_index) { + None | Some(&BindlessResourceType::None) => resources.push( + extended_bindless_descriptor + .resources + .get(bindless_index) + .copied() + .unwrap_or(BindlessResourceType::None), + ), + Some(&resource_type) => resources.push(resource_type), + } + } + + Some(BindlessDescriptor { + resources: Cow::Owned(resources), + buffers: Cow::Owned(buffers), + index_tables: Cow::Owned(index_tables), + }) + } +} + +impl Material for ExtendedMaterial { + fn vertex_shader() -> ShaderRef { + match E::vertex_shader() { + ShaderRef::Default => B::vertex_shader(), + specified => specified, + } + } + + fn fragment_shader() -> ShaderRef { + match E::fragment_shader() { + ShaderRef::Default => B::fragment_shader(), + specified => specified, + } + } + + fn alpha_mode(&self) -> AlphaMode { + match E::alpha_mode() { + Some(specified) => specified, + None => B::alpha_mode(&self.base), + } + } + + fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { + B::opaque_render_method(&self.base) + } + + fn depth_bias(&self) -> f32 { + B::depth_bias(&self.base) + } + + fn reads_view_transmission_texture(&self) -> bool { + B::reads_view_transmission_texture(&self.base) + } + + fn enable_prepass() -> bool { + E::enable_prepass() + } + + fn enable_shadows() -> bool { + E::enable_shadows() + } + + fn prepass_vertex_shader() -> ShaderRef { + match E::prepass_vertex_shader() { + ShaderRef::Default => B::prepass_vertex_shader(), + specified => specified, + } + } + + fn prepass_fragment_shader() -> ShaderRef { + match E::prepass_fragment_shader() { + ShaderRef::Default => B::prepass_fragment_shader(), + specified => specified, + } + } + + fn deferred_vertex_shader() -> ShaderRef { + match E::deferred_vertex_shader() { + ShaderRef::Default => B::deferred_vertex_shader(), + specified => specified, + } + } + + fn deferred_fragment_shader() -> ShaderRef { + match E::deferred_fragment_shader() { + ShaderRef::Default => B::deferred_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_prepass_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_prepass_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_deferred_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_deferred_fragment_shader(), + specified => specified, + } + } + + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + // Call the base material's specialize function + let base_key = MaterialPipelineKey:: { + mesh_key: key.mesh_key, + bind_group_data: key.bind_group_data.base, + }; + B::specialize(pipeline, descriptor, layout, base_key)?; + + // Call the extended material's specialize function afterwards + E::specialize( + &MaterialExtensionPipeline { + mesh_pipeline: pipeline.mesh_pipeline.clone(), + }, + descriptor, + layout, + MaterialExtensionKey { + mesh_key: key.mesh_key, + bind_group_data: key.bind_group_data.extension, + }, + ) + } +} diff --git a/vendor/bevy_pbr/src/fog.rs b/vendor/bevy_pbr/src/fog.rs new file mode 100644 index 00000000..8ee6a62d --- /dev/null +++ b/vendor/bevy_pbr/src/fog.rs @@ -0,0 +1,477 @@ +use bevy_camera::Camera; +use bevy_color::{Color, ColorToComponents, LinearRgba}; +use bevy_ecs::prelude::*; +use bevy_math::{ops, Vec3}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::extract_component::ExtractComponent; + +/// Configures the “classic” computer graphics [distance fog](https://en.wikipedia.org/wiki/Distance_fog) effect, +/// in which objects appear progressively more covered in atmospheric haze the further away they are from the camera. +/// Affects meshes rendered via the PBR [`StandardMaterial`](crate::StandardMaterial). +/// +/// ## Falloff +/// +/// The rate at which fog intensity increases with distance is controlled by the falloff mode. +/// Currently, the following fog falloff modes are supported: +/// +/// - [`FogFalloff::Linear`] +/// - [`FogFalloff::Exponential`] +/// - [`FogFalloff::ExponentialSquared`] +/// - [`FogFalloff::Atmospheric`] +/// +/// ## Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::prelude::*; +/// # use bevy_camera::prelude::*; +/// # use bevy_pbr::prelude::*; +/// # use bevy_color::Color; +/// # fn system(mut commands: Commands) { +/// commands.spawn(( +/// // Setup your camera as usual +/// Camera3d::default(), +/// // Add fog to the same entity +/// DistanceFog { +/// color: Color::WHITE, +/// falloff: FogFalloff::Exponential { density: 1e-3 }, +/// ..Default::default() +/// }, +/// )); +/// # } +/// # bevy_ecs::system::assert_is_system(system); +/// ``` +/// +/// ## Material Override +/// +/// Once enabled for a specific camera, the fog effect can also be disabled for individual +/// [`StandardMaterial`](crate::StandardMaterial) instances via the `fog_enabled` flag. +#[derive(Debug, Clone, Component, Reflect, ExtractComponent)] +#[extract_component_filter(With)] +#[reflect(Component, Default, Debug, Clone)] +pub struct DistanceFog { + /// The color of the fog effect. + /// + /// **Tip:** The alpha channel of the color can be used to “modulate” the fog effect without + /// changing the fog falloff mode or parameters. + pub color: Color, + + /// Color used to modulate the influence of directional light colors on the + /// fog, where the view direction aligns with each directional light direction, + /// producing a “glow” or light dispersion effect. (e.g. around the sun) + /// + /// Use [`Color::NONE`] to disable the effect. + pub directional_light_color: Color, + + /// The exponent applied to the directional light alignment calculation. + /// A higher value means a more concentrated “glow”. + pub directional_light_exponent: f32, + + /// Determines which falloff mode to use, and its parameters. + pub falloff: FogFalloff, +} + +/// Allows switching between different fog falloff modes, and configuring their parameters. +/// +/// ## Convenience Methods +/// +/// When using non-linear fog modes it can be hard to determine the right parameter values +/// for a given scene. +/// +/// For easier artistic control, instead of creating the enum variants directly, you can use the +/// visibility-based convenience methods: +/// +/// - For `FogFalloff::Exponential`: +/// - [`FogFalloff::from_visibility()`] +/// - [`FogFalloff::from_visibility_contrast()`] +/// +/// - For `FogFalloff::ExponentialSquared`: +/// - [`FogFalloff::from_visibility_squared()`] +/// - [`FogFalloff::from_visibility_contrast_squared()`] +/// +/// - For `FogFalloff::Atmospheric`: +/// - [`FogFalloff::from_visibility_color()`] +/// - [`FogFalloff::from_visibility_colors()`] +/// - [`FogFalloff::from_visibility_contrast_color()`] +/// - [`FogFalloff::from_visibility_contrast_colors()`] +#[derive(Debug, Clone, Reflect)] +#[reflect(Clone)] +pub enum FogFalloff { + /// A linear fog falloff that grows in intensity between `start` and `end` distances. + /// + /// This falloff mode is simpler to control than other modes, however it can produce results that look “artificial”, depending on the scene. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - ((end - distance) / (end - start)).clamp(0.0, 1.0); + /// ``` + /// + /// + /// Plot showing how linear fog falloff behaves for start and end values of 0.8 and 2.2, respectively. + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// distance + /// fog intensity + /// + /// + /// + /// start + /// end + /// + Linear { + /// Distance from the camera where fog is completely transparent, in world units. + start: f32, + + /// Distance from the camera where fog is completely opaque, in world units. + end: f32, + }, + + /// An exponential fog falloff with a given `density`. + /// + /// Initially gains intensity quickly with distance, then more slowly. Typically produces more natural results than [`FogFalloff::Linear`], + /// but is a bit harder to control. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility()`] convenience method to create an exponential falloff with the proper + /// density for a desired visibility distance in world units; + /// - It's not _unusual_ to have very large or very small values for the density, depending on the scene + /// scale. Typically, for scenes with objects in the scale of thousands of units, you might want density values + /// in the ballpark of `0.001`. Conversely, for really small scale scenes you might want really high values of + /// density; + /// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).exp(); + /// ``` + /// + /// + /// Plot showing how exponential fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + Exponential { + /// Multiplier applied to the world distance (within the exponential fog falloff calculation). + density: f32, + }, + + /// A squared exponential fog falloff with a given `density`. + /// + /// Similar to [`FogFalloff::Exponential`], but grows more slowly in intensity for closer distances + /// before “catching up”. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_squared()`] convenience method to create an exponential squared falloff + /// with the proper density for a desired visibility distance in world units; + /// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).squared().exp(); + /// ``` + /// + /// + /// Plot showing how exponential squared fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + ExponentialSquared { + /// Multiplier applied to the world distance (within the exponential squared fog falloff calculation). + density: f32, + }, + + /// A more general form of the [`FogFalloff::Exponential`] mode. The falloff formula is separated into + /// two terms, `extinction` and `inscattering`, for a somewhat simplified atmospheric scattering model. + /// Additionally, individual color channels can have their own density values, resulting in a total of + /// six different configuration parameters. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_colors()`] or [`FogFalloff::from_visibility_color()`] convenience methods + /// to create an atmospheric falloff with the proper densities for a desired visibility distance in world units and + /// extinction and inscattering colors; + /// - Combine the atmospheric fog parameters with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// Unlike other modes, atmospheric falloff doesn't use a simple intensity-based blend of fog color with + /// object color. Instead, it calculates per-channel extinction and inscattering factors, which are + /// then used to calculate the final color. + /// + /// ```text + /// let extinction_factor = 1.0 - 1.0 / (distance * extinction).exp(); + /// let inscattering_factor = 1.0 - 1.0 / (distance * inscattering).exp(); + /// let result = input_color * (1.0 - extinction_factor) + fog_color * inscattering_factor; + /// ``` + /// + /// ## Equivalence to [`FogFalloff::Exponential`] + /// + /// For a density value of `D`, the following two falloff modes will produce identical visual results: + /// + /// ``` + /// # use bevy_pbr::prelude::*; + /// # use bevy_math::prelude::*; + /// # const D: f32 = 0.5; + /// # + /// let exponential = FogFalloff::Exponential { + /// density: D, + /// }; + /// + /// let atmospheric = FogFalloff::Atmospheric { + /// extinction: Vec3::new(D, D, D), + /// inscattering: Vec3::new(D, D, D), + /// }; + /// ``` + /// + /// **Note:** While the results are identical, [`FogFalloff::Atmospheric`] is computationally more expensive. + Atmospheric { + /// Controls how much light is removed due to atmospheric “extinction”, i.e. loss of light due to + /// photons being absorbed by atmospheric particles. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: Multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + extinction: Vec3, + + /// Controls how much light is added due to light scattering from the sun through the atmosphere. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: A multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + inscattering: Vec3, + }, +} + +impl FogFalloff { + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast(visibility: f32, contrast_threshold: f32) -> FogFalloff { + FogFalloff::Exponential { + density: FogFalloff::koschmieder(visibility, contrast_threshold), + } + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_squared(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast_squared( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast_squared( + visibility: f32, + contrast_threshold: f32, + ) -> FogFalloff { + FogFalloff::ExponentialSquared { + density: (FogFalloff::koschmieder(visibility, contrast_threshold) / visibility).sqrt(), + } + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// and a shared color for both extinction and inscattering, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_color( + visibility: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// extinction and inscattering colors, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_colors( + visibility: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_color, + inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, and a shared color for both extinction and inscattering. + pub fn from_visibility_contrast_color( + visibility: f32, + contrast_threshold: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + contrast_threshold, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, extinction and inscattering colors. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_contrast_colors( + visibility: f32, + contrast_threshold: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + use core::f32::consts::E; + + let [r_e, g_e, b_e, a_e] = LinearRgba::from(extinction_color).to_f32_array(); + let [r_i, g_i, b_i, a_i] = LinearRgba::from(inscattering_color).to_f32_array(); + + FogFalloff::Atmospheric { + extinction: Vec3::new( + // Values are subtracted from 1.0 here to preserve the intuitive/artistic meaning of + // colors, since they're later subtracted. (e.g. by giving a blue extinction color, you + // get blue and _not_ yellow results) + ops::powf(1.0 - r_e, E), + ops::powf(1.0 - g_e, E), + ops::powf(1.0 - b_e, E), + ) * FogFalloff::koschmieder(visibility, contrast_threshold) + * ops::powf(a_e, E), + + inscattering: Vec3::new(ops::powf(r_i, E), ops::powf(g_i, E), ops::powf(b_i, E)) + * FogFalloff::koschmieder(visibility, contrast_threshold) + * ops::powf(a_i, E), + } + } + + /// A 2% contrast threshold was originally proposed by Koschmieder, being the + /// minimum visual contrast at which a human observer could detect an object. + /// We use a revised 5% contrast threshold, deemed more realistic for typical human observers. + pub const REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD: f32 = 0.05; + + /// Calculates the extinction coefficient β, from V and Cₜ, where: + /// + /// - Cₜ is the contrast threshold, in the range of `0.0` to `1.0` + /// - V is the visibility distance in which a perfectly black object is still identifiable + /// against the horizon sky within the contrast threshold + /// + /// We start with Koschmieder's equation: + /// + /// ```text + /// -ln(Cₜ) + /// V = ───────── + /// β + /// ``` + /// + /// Multiplying both sides by β/V, that gives us: + /// + /// ```text + /// -ln(Cₜ) + /// β = ───────── + /// V + /// ``` + /// + /// See: + /// - + /// - + pub fn koschmieder(v: f32, c_t: f32) -> f32 { + -ops::ln(c_t) / v + } +} + +impl Default for DistanceFog { + fn default() -> Self { + DistanceFog { + color: Color::WHITE, + falloff: FogFalloff::Linear { + start: 0.0, + end: 100.0, + }, + directional_light_color: Color::NONE, + directional_light_exponent: 8.0, + } + } +} diff --git a/vendor/bevy_pbr/src/lib.rs b/vendor/bevy_pbr/src/lib.rs new file mode 100644 index 00000000..79163cb6 --- /dev/null +++ b/vendor/bevy_pbr/src/lib.rs @@ -0,0 +1,395 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] + +extern crate alloc; + +#[cfg(feature = "meshlet")] +mod meshlet; +pub mod wireframe; + +/// Experimental features that are not yet finished. Please report any issues you encounter! +/// +/// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. +#[cfg(feature = "meshlet")] +pub mod experimental { + /// Render high-poly 3d meshes using an efficient GPU-driven method. + /// See [`MeshletPlugin`](meshlet::MeshletPlugin) and [`MeshletMesh`](meshlet::MeshletMesh) for details. + pub mod meshlet { + pub use crate::meshlet::*; + } +} + +mod atmosphere; +mod cluster; +mod components; +pub mod decal; +pub mod deferred; +pub mod diagnostic; +mod extended_material; +mod fog; +mod light_probe; +mod lightmap; +mod material; +mod material_bind_groups; +mod medium; +mod mesh_material; +mod parallax; +mod pbr_material; +mod prepass; +mod render; +mod ssao; +mod ssr; +mod volumetric_fog; + +use bevy_color::{Color, LinearRgba}; + +pub use atmosphere::*; +use bevy_light::{ + AmbientLight, DirectionalLight, PointLight, ShadowFilteringMethod, SimulationLightSystems, + SpotLight, +}; +use bevy_shader::{load_shader_library, ShaderRef}; +pub use cluster::*; +pub use components::*; +pub use decal::clustered::ClusteredDecalPlugin; +pub use extended_material::*; +pub use fog::*; +pub use light_probe::*; +pub use lightmap::*; +pub use material::*; +pub use material_bind_groups::*; +pub use medium::*; +pub use mesh_material::*; +pub use parallax::*; +pub use pbr_material::*; +pub use prepass::*; +pub use render::*; +pub use ssao::*; +pub use ssr::*; +pub use volumetric_fog::VolumetricFogPlugin; + +/// The PBR prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::{ + fog::{DistanceFog, FogFalloff}, + material::{Material, MaterialPlugin}, + mesh_material::MeshMaterial3d, + parallax::ParallaxMappingMethod, + pbr_material::StandardMaterial, + ssao::ScreenSpaceAmbientOcclusionPlugin, + }; +} + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + /// Render graph nodes specific to 3D PBR rendering. + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum NodePbr { + /// Label for the shadow pass node that draws meshes that were visible + /// from the light last frame. + EarlyShadowPass, + /// Label for the shadow pass node that draws meshes that became visible + /// from the light this frame. + LateShadowPass, + /// Label for the screen space ambient occlusion render node. + ScreenSpaceAmbientOcclusion, + DeferredLightingPass, + /// Label for the volumetric lighting pass. + VolumetricFog, + /// Label for the shader that transforms and culls meshes that were + /// visible last frame. + EarlyGpuPreprocess, + /// Label for the shader that transforms and culls meshes that became + /// visible this frame. + LateGpuPreprocess, + /// Label for the screen space reflections pass. + ScreenSpaceReflections, + /// Label for the node that builds indirect draw parameters for meshes + /// that were visible last frame. + EarlyPrepassBuildIndirectParameters, + /// Label for the node that builds indirect draw parameters for meshes + /// that became visible this frame. + LatePrepassBuildIndirectParameters, + /// Label for the node that builds indirect draw parameters for the main + /// rendering pass, containing all meshes that are visible this frame. + MainBuildIndirectParameters, + ClearIndirectParametersMetadata, + } +} + +use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; +use bevy_app::prelude::*; +use bevy_asset::{AssetApp, AssetPath, Assets, Handle, RenderAssetUsages}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::prelude::*; +#[cfg(feature = "bluenoise_texture")] +use bevy_image::{CompressedImageFormats, ImageType}; +use bevy_image::{Image, ImageSampler}; +use bevy_render::{ + alpha::AlphaMode, + camera::sort_cameras, + extract_resource::ExtractResourcePlugin, + render_graph::RenderGraph, + render_resource::{ + Extent3d, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, + }, + sync_component::SyncComponentPlugin, + ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; + +use std::path::PathBuf; + +fn shader_ref(path: PathBuf) -> ShaderRef { + ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) +} + +pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 18; +pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 19; + +/// Sets up the entire PBR infrastructure of bevy. +pub struct PbrPlugin { + /// Controls if the prepass is enabled for the [`StandardMaterial`]. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + pub prepass_enabled: bool, + /// Controls if [`DeferredPbrLightingPlugin`] is added. + pub add_default_deferred_lighting_plugin: bool, + /// Controls if GPU [`MeshUniform`] building is enabled. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Default for PbrPlugin { + fn default() -> Self { + Self { + prepass_enabled: true, + add_default_deferred_lighting_plugin: true, + use_gpu_instance_buffer_builder: true, + debug_flags: RenderDebugFlags::default(), + } + } +} + +/// A resource that stores the spatio-temporal blue noise texture. +#[derive(Resource)] +pub struct Bluenoise { + /// Texture handle for spatio-temporal blue noise + pub texture: Handle, +} + +impl Plugin for PbrPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "render/pbr_types.wgsl"); + load_shader_library!(app, "render/pbr_bindings.wgsl"); + load_shader_library!(app, "render/utils.wgsl"); + load_shader_library!(app, "render/clustered_forward.wgsl"); + load_shader_library!(app, "render/pbr_lighting.wgsl"); + load_shader_library!(app, "render/pbr_transmission.wgsl"); + load_shader_library!(app, "render/shadows.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_types.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_functions.wgsl"); + load_shader_library!(app, "render/shadow_sampling.wgsl"); + load_shader_library!(app, "render/pbr_functions.wgsl"); + load_shader_library!(app, "render/rgb9e5.wgsl"); + load_shader_library!(app, "render/pbr_ambient.wgsl"); + load_shader_library!(app, "render/pbr_fragment.wgsl"); + load_shader_library!(app, "render/pbr.wgsl"); + load_shader_library!(app, "render/pbr_prepass_functions.wgsl"); + load_shader_library!(app, "render/pbr_prepass.wgsl"); + load_shader_library!(app, "render/parallax_mapping.wgsl"); + load_shader_library!(app, "render/view_transformations.wgsl"); + + // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. + load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); + + app.register_asset_reflect::() + .init_resource::() + .add_plugins(( + MeshRenderPlugin { + use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, + debug_flags: self.debug_flags, + }, + MaterialsPlugin { + debug_flags: self.debug_flags, + }, + MaterialPlugin:: { + debug_flags: self.debug_flags, + ..Default::default() + }, + ScreenSpaceAmbientOcclusionPlugin, + FogPlugin, + ExtractResourcePlugin::::default(), + SyncComponentPlugin::::default(), + LightmapPlugin, + LightProbePlugin, + GpuMeshPreprocessPlugin { + use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, + }, + VolumetricFogPlugin, + ScreenSpaceReflectionsPlugin, + ClusteredDecalPlugin, + )) + .add_plugins(( + decal::ForwardDecalPlugin, + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + )) + .add_plugins((ScatteringMediumPlugin, AtmospherePlugin)) + .configure_sets( + PostUpdate, + ( + SimulationLightSystems::AddClusters, + SimulationLightSystems::AssignLightsToClusters, + ) + .chain(), + ); + + if self.add_default_deferred_lighting_plugin { + app.add_plugins(DeferredPbrLightingPlugin); + } + + // Initialize the default material handle. + app.world_mut() + .resource_mut::>() + .insert( + &Handle::::default(), + StandardMaterial { + base_color: Color::srgb(1.0, 0.0, 0.5), + ..Default::default() + }, + ) + .unwrap(); + + let has_bluenoise = app + .get_sub_app(RenderApp) + .is_some_and(|render_app| render_app.world().is_resource_added::()); + + if !has_bluenoise { + let mut images = app.world_mut().resource_mut::>(); + #[cfg(feature = "bluenoise_texture")] + let handle = { + let image = Image::from_buffer( + include_bytes!("bluenoise/stbn.ktx2"), + ImageType::Extension("ktx2"), + CompressedImageFormats::NONE, + false, + ImageSampler::Default, + RenderAssetUsages::RENDER_WORLD, + ) + .expect("Failed to decode embedded blue-noise texture"); + images.add(image) + }; + + #[cfg(not(feature = "bluenoise_texture"))] + let handle = { images.add(stbn_placeholder()) }; + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .world_mut() + .insert_resource(Bluenoise { texture: handle }); + } + } + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Extract the required data from the main world + render_app + .add_systems( + RenderStartup, + ( + init_shadow_samplers, + init_global_clusterable_object_meta, + init_fallback_bindless_resources, + ), + ) + .add_systems( + ExtractSchedule, + ( + extract_clusters, + extract_lights, + extract_ambient_light_resource, + extract_ambient_light, + extract_shadow_filtering_method, + late_sweep_material_instances, + ), + ) + .add_systems( + Render, + ( + prepare_lights + .in_set(RenderSystems::ManageViews) + .after(sort_cameras), + prepare_clusters.in_set(RenderSystems::PrepareResources), + ), + ) + .init_resource::() + .init_resource::(); + + render_app.world_mut().add_observer(add_light_view_entities); + render_app + .world_mut() + .add_observer(remove_light_view_entities); + render_app.world_mut().add_observer(extracted_light_removed); + + let early_shadow_pass_node = EarlyShadowPassNode::from_world(render_app.world_mut()); + let late_shadow_pass_node = LateShadowPassNode::from_world(render_app.world_mut()); + let mut graph = render_app.world_mut().resource_mut::(); + let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap(); + draw_3d_graph.add_node(NodePbr::EarlyShadowPass, early_shadow_pass_node); + draw_3d_graph.add_node(NodePbr::LateShadowPass, late_shadow_pass_node); + draw_3d_graph.add_node_edges(( + NodePbr::EarlyShadowPass, + NodePbr::LateShadowPass, + Node3d::StartMainPass, + )); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let global_cluster_settings = make_global_cluster_settings(render_app.world()); + app.insert_resource(global_cluster_settings); + } +} + +pub fn stbn_placeholder() -> Image { + let format = TextureFormat::Rgba8Unorm; + let data = vec![255, 0, 255, 255]; + Image { + data: Some(data), + data_order: TextureDataOrder::default(), + texture_descriptor: TextureDescriptor { + size: Extent3d::default(), + format, + dimension: TextureDimension::D2, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + sampler: ImageSampler::Default, + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::RENDER_WORLD, + copy_on_resize: false, + } +} diff --git a/vendor/bevy_pbr/src/light_probe/copy.wgsl b/vendor/bevy_pbr/src/light_probe/copy.wgsl new file mode 100644 index 00000000..91405622 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/copy.wgsl @@ -0,0 +1,21 @@ +// Copy the base mip (level 0) from a source cubemap to a destination cubemap, +// performing format conversion if needed (the destination is always rgba16float). +// The alpha channel is filled with 1.0. + +@group(0) @binding(0) var src_cubemap: texture_2d_array; +@group(0) @binding(1) var dst_cubemap: texture_storage_2d_array; + +@compute +@workgroup_size(8, 8, 1) +fn copy(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(src_cubemap).xy; + + // Bounds check + if (any(global_id.xy >= size)) { + return; + } + + let color = textureLoad(src_cubemap, vec2u(global_id.xy), global_id.z, 0); + + textureStore(dst_cubemap, vec2u(global_id.xy), global_id.z, vec4f(color.rgb, 1.0)); +} \ No newline at end of file diff --git a/vendor/bevy_pbr/src/light_probe/downsample.wgsl b/vendor/bevy_pbr/src/light_probe/downsample.wgsl new file mode 100644 index 00000000..e28c9b2f --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/downsample.wgsl @@ -0,0 +1,439 @@ +// Single pass downsampling shader for creating the mip chain for an array texture +// Ported from https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/c16b1d286b5b438b75da159ab51ff426bacea3d1/sdk/include/FidelityFX/gpu/spd/ffx_spd.h + +@group(0) @binding(0) var sampler_linear_clamp: sampler; +@group(0) @binding(1) var constants: Constants; +#ifdef COMBINE_BIND_GROUP +@group(0) @binding(2) var mip_0: texture_2d_array; +@group(0) @binding(3) var mip_1: texture_storage_2d_array; +@group(0) @binding(4) var mip_2: texture_storage_2d_array; +@group(0) @binding(5) var mip_3: texture_storage_2d_array; +@group(0) @binding(6) var mip_4: texture_storage_2d_array; +@group(0) @binding(7) var mip_5: texture_storage_2d_array; +@group(0) @binding(8) var mip_6: texture_storage_2d_array; +@group(0) @binding(9) var mip_7: texture_storage_2d_array; +@group(0) @binding(10) var mip_8: texture_storage_2d_array; +@group(0) @binding(11) var mip_9: texture_storage_2d_array; +@group(0) @binding(12) var mip_10: texture_storage_2d_array; +@group(0) @binding(13) var mip_11: texture_storage_2d_array; +@group(0) @binding(14) var mip_12: texture_storage_2d_array; +#endif + +#ifdef FIRST_PASS +@group(0) @binding(2) var mip_0: texture_2d_array; +@group(0) @binding(3) var mip_1: texture_storage_2d_array; +@group(0) @binding(4) var mip_2: texture_storage_2d_array; +@group(0) @binding(5) var mip_3: texture_storage_2d_array; +@group(0) @binding(6) var mip_4: texture_storage_2d_array; +@group(0) @binding(7) var mip_5: texture_storage_2d_array; +@group(0) @binding(8) var mip_6: texture_storage_2d_array; +#endif + +#ifdef SECOND_PASS +@group(0) @binding(2) var mip_6: texture_2d_array; +@group(0) @binding(3) var mip_7: texture_storage_2d_array; +@group(0) @binding(4) var mip_8: texture_storage_2d_array; +@group(0) @binding(5) var mip_9: texture_storage_2d_array; +@group(0) @binding(6) var mip_10: texture_storage_2d_array; +@group(0) @binding(7) var mip_11: texture_storage_2d_array; +@group(0) @binding(8) var mip_12: texture_storage_2d_array; +#endif + +struct Constants { mips: u32, inverse_input_size: vec2f } + +var spd_intermediate_r: array, 16>; +var spd_intermediate_g: array, 16>; +var spd_intermediate_b: array, 16>; +var spd_intermediate_a: array, 16>; + +@compute +@workgroup_size(256, 1, 1) +fn downsample_first( + @builtin(workgroup_id) workgroup_id: vec3u, + @builtin(local_invocation_index) local_invocation_index: u32 +) { + + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + spd_downsample_mips_0_1(x, y, workgroup_id.xy, local_invocation_index, constants.mips, workgroup_id.z); + + spd_downsample_next_four(x, y, workgroup_id.xy, local_invocation_index, 2u, constants.mips, workgroup_id.z); +} + +// TODO: Once wgpu supports globallycoherent buffers, make it actually a single pass +@compute +@workgroup_size(256, 1, 1) +fn downsample_second( + @builtin(workgroup_id) workgroup_id: vec3u, + @builtin(local_invocation_index) local_invocation_index: u32, +) { + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + spd_downsample_mips_6_7(x, y, constants.mips, workgroup_id.z); + + spd_downsample_next_four(x, y, vec2(0u), local_invocation_index, 8u, constants.mips, workgroup_id.z); +} + +fn spd_downsample_mips_0_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, mips: u32, slice: u32) { + var v: array; + + var tex = (workgroup_id * 64u) + vec2(x * 2u, y * 2u); + var pix = (workgroup_id * 32u) + vec2(x, y); + v[0] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[0], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u); + pix = (workgroup_id * 32u) + vec2(x + 16u, y); + v[1] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[1], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u, y * 2u + 32u); + pix = (workgroup_id * 32u) + vec2(x, y + 16u); + v[2] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[2], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u + 32u); + pix = (workgroup_id * 32u) + vec2(x + 16u, y + 16u); + v[3] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[3], 0u, slice); + + if mips <= 1u { return; } + +#ifdef SUBGROUP_SUPPORT + v[0] = spd_reduce_quad(v[0]); + v[1] = spd_reduce_quad(v[1]); + v[2] = spd_reduce_quad(v[2]); + v[3] = spd_reduce_quad(v[3]); + + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 16u) + vec2(x / 2u, y / 2u), v[0], 1u, slice); + spd_store_intermediate(x / 2u, y / 2u, v[0]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u + 8u, y / 2u), v[1], 1u, slice); + spd_store_intermediate(x / 2u + 8u, y / 2u, v[1]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u, y / 2u + 8u), v[2], 1u, slice); + spd_store_intermediate(x / 2u, y / 2u + 8u, v[2]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u + 8u, y / 2u + 8u), v[3], 1u, slice); + spd_store_intermediate(x / 2u + 8u, y / 2u + 8u, v[3]); + } +#else + for (var i = 0u; i < 4u; i++) { + spd_store_intermediate(x, y, v[i]); + workgroupBarrier(); + if local_invocation_index < 64u { + v[i] = spd_reduce_intermediate( + vec2(x * 2u + 0u, y * 2u + 0u), + vec2(x * 2u + 1u, y * 2u + 0u), + vec2(x * 2u + 0u, y * 2u + 1u), + vec2(x * 2u + 1u, y * 2u + 1u), + ); + spd_store(vec2(workgroup_id * 16) + vec2(x + (i % 2u) * 8u, y + (i / 2u) * 8u), v[i], 1u, slice); + } + workgroupBarrier(); + } + + if local_invocation_index < 64u { + spd_store_intermediate(x + 0u, y + 0u, v[0]); + spd_store_intermediate(x + 8u, y + 0u, v[1]); + spd_store_intermediate(x + 0u, y + 8u, v[2]); + spd_store_intermediate(x + 8u, y + 8u, v[3]); + } +#endif +} + +fn spd_downsample_next_four(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, mips: u32, slice: u32) { + if mips <= base_mip { return; } + workgroupBarrier(); + spd_downsample_mip_2(x, y, workgroup_id, local_invocation_index, base_mip, slice); + + if mips <= base_mip + 1u { return; } + workgroupBarrier(); + spd_downsample_mip_3(x, y, workgroup_id, local_invocation_index, base_mip + 1u, slice); + + if mips <= base_mip + 2u { return; } + workgroupBarrier(); + spd_downsample_mip_4(x, y, workgroup_id, local_invocation_index, base_mip + 2u, slice); + + if mips <= base_mip + 3u { return; } + workgroupBarrier(); + spd_downsample_mip_5(x, y, workgroup_id, local_invocation_index, base_mip + 3u, slice); +} + +fn spd_downsample_mip_2(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + var v = spd_load_intermediate(x, y); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 8u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x + (y / 2u) % 2u, y, v); + } +#else + if local_invocation_index < 64u { + let v = spd_reduce_intermediate( + vec2(x * 2u + 0u, y * 2u + 0u), + vec2(x * 2u + 1u, y * 2u + 0u), + vec2(x * 2u + 0u, y * 2u + 1u), + vec2(x * 2u + 1u, y * 2u + 1u), + ); + spd_store((workgroup_id * 8u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x * 2u + y % 2u, y * 2u, v); + } +#endif +} + +fn spd_downsample_mip_3(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 64u { + var v = spd_load_intermediate(x * 2u + y % 2u, y * 2u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 4u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x * 2u + y / 2u, y * 2u, v); + } + } +#else + if local_invocation_index < 16u { + let v = spd_reduce_intermediate( + vec2(x * 4u + 0u + 0u, y * 4u + 0u), + vec2(x * 4u + 2u + 0u, y * 4u + 0u), + vec2(x * 4u + 0u + 1u, y * 4u + 2u), + vec2(x * 4u + 2u + 1u, y * 4u + 2u), + ); + spd_store((workgroup_id * 4u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x * 4u + y, y * 4u, v); + } +#endif +} + +fn spd_downsample_mip_4(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 16u { + var v = spd_load_intermediate(x * 4u + y, y * 4u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 2u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x / 2u + y, 0u, v); + } + } +#else + if local_invocation_index < 4u { + let v = spd_reduce_intermediate( + vec2(x * 8u + 0u + 0u + y * 2u, y * 8u + 0u), + vec2(x * 8u + 4u + 0u + y * 2u, y * 8u + 0u), + vec2(x * 8u + 0u + 1u + y * 2u, y * 8u + 4u), + vec2(x * 8u + 4u + 1u + y * 2u, y * 8u + 4u), + ); + spd_store((workgroup_id * 2u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x + y * 2u, 0u, v); + } +#endif +} + +fn spd_downsample_mip_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 4u { + var v = spd_load_intermediate(local_invocation_index, 0u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store(workgroup_id, v, base_mip, slice); + } + } +#else + if local_invocation_index < 1u { + let v = spd_reduce_intermediate(vec2(0u, 0u), vec2(1u, 0u), vec2(2u, 0u), vec2(3u, 0u)); + spd_store(workgroup_id, v, base_mip, slice); + } +#endif +} + +fn spd_downsample_mips_6_7(x: u32, y: u32, mips: u32, slice: u32) { + var tex = vec2(x * 4u + 0u, y * 4u + 0u); + var pix = vec2(x * 2u + 0u, y * 2u + 0u); + let v0 = spd_reduce_load_4( + vec2(x * 4u + 0u, y * 4u + 0u), + vec2(x * 4u + 1u, y * 4u + 0u), + vec2(x * 4u + 0u, y * 4u + 1u), + vec2(x * 4u + 1u, y * 4u + 1u), + slice + ); + spd_store(pix, v0, 6u, slice); + + tex = vec2(x * 4u + 2u, y * 4u + 0u); + pix = vec2(x * 2u + 1u, y * 2u + 0u); + let v1 = spd_reduce_load_4( + vec2(x * 4u + 2u, y * 4u + 0u), + vec2(x * 4u + 3u, y * 4u + 0u), + vec2(x * 4u + 2u, y * 4u + 1u), + vec2(x * 4u + 3u, y * 4u + 1u), + slice + ); + spd_store(pix, v1, 6u, slice); + + tex = vec2(x * 4u + 0u, y * 4u + 2u); + pix = vec2(x * 2u + 0u, y * 2u + 1u); + let v2 = spd_reduce_load_4( + vec2(x * 4u + 0u, y * 4u + 2u), + vec2(x * 4u + 1u, y * 4u + 2u), + vec2(x * 4u + 0u, y * 4u + 3u), + vec2(x * 4u + 1u, y * 4u + 3u), + slice + ); + spd_store(pix, v2, 6u, slice); + + tex = vec2(x * 4u + 2u, y * 4u + 2u); + pix = vec2(x * 2u + 1u, y * 2u + 1u); + let v3 = spd_reduce_load_4( + vec2(x * 4u + 2u, y * 4u + 2u), + vec2(x * 4u + 3u, y * 4u + 2u), + vec2(x * 4u + 2u, y * 4u + 3u), + vec2(x * 4u + 3u, y * 4u + 3u), + slice + ); + spd_store(pix, v3, 6u, slice); + + if mips < 7u { return; } + + let v = spd_reduce_4(v0, v1, v2, v3); + spd_store(vec2(x, y), v, 7u, slice); + spd_store_intermediate(x, y, v); +} + +fn remap_for_wave_reduction(a: u32) -> vec2u { + // This function maps linear thread IDs to 2D coordinates in a special pattern + // to ensure that neighboring threads process neighboring pixels + // For example, this transforms linear thread IDs 0,1,2,3 into a 2×2 square + + // Extract bits to form the X and Y coordinates + let x = insertBits(extractBits(a, 2u, 3u), a, 0u, 1u); + let y = insertBits(extractBits(a, 3u, 3u), extractBits(a, 1u, 2u), 0u, 2u); + + return vec2u(x, y); +} + +fn spd_reduce_load_source_image(uv: vec2u, slice: u32) -> vec4f { + let texture_coord = (vec2f(uv) + 0.5) * constants.inverse_input_size; + + #ifdef COMBINE_BIND_GROUP + let result = textureSampleLevel(mip_0, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + #ifdef FIRST_PASS + let result = textureSampleLevel(mip_0, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + #ifdef SECOND_PASS + let result = textureSampleLevel(mip_6, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + +#ifdef SRGB_CONVERSION + return vec4( + srgb_from_linear(result.r), + srgb_from_linear(result.g), + srgb_from_linear(result.b), + result.a + ); +#else + return result; +#endif + +} + +fn spd_store(pix: vec2u, value: vec4f, mip: u32, slice: u32) { + if mip >= constants.mips { return; } + switch mip { + #ifdef COMBINE_BIND_GROUP + case 0u: { textureStore(mip_1, pix, slice, value); } + case 1u: { textureStore(mip_2, pix, slice, value); } + case 2u: { textureStore(mip_3, pix, slice, value); } + case 3u: { textureStore(mip_4, pix, slice, value); } + case 4u: { textureStore(mip_5, pix, slice, value); } + case 5u: { textureStore(mip_6, pix, slice, value); } + case 6u: { textureStore(mip_7, pix, slice, value); } + case 7u: { textureStore(mip_8, pix, slice, value); } + case 8u: { textureStore(mip_9, pix, slice, value); } + case 9u: { textureStore(mip_10, pix, slice, value); } + case 10u: { textureStore(mip_11, pix, slice, value); } + case 11u: { textureStore(mip_12, pix, slice, value); } + #endif + #ifdef FIRST_PASS + case 0u: { textureStore(mip_1, pix, slice, value); } + case 1u: { textureStore(mip_2, pix, slice, value); } + case 2u: { textureStore(mip_3, pix, slice, value); } + case 3u: { textureStore(mip_4, pix, slice, value); } + case 4u: { textureStore(mip_5, pix, slice, value); } + case 5u: { textureStore(mip_6, pix, slice, value); } + #endif + #ifdef SECOND_PASS + case 6u: { textureStore(mip_7, pix, slice, value); } + case 7u: { textureStore(mip_8, pix, slice, value); } + case 8u: { textureStore(mip_9, pix, slice, value); } + case 9u: { textureStore(mip_10, pix, slice, value); } + case 10u: { textureStore(mip_11, pix, slice, value); } + case 11u: { textureStore(mip_12, pix, slice, value); } + #endif + default: {} + } +} + +fn spd_store_intermediate(x: u32, y: u32, value: vec4f) { + spd_intermediate_r[x][y] = value.x; + spd_intermediate_g[x][y] = value.y; + spd_intermediate_b[x][y] = value.z; + spd_intermediate_a[x][y] = value.w; +} + +fn spd_load_intermediate(x: u32, y: u32) -> vec4f { + return vec4(spd_intermediate_r[x][y], spd_intermediate_g[x][y], spd_intermediate_b[x][y], spd_intermediate_a[x][y]); +} + +fn spd_reduce_intermediate(i0: vec2u, i1: vec2u, i2: vec2u, i3: vec2u) -> vec4f { + let v0 = spd_load_intermediate(i0.x, i0.y); + let v1 = spd_load_intermediate(i1.x, i1.y); + let v2 = spd_load_intermediate(i2.x, i2.y); + let v3 = spd_load_intermediate(i3.x, i3.y); + return spd_reduce_4(v0, v1, v2, v3); +} + +fn spd_reduce_load_4(i0: vec2u, i1: vec2u, i2: vec2u, i3: vec2u, slice: u32) -> vec4f { + #ifdef COMBINE_BIND_GROUP + let v0 = textureLoad(mip_6, i0, slice); + let v1 = textureLoad(mip_6, i1, slice); + let v2 = textureLoad(mip_6, i2, slice); + let v3 = textureLoad(mip_6, i3, slice); + return spd_reduce_4(v0, v1, v2, v3); + #endif + #ifdef FIRST_PASS + return vec4(0.0, 0.0, 0.0, 0.0); + #endif + #ifdef SECOND_PASS + let v0 = textureLoad(mip_6, i0, slice, 0); + let v1 = textureLoad(mip_6, i1, slice, 0); + let v2 = textureLoad(mip_6, i2, slice, 0); + let v3 = textureLoad(mip_6, i3, slice, 0); + return spd_reduce_4(v0, v1, v2, v3); + #endif +} + +fn spd_reduce_4(v0: vec4f, v1: vec4f, v2: vec4f, v3: vec4f) -> vec4f { + return (v0 + v1 + v2 + v3) * 0.25; +} + +#ifdef SUBGROUP_SUPPORT +fn spd_reduce_quad(v: vec4f) -> vec4f { + let v0 = v; + let v1 = quadSwapX(v); + let v2 = quadSwapY(v); + let v3 = quadSwapDiagonal(v); + return spd_reduce_4(v0, v1, v2, v3); +} +#endif + +fn srgb_from_linear(value: f32) -> f32 { + let j = vec3(0.0031308 * 12.92, 12.92, 1.0 / 2.4); + let k = vec2(1.055, -0.055); + return clamp(j.x, value * j.y, pow(value, j.z) * k.x + k.y); +} \ No newline at end of file diff --git a/vendor/bevy_pbr/src/light_probe/environment_filter.wgsl b/vendor/bevy_pbr/src/light_probe/environment_filter.wgsl new file mode 100644 index 00000000..7b24b760 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/environment_filter.wgsl @@ -0,0 +1,185 @@ +#import bevy_render::maths::PI +#import bevy_pbr::{ + lighting, + utils::{sample_cosine_hemisphere, dir_to_cube_uv, sample_cube_dir, hammersley_2d, rand_vec2f} +} + +struct FilteringConstants { + mip_level: f32, + sample_count: u32, + roughness: f32, + noise_size_bits: vec2u, +} + +@group(0) @binding(0) var input_texture: texture_2d_array; +@group(0) @binding(1) var input_sampler: sampler; +@group(0) @binding(2) var output_texture: texture_storage_2d_array; +@group(0) @binding(3) var constants: FilteringConstants; +@group(0) @binding(4) var blue_noise_texture: texture_2d_array; + +// Sample an environment map with a specific LOD +fn sample_environment(dir: vec3f, level: f32) -> vec4f { + let cube_uv = dir_to_cube_uv(dir); + return textureSampleLevel(input_texture, input_sampler, cube_uv.uv, cube_uv.face, level); +} + +// Blue noise randomization +#ifdef HAS_BLUE_NOISE +fn sample_noise(pixel_coords: vec2u) -> vec4f { + let noise_size = vec2u(1) << constants.noise_size_bits; + let noise_size_mask = noise_size - vec2u(1u); + let noise_coords = pixel_coords & noise_size_mask; + let uv = vec2f(noise_coords) / vec2f(noise_size); + return textureSampleLevel(blue_noise_texture, input_sampler, uv, 0u, 0.0); +} +#else +// pseudo-random numbers using RNG +fn sample_noise(pixel_coords: vec2u) -> vec4f { + var rng_state: u32 = (pixel_coords.x * 3966231743u) ^ (pixel_coords.y * 3928936651u); + let rnd = rand_vec2f(&rng_state); + return vec4f(rnd, 0.0, 0.0); +} +#endif + +// Calculate LOD for environment map lookup using filtered importance sampling +fn calculate_environment_map_lod(pdf: f32, width: f32, samples: f32) -> f32 { + // Solid angle of current sample + let omega_s = 1.0 / (samples * pdf); + + // Solid angle of a texel in the environment map + let omega_p = 4.0 * PI / (6.0 * width * width); + + // Filtered importance sampling: compute the correct LOD + return 0.5 * log2(omega_s / omega_p); +} + +@compute +@workgroup_size(8, 8, 1) +fn generate_radiance_map(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(output_texture).xy; + let invSize = 1.0 / vec2f(size); + + let coords = vec2u(global_id.xy); + let face = global_id.z; + + if (any(coords >= size)) { + return; + } + + // Convert texture coordinates to direction vector + let uv = (vec2f(coords) + 0.5) * invSize; + let normal = sample_cube_dir(uv, face); + + // For radiance map, view direction = normal for perfect reflection + let view = normal; + + // Convert perceptual roughness to physical microfacet roughness + let perceptual_roughness = constants.roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + + // Get blue noise offset for stratification + let vector_noise = sample_noise(coords); + + var radiance = vec3f(0.0); + var total_weight = 0.0; + + // Skip sampling for mirror reflection (roughness = 0) + if (roughness < 0.01) { + radiance = sample_environment(normal, 0.0).rgb; + textureStore(output_texture, coords, face, vec4f(radiance, 1.0)); + return; + } + + // For higher roughness values, use importance sampling + let sample_count = constants.sample_count; + + for (var i = 0u; i < sample_count; i++) { + // Get sample coordinates from Hammersley sequence with blue noise offset + var xi = hammersley_2d(i, sample_count); + xi = fract(xi + vector_noise.rg); // Apply Cranley-Patterson rotation + + // Sample the GGX distribution with the spherical-cap VNDF method + let light_dir = lighting::sample_visible_ggx(xi, roughness, normal, view); + + // Calculate weight (N·L) + let NdotL = dot(normal, light_dir); + + if (NdotL > 0.0) { + // Reconstruct the microfacet half-vector from view and light and compute PDF terms + let half_vector = normalize(view + light_dir); + let NdotH = dot(normal, half_vector); + let NdotV = dot(normal, view); + + // Get the geometric shadowing term + let G = lighting::G_Smith(NdotV, NdotL, roughness); + + // PDF that matches the bounded-VNDF sampling + let pdf = lighting::ggx_vndf_pdf(view, NdotH, roughness); + + // Calculate LOD using filtered importance sampling + // This is crucial to avoid fireflies and improve quality + let width = f32(size.x); + let lod = calculate_environment_map_lod(pdf, width, f32(sample_count)); + + // Get source mip level - ensure we don't go negative + let source_mip = max(0.0, lod); + + // Sample environment map with the light direction + var sample_color = sample_environment(light_dir, source_mip).rgb; + + // Accumulate weighted sample, including geometric term + radiance += sample_color * NdotL * G; + total_weight += NdotL * G; + } + } + + // Normalize by total weight + if (total_weight > 0.0) { + radiance = radiance / total_weight; + } + + // Write result to output texture + textureStore(output_texture, coords, face, vec4f(radiance, 1.0)); +} + +@compute +@workgroup_size(8, 8, 1) +fn generate_irradiance_map(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(output_texture).xy; + let invSize = 1.0 / vec2f(size); + + let coords = vec2u(global_id.xy); + let face = global_id.z; + + if (any(coords >= size)) { + return; + } + + // Convert texture coordinates to direction vector + let uv = (vec2f(coords) + 0.5) * invSize; + let normal = sample_cube_dir(uv, face); + + var irradiance = vec3f(0.0); + + // Use uniform sampling on a hemisphere + for (var i = 0u; i < constants.sample_count; i++) { + // Build a deterministic RNG seed for this pixel / sample + // 4 randomly chosen 32-bit primes + var rng: u32 = (coords.x * 2131358057u) ^ (coords.y * 3416869721u) ^ (face * 1199786941u) ^ (i * 566200673u); + + // Sample a direction from the upper hemisphere around the normal + var sample_dir = sample_cosine_hemisphere(normal, &rng); + + // Sample environment with level 0 (no mip) + var sample_color = sample_environment(sample_dir, 0.0).rgb; + + // Accumulate the contribution + irradiance += sample_color; + } + + // Normalize by number of samples (cosine-weighted sampling already accounts for PDF) + irradiance = irradiance / f32(constants.sample_count); + + // Write result to output texture + textureStore(output_texture, coords, face, vec4f(irradiance, 1.0)); +} diff --git a/vendor/bevy_pbr/src/light_probe/environment_map.rs b/vendor/bevy_pbr/src/light_probe/environment_map.rs new file mode 100644 index 00000000..bd1ba1ae --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/environment_map.rs @@ -0,0 +1,310 @@ +//! Environment maps and reflection probes. +//! +//! An *environment map* consists of a pair of diffuse and specular cubemaps +//! that together reflect the static surrounding area of a region in space. When +//! available, the PBR shader uses these to apply diffuse light and calculate +//! specular reflections. +//! +//! Environment maps come in two flavors, depending on what other components the +//! entities they're attached to have: +//! +//! 1. If attached to a view, they represent the objects located a very far +//! distance from the view, in a similar manner to a skybox. Essentially, these +//! *view environment maps* represent a higher-quality replacement for +//! [`AmbientLight`](bevy_light::AmbientLight) for outdoor scenes. The indirect light from such +//! environment maps are added to every point of the scene, including +//! interior enclosed areas. +//! +//! 2. If attached to a [`bevy_light::LightProbe`], environment maps represent the immediate +//! surroundings of a specific location in the scene. These types of +//! environment maps are known as *reflection probes*. +//! +//! Typically, environment maps are static (i.e. "baked", calculated ahead of +//! time) and so only reflect fixed static geometry. The environment maps must +//! be pre-filtered into a pair of cubemaps, one for the diffuse component and +//! one for the specular component, according to the [split-sum approximation]. +//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or +//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution, +//! while the specular map uses the GGX distribution. +//! +//! The Khronos Group has [several pre-filtered environment maps] available for +//! you to use. +//! +//! Currently, reflection probes (i.e. environment maps attached to light +//! probes) use binding arrays (also known as bindless textures) and +//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are +//! also unsupported if GLSL is in use, due to `naga` limitations. Environment +//! maps attached to views are, however, supported on all platforms. +//! +//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf +//! +//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler +//! +//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui +//! +//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments + +use bevy_asset::AssetId; +use bevy_ecs::{query::QueryItem, system::lifetimeless::Read}; +use bevy_image::Image; +use bevy_light::EnvironmentMapLight; +use bevy_render::{ + extract_instances::ExtractInstance, + render_asset::RenderAssets, + render_resource::{ + binding_types::{self, uniform_buffer}, + BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, ShaderStages, TextureSampleType, + TextureView, + }, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, GpuImage}, +}; + +use core::{num::NonZero, ops::Deref}; + +use crate::{ + add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, + MAX_VIEW_LIGHT_PROBES, +}; + +use super::{LightProbeComponent, RenderViewLightProbes}; + +/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles. +/// +/// This is for use in the render app. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvironmentMapIds { + /// The blurry image that represents diffuse radiance surrounding a region. + pub(crate) diffuse: AssetId, + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub(crate) specular: AssetId, +} + +/// All the bind group entries necessary for PBR shaders to access the +/// environment maps exposed to a view. +pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> { + /// The version used when binding arrays aren't available on the current + /// platform. + Single { + /// The texture view of the view's diffuse cubemap. + diffuse_texture_view: &'a TextureView, + + /// The texture view of the view's specular cubemap. + specular_texture_view: &'a TextureView, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, + + /// The version used when binding arrays are available on the current + /// platform. + Multiple { + /// A texture view of each diffuse cubemap, in the same order that they are + /// supplied to the view (i.e. in the same order as + /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]). + /// + /// This is a vector of `wgpu::TextureView`s. But we don't want to import + /// `wgpu` in this crate, so we refer to it indirectly like this. + diffuse_texture_views: Vec<&'a ::Target>, + + /// As above, but for specular cubemaps. + specular_texture_views: Vec<&'a ::Target>, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, +} + +/// Information about the environment map attached to the view, if any. This is +/// a global environment map that lights everything visible in the view, as +/// opposed to a light probe which affects only a specific area. +pub struct EnvironmentMapViewLightProbeInfo { + /// The index of the diffuse and specular cubemaps in the binding arrays. + pub(crate) cubemap_index: i32, + /// The smallest mip level of the specular cubemap. + pub(crate) smallest_specular_mip_level: u32, + /// The scale factor applied to the diffuse and specular light in the + /// cubemap. This is in units of cd/m² (candela per square meter). + pub(crate) intensity: f32, + /// Whether this lightmap affects the diffuse lighting of lightmapped + /// meshes. + pub(crate) affects_lightmapped_mesh_diffuse: bool, +} + +impl ExtractInstance for EnvironmentMapIds { + type QueryData = Read; + + type QueryFilter = (); + + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(EnvironmentMapIds { + diffuse: item.diffuse_map.id(), + specular: item.specular_map.id(), + }) + } +} + +/// Returns the bind group layout entries for the environment map diffuse and +/// specular binding arrays respectively, in addition to the sampler. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [BindGroupLayoutEntryBuilder; 4] { + let mut texture_cube_binding = + binding_types::texture_cube(TextureSampleType::Float { filterable: true }); + if binding_arrays_are_usable(render_device, render_adapter) { + texture_cube_binding = + texture_cube_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + } + + [ + texture_cube_binding, + texture_cube_binding, + binding_types::sampler(SamplerBindingType::Filtering), + uniform_buffer::(true).visibility(ShaderStages::FRAGMENT), + ] +} + +impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> { + /// Looks up and returns the bindings for the environment map diffuse and + /// specular binding arrays respectively, as well as the sampler. + pub(crate) fn get( + render_view_environment_maps: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> RenderViewEnvironmentMapBindGroupEntries<'a> { + if binding_arrays_are_usable(render_device, render_adapter) { + let mut diffuse_texture_views = vec![]; + let mut specular_texture_views = vec![]; + let mut sampler = None; + + if let Some(environment_maps) = render_view_environment_maps { + for &cubemap_id in &environment_maps.binding_index_to_textures { + add_cubemap_texture_view( + &mut diffuse_texture_views, + &mut sampler, + cubemap_id.diffuse, + images, + fallback_image, + ); + add_cubemap_texture_view( + &mut specular_texture_views, + &mut sampler, + cubemap_id.specular, + images, + fallback_image, + ); + } + } + + // Pad out the bindings to the size of the binding array using fallback + // textures. This is necessary on D3D12 and Metal. + diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view); + specular_texture_views + .resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view); + + return RenderViewEnvironmentMapBindGroupEntries::Multiple { + diffuse_texture_views, + specular_texture_views, + sampler: sampler.unwrap_or(&fallback_image.cube.sampler), + }; + } + + if let Some(environment_maps) = render_view_environment_maps + && let Some(cubemap) = environment_maps.binding_index_to_textures.first() + && let (Some(diffuse_image), Some(specular_image)) = + (images.get(cubemap.diffuse), images.get(cubemap.specular)) + { + return RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view: &diffuse_image.texture_view, + specular_texture_view: &specular_image.texture_view, + sampler: &diffuse_image.sampler, + }; + } + + RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view: &fallback_image.cube.texture_view, + specular_texture_view: &fallback_image.cube.texture_view, + sampler: &fallback_image.cube.sampler, + } + } +} + +impl LightProbeComponent for EnvironmentMapLight { + type AssetId = EnvironmentMapIds; + + // Information needed to render with the environment map attached to the + // view. + type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo; + + fn id(&self, image_assets: &RenderAssets) -> Option { + if image_assets.get(&self.diffuse_map).is_none() + || image_assets.get(&self.specular_map).is_none() + { + None + } else { + Some(EnvironmentMapIds { + diffuse: self.diffuse_map.id(), + specular: self.specular_map.id(), + }) + } + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn affects_lightmapped_mesh_diffuse(&self) -> bool { + self.affects_lightmapped_mesh_diffuse + } + + fn create_render_view_light_probes( + view_component: Option<&EnvironmentMapLight>, + image_assets: &RenderAssets, + ) -> RenderViewLightProbes { + let mut render_view_light_probes = RenderViewLightProbes::new(); + + // Find the index of the cubemap associated with the view, and determine + // its smallest mip level. + if let Some(EnvironmentMapLight { + diffuse_map: diffuse_map_handle, + specular_map: specular_map_handle, + intensity, + affects_lightmapped_mesh_diffuse, + .. + }) = view_component + && let (Some(_), Some(specular_map)) = ( + image_assets.get(diffuse_map_handle), + image_assets.get(specular_map_handle), + ) + { + render_view_light_probes.view_light_probe_info = EnvironmentMapViewLightProbeInfo { + cubemap_index: render_view_light_probes.get_or_insert_cubemap(&EnvironmentMapIds { + diffuse: diffuse_map_handle.id(), + specular: specular_map_handle.id(), + }) as i32, + smallest_specular_mip_level: specular_map.mip_level_count - 1, + intensity: *intensity, + affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse, + }; + }; + + render_view_light_probes + } +} + +impl Default for EnvironmentMapViewLightProbeInfo { + fn default() -> Self { + Self { + cubemap_index: -1, + smallest_specular_mip_level: 0, + intensity: 1.0, + affects_lightmapped_mesh_diffuse: true, + } + } +} diff --git a/vendor/bevy_pbr/src/light_probe/environment_map.wgsl b/vendor/bevy_pbr/src/light_probe/environment_map.wgsl new file mode 100644 index 00000000..93737508 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/environment_map.wgsl @@ -0,0 +1,277 @@ +#define_import_path bevy_pbr::environment_map + +#import bevy_pbr::light_probe::query_light_probe +#import bevy_pbr::mesh_view_bindings as bindings +#import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::mesh_view_bindings::environment_map_uniform +#import bevy_pbr::lighting::{F_Schlick_vec, LightingInput, LayerLightingInput, LAYER_BASE, LAYER_CLEARCOAT} +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges + +struct EnvironmentMapLight { + diffuse: vec3, + specular: vec3, +}; + +struct EnvironmentMapRadiances { + irradiance: vec3, + radiance: vec3, +} + +// Define two versions of this function, one for the case in which there are +// multiple light probes and one for the case in which only the view light probe +// is present. + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + input: LayerLightingInput, + clusterable_object_index_ranges: ptr, + world_position: vec3, + found_diffuse_indirect: bool, +) -> EnvironmentMapRadiances { + // Unpack. + let N = input.N; + let R = input.R; + let perceptual_roughness = input.perceptual_roughness; + let roughness = input.roughness; + + var radiances: EnvironmentMapRadiances; + + // Search for a reflection probe that contains the fragment. + var query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ false, + clusterable_object_index_ranges, + ); + + // If we didn't find a reflection probe, use the view environment map if applicable. + if (query_result.texture_index < 0) { + query_result.texture_index = light_probes.view_cubemap_index; + query_result.intensity = light_probes.intensity_for_view; + query_result.affects_lightmapped_mesh_diffuse = + light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u; + } + + // If there's no cubemap, bail out. + if (query_result.texture_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + let radiance_level = perceptual_roughness * f32(textureNumLevels( + bindings::specular_environment_maps[query_result.texture_index]) - 1u); + + // If we're lightmapped, and we shouldn't accumulate diffuse light from the + // environment map, note that. + var enable_diffuse = !found_diffuse_indirect; +#ifdef LIGHTMAP + enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse; +#endif // LIGHTMAP + + if (enable_diffuse) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_maps[query_result.texture_index], + bindings::environment_map_sampler, + irradiance_sample_dir, + 0.0).rgb * query_result.intensity; + } + + var radiance_sample_dir = radiance_sample_direction(N, R, roughness); + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; + radiances.radiance = textureSampleLevel( + bindings::specular_environment_maps[query_result.texture_index], + bindings::environment_map_sampler, + radiance_sample_dir, + radiance_level).rgb * query_result.intensity; + + return radiances; +} + +#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + input: LayerLightingInput, + clusterable_object_index_ranges: ptr, + world_position: vec3, + found_diffuse_indirect: bool, +) -> EnvironmentMapRadiances { + // Unpack. + let N = input.N; + let R = input.R; + let perceptual_roughness = input.perceptual_roughness; + let roughness = input.roughness; + + var radiances: EnvironmentMapRadiances; + + if (light_probes.view_cubemap_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + // Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform + // because textureNumLevels() does not work on WebGL2 + let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view); + + let intensity = light_probes.intensity_for_view; + + // If we're lightmapped, and we shouldn't accumulate diffuse light from the + // environment map, note that. + var enable_diffuse = !found_diffuse_indirect; +#ifdef LIGHTMAP + enable_diffuse = enable_diffuse && + light_probes.view_environment_map_affects_lightmapped_mesh_diffuse; +#endif // LIGHTMAP + + if (enable_diffuse) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_map, + bindings::environment_map_sampler, + irradiance_sample_dir, + 0.0).rgb * intensity; + } + + var radiance_sample_dir = radiance_sample_direction(N, R, roughness); + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; + radiances.radiance = textureSampleLevel( + bindings::specular_environment_map, + bindings::environment_map_sampler, + radiance_sample_dir, + radiance_level).rgb * intensity; + + return radiances; +} + +#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +#ifdef STANDARD_MATERIAL_CLEARCOAT + +// Adds the environment map light from the clearcoat layer to that of the base +// layer. +fn environment_map_light_clearcoat( + out: ptr, + input: ptr, + clusterable_object_index_ranges: ptr, + found_diffuse_indirect: bool, +) { + // Unpack. + let world_position = (*input).P; + let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV; + let clearcoat_strength = (*input).clearcoat_strength; + + // Calculate the Fresnel term `Fc` for the clearcoat layer. + // 0.04 is a hardcoded value for F0 from the Filament spec. + let clearcoat_F0 = vec3(0.04); + let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength; + let inv_Fc = 1.0 - Fc; + + let clearcoat_radiances = compute_radiances( + (*input).layers[LAYER_CLEARCOAT], + clusterable_object_index_ranges, + world_position, + found_diffuse_indirect, + ); + + // Composite the clearcoat layer on top of the existing one. + // These formulas are from Filament: + // + (*out).diffuse *= inv_Fc; + (*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc; +} + +#endif // STANDARD_MATERIAL_CLEARCOAT + +fn environment_map_light( + input: ptr, + clusterable_object_index_ranges: ptr, + found_diffuse_indirect: bool, +) -> EnvironmentMapLight { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F_ab = (*input).F_ab; + let F0 = (*input).F0_; + let world_position = (*input).P; + + var out: EnvironmentMapLight; + + let radiances = compute_radiances( + (*input).layers[LAYER_BASE], + clusterable_object_index_ranges, + world_position, + found_diffuse_indirect, + ); + + if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) { + out.diffuse = vec3(0.0); + out.specular = vec3(0.0); + return out; + } + + // No real world material has specular values under 0.02, so we use this range as a + // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. + // See: https://google.github.io/filament/Filament.html#specularocclusion + let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33))); + + // Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf + // We initially used this (https://bruop.github.io/ibl) reference with Roughness Dependent + // Fresnel, but it made fresnel very bright so we reverted to the "typical" fresnel term. + let FssEss = (F0 * F_ab.x + F_ab.y) * specular_occlusion; + let Ems = 1.0 - (F_ab.x + F_ab.y); + let Favg = F0 + (1.0 - F0) / 21.0; + let Fms = FssEss * Favg / (1.0 - Ems * Favg); + let FmsEms = Fms * Ems; + let Edss = 1.0 - (FssEss + FmsEms); + let kD = diffuse_color * Edss; + + if (!found_diffuse_indirect) { + out.diffuse = (FmsEms + kD) * radiances.irradiance; + } else { + out.diffuse = vec3(0.0); + } + + out.specular = FssEss * radiances.radiance; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + environment_map_light_clearcoat( + &out, + input, + clusterable_object_index_ranges, + found_diffuse_indirect, + ); +#endif // STANDARD_MATERIAL_CLEARCOAT + + return out; +} + +// "Moving Frostbite to Physically Based Rendering 3.0", listing 22 +// https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf#page=70 +fn radiance_sample_direction(N: vec3, R: vec3, roughness: f32) -> vec3 { + let smoothness = saturate(1.0 - roughness); + let lerp_factor = smoothness * (sqrt(smoothness) + roughness); + return mix(N, R, lerp_factor); +} diff --git a/vendor/bevy_pbr/src/light_probe/generate.rs b/vendor/bevy_pbr/src/light_probe/generate.rs new file mode 100644 index 00000000..a944513b --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/generate.rs @@ -0,0 +1,1191 @@ +//! Like [`EnvironmentMapLight`], but filtered in realtime from a cubemap. +//! +//! An environment map needs to be processed to be able to support uses beyond a simple skybox, +//! such as reflections, and ambient light contribution. +//! This process is called filtering, and can either be done ahead of time (prefiltering), or +//! in realtime, although at a reduced quality. Prefiltering is preferred, but not always possible: +//! sometimes you only gain access to an environment map at runtime, for whatever reason. +//! Typically this is from realtime reflection probes, but can also be from other sources. +//! +//! In any case, Bevy supports both modes of filtering. +//! This module provides realtime filtering via [`bevy_light::GeneratedEnvironmentMapLight`]. +//! For prefiltered environment maps, see [`bevy_light::EnvironmentMapLight`]. +//! These components are intended to be added to a camera. +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryState, With, Without}, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::Image; +use bevy_math::{Quat, UVec2, Vec2}; +use bevy_render::{ + diagnostic::RecordDiagnostics, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel}, + render_resource::{ + binding_types::*, AddressMode, BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, + BindGroupLayoutEntries, CachedComputePipelineId, ComputePassDescriptor, + ComputePipelineDescriptor, DownlevelFlags, Extent3d, FilterMode, PipelineCache, Sampler, + SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, StorageTextureAccess, + Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, + TextureFormatFeatureFlags, TextureSampleType, TextureUsages, TextureView, + TextureViewDescriptor, TextureViewDimension, UniformBuffer, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + settings::WgpuFeatures, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{CachedTexture, GpuImage, TextureCache}, + Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, +}; + +// Implementation: generate diffuse and specular cubemaps required by PBR +// from a given high-res cubemap by +// +// 1. Copying the base mip (level 0) of the source cubemap into an intermediate +// storage texture. +// 2. Generating mipmaps using [single-pass down-sampling] (SPD). +// 3. Convolving the mip chain twice: +// * a [Lambertian convolution] for the 32 × 32 diffuse cubemap +// * a [GGX convolution], once per mip level, for the specular cubemap. +// +// [single-pass down-sampling]: https://gpuopen.com/fidelityfx-spd/ +// [Lambertian convolution]: https://bruop.github.io/ibl/#:~:text=Lambertian%20Diffuse%20Component +// [GGX convolution]: https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf + +use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight}; +use bevy_shader::ShaderDefVal; +use core::cmp::min; +use tracing::info; + +use crate::Bluenoise; + +/// Labels for the environment map generation nodes +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] +pub enum GeneratorNode { + Downsampling, + Filtering, +} + +/// Stores the bind group layouts for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorBindGroupLayouts { + pub downsampling_first: BindGroupLayoutDescriptor, + pub downsampling_second: BindGroupLayoutDescriptor, + pub radiance: BindGroupLayoutDescriptor, + pub irradiance: BindGroupLayoutDescriptor, + pub copy: BindGroupLayoutDescriptor, +} + +/// Samplers for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorSamplers { + pub linear: Sampler, +} + +/// Pipelines for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorPipelines { + pub downsample_first: CachedComputePipelineId, + pub downsample_second: CachedComputePipelineId, + pub copy: CachedComputePipelineId, + pub radiance: CachedComputePipelineId, + pub irradiance: CachedComputePipelineId, +} + +/// Configuration for downsampling strategy based on device limits +#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)] +pub struct DownsamplingConfig { + // can bind ≥12 storage textures and use read-write storage textures + pub combine_bind_group: bool, +} + +pub struct EnvironmentMapGenerationPlugin; + +impl Plugin for EnvironmentMapGenerationPlugin { + fn build(&self, _: &mut App) {} + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + let adapter = render_app.world().resource::(); + let device = render_app.world().resource::(); + + // Cubemap SPD requires at least 6 storage textures + let limit_support = device.limits().max_storage_textures_per_shader_stage >= 6 + && device.limits().max_compute_workgroup_storage_size != 0 + && device.limits().max_compute_workgroup_size_x != 0; + + let downlevel_support = adapter + .get_downlevel_capabilities() + .flags + .contains(DownlevelFlags::COMPUTE_SHADERS); + + if !limit_support || !downlevel_support { + info!("Disabling EnvironmentMapGenerationPlugin because compute is not supported on this platform. This is safe to ignore if you are not using EnvironmentMapGenerationPlugin."); + return; + } + } else { + return; + } + + embedded_asset!(app, "environment_filter.wgsl"); + embedded_asset!(app, "downsample.wgsl"); + embedded_asset!(app, "copy.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()) + .add_systems(Update, generate_environment_map_light); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_render_graph_node::(Core3d, GeneratorNode::Downsampling) + .add_render_graph_node::(Core3d, GeneratorNode::Filtering) + .add_render_graph_edges( + Core3d, + ( + Node3d::EndPrepasses, + GeneratorNode::Downsampling, + GeneratorNode::Filtering, + Node3d::StartMainPass, + ), + ) + .add_systems( + ExtractSchedule, + extract_generated_environment_map_entities.after(generate_environment_map_light), + ) + .add_systems( + Render, + prepare_generated_environment_map_bind_groups + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + prepare_generated_environment_map_intermediate_textures + .in_set(RenderSystems::PrepareResources), + ) + .add_systems( + RenderStartup, + initialize_generated_environment_map_resources, + ); + } +} + +// The number of storage textures required to combine the bind group +const REQUIRED_STORAGE_TEXTURES: u32 = 12; + +/// Initializes all render-world resources used by the environment-map generator once on +/// [`bevy_render::RenderStartup`]. +pub fn initialize_generated_environment_map_resources( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + pipeline_cache: Res, + asset_server: Res, +) { + // Determine whether we can use a single, large bind group for all mip outputs + let storage_texture_limit = render_device.limits().max_storage_textures_per_shader_stage; + + // Determine whether we can read and write to the same rgba16f storage texture + let read_write_support = render_adapter + .get_texture_format_features(TextureFormat::Rgba16Float) + .flags + .contains(TextureFormatFeatureFlags::STORAGE_READ_WRITE); + + // Combine the bind group and use read-write storage if it is supported + let combine_bind_group = + storage_texture_limit >= REQUIRED_STORAGE_TEXTURES && read_write_support; + + // Output mips are write-only + let mips = + texture_storage_2d_array(TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly); + + // Bind group layouts + let (downsampling_first, downsampling_second) = if combine_bind_group { + // One big bind group layout containing all outputs 1–12 + let downsampling = BindGroupLayoutDescriptor::new( + "downsampling_bind_group_layout_combined", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 1 + mips, // 2 + mips, // 3 + mips, // 4 + mips, // 5 + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::ReadWrite, + ), // 6 + mips, // 7 + mips, // 8 + mips, // 9 + mips, // 10 + mips, // 11 + mips, // 12 + ), + ), + ); + + (downsampling.clone(), downsampling) + } else { + // Split layout: first pass outputs 1–6, second pass outputs 7–12 (input mip6 read-only) + + let downsampling_first = BindGroupLayoutDescriptor::new( + "downsampling_first_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + // Input mip 0 + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 1 + mips, // 2 + mips, // 3 + mips, // 4 + mips, // 5 + mips, // 6 + ), + ), + ); + + let downsampling_second = BindGroupLayoutDescriptor::new( + "downsampling_second_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + // Input mip 6 + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 7 + mips, // 8 + mips, // 9 + mips, // 10 + mips, // 11 + mips, // 12 + ), + ), + ); + + (downsampling_first, downsampling_second) + }; + let radiance = BindGroupLayoutDescriptor::new( + "radiance_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source environment cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), // Source sampler + // Output specular map + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(false), // Uniforms + texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture + ), + ), + ); + + let irradiance = BindGroupLayoutDescriptor::new( + "irradiance_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source environment cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), // Source sampler + // Output irradiance map + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(false), // Uniforms + texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture + ), + ), + ); + + let copy = BindGroupLayoutDescriptor::new( + "copy_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + // Destination mip0 + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ); + + let layouts = GeneratorBindGroupLayouts { + downsampling_first, + downsampling_second, + radiance, + irradiance, + copy, + }; + + // Samplers + let linear = render_device.create_sampler(&SamplerDescriptor { + label: Some("generator_linear_sampler"), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Linear, + ..Default::default() + }); + + let samplers = GeneratorSamplers { linear }; + + // Pipelines + let features = render_device.features(); + let mut shader_defs = vec![]; + if features.contains(WgpuFeatures::SUBGROUP) { + shader_defs.push(ShaderDefVal::Int("SUBGROUP_SUPPORT".into(), 1)); + } + if combine_bind_group { + shader_defs.push(ShaderDefVal::Int("COMBINE_BIND_GROUP".into(), 1)); + } + #[cfg(feature = "bluenoise_texture")] + { + shader_defs.push(ShaderDefVal::Int("HAS_BLUE_NOISE".into(), 1)); + } + + let downsampling_shader = load_embedded_asset!(asset_server.as_ref(), "downsample.wgsl"); + let env_filter_shader = load_embedded_asset!(asset_server.as_ref(), "environment_filter.wgsl"); + let copy_shader = load_embedded_asset!(asset_server.as_ref(), "copy.wgsl"); + + // First pass for base mip Levels (0-5) + let downsample_first = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("downsampling_first_pipeline".into()), + layout: vec![layouts.downsampling_first.clone()], + push_constant_ranges: vec![], + shader: downsampling_shader.clone(), + shader_defs: { + let mut defs = shader_defs.clone(); + if !combine_bind_group { + defs.push(ShaderDefVal::Int("FIRST_PASS".into(), 1)); + } + defs + }, + entry_point: Some("downsample_first".into()), + zero_initialize_workgroup_memory: false, + }); + + let downsample_second = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("downsampling_second_pipeline".into()), + layout: vec![layouts.downsampling_second.clone()], + push_constant_ranges: vec![], + shader: downsampling_shader, + shader_defs: { + let mut defs = shader_defs.clone(); + if !combine_bind_group { + defs.push(ShaderDefVal::Int("SECOND_PASS".into(), 1)); + } + defs + }, + entry_point: Some("downsample_second".into()), + zero_initialize_workgroup_memory: false, + }); + + // Radiance map for specular environment maps + let radiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("radiance_pipeline".into()), + layout: vec![layouts.radiance.clone()], + push_constant_ranges: vec![], + shader: env_filter_shader.clone(), + shader_defs: shader_defs.clone(), + entry_point: Some("generate_radiance_map".into()), + zero_initialize_workgroup_memory: false, + }); + + // Irradiance map for diffuse environment maps + let irradiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("irradiance_pipeline".into()), + layout: vec![layouts.irradiance.clone()], + push_constant_ranges: vec![], + shader: env_filter_shader, + shader_defs: shader_defs.clone(), + entry_point: Some("generate_irradiance_map".into()), + zero_initialize_workgroup_memory: false, + }); + + // Copy pipeline handles format conversion and populates mip0 when formats differ + let copy_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("copy_pipeline".into()), + layout: vec![layouts.copy.clone()], + push_constant_ranges: vec![], + shader: copy_shader, + shader_defs: vec![], + entry_point: Some("copy".into()), + zero_initialize_workgroup_memory: false, + }); + + let pipelines = GeneratorPipelines { + downsample_first, + downsample_second, + radiance, + irradiance, + copy: copy_pipeline, + }; + + // Insert all resources into the render world + commands.insert_resource(layouts); + commands.insert_resource(samplers); + commands.insert_resource(pipelines); + commands.insert_resource(DownsamplingConfig { combine_bind_group }); +} + +pub fn extract_generated_environment_map_entities( + query: Extract< + Query<( + RenderEntity, + &GeneratedEnvironmentMapLight, + &EnvironmentMapLight, + )>, + >, + mut commands: Commands, + render_images: Res>, +) { + for (entity, filtered_env_map, env_map_light) in query.iter() { + let Some(env_map) = render_images.get(&filtered_env_map.environment_map) else { + continue; + }; + + let diffuse_map = render_images.get(&env_map_light.diffuse_map); + let specular_map = render_images.get(&env_map_light.specular_map); + + // continue if the diffuse map is not found + if diffuse_map.is_none() || specular_map.is_none() { + continue; + } + + let diffuse_map = diffuse_map.unwrap(); + let specular_map = specular_map.unwrap(); + + let render_filtered_env_map = RenderEnvironmentMap { + environment_map: env_map.clone(), + diffuse_map: diffuse_map.clone(), + specular_map: specular_map.clone(), + intensity: filtered_env_map.intensity, + rotation: filtered_env_map.rotation, + affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse, + }; + commands + .get_entity(entity) + .expect("Entity not synced to render world") + .insert(render_filtered_env_map); + } +} + +// A render-world specific version of FilteredEnvironmentMapLight that uses CachedTexture +#[derive(Component, Clone)] +pub struct RenderEnvironmentMap { + pub environment_map: GpuImage, + pub diffuse_map: GpuImage, + pub specular_map: GpuImage, + pub intensity: f32, + pub rotation: Quat, + pub affects_lightmapped_mesh_diffuse: bool, +} + +#[derive(Component)] +pub struct IntermediateTextures { + pub environment_map: CachedTexture, +} + +/// Returns the total number of mip levels for the provided square texture size. +/// `size` must be a power of two greater than zero. For example, `size = 512` → `9`. +#[inline] +fn compute_mip_count(size: u32) -> u32 { + debug_assert!(size.is_power_of_two()); + 32 - size.leading_zeros() +} + +/// Prepares textures needed for single pass downsampling +pub fn prepare_generated_environment_map_intermediate_textures( + light_probes: Query<(Entity, &RenderEnvironmentMap)>, + render_device: Res, + mut texture_cache: ResMut, + mut commands: Commands, +) { + for (entity, env_map_light) in &light_probes { + let base_size = env_map_light.environment_map.size.width; + let mip_level_count = compute_mip_count(base_size); + + let environment_map = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("intermediate_environment_map"), + size: Extent3d { + width: base_size, + height: base_size, + depth_or_array_layers: 6, // Cubemap faces + }, + mip_level_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::STORAGE_BINDING + | TextureUsages::COPY_DST, + view_formats: &[], + }, + ); + + commands + .entity(entity) + .insert(IntermediateTextures { environment_map }); + } +} + +/// Shader constants for downsampling algorithm +#[derive(Clone, Copy, ShaderType)] +#[repr(C)] +pub struct DownsamplingConstants { + mips: u32, + inverse_input_size: Vec2, + _padding: u32, +} + +/// Constants for filtering +#[derive(Clone, Copy, ShaderType)] +#[repr(C)] +pub struct FilteringConstants { + mip_level: f32, + sample_count: u32, + roughness: f32, + noise_size_bits: UVec2, +} + +/// Stores bind groups for the environment map generation pipelines +#[derive(Component)] +pub struct GeneratorBindGroups { + pub downsampling_first: BindGroup, + pub downsampling_second: BindGroup, + pub radiance: Vec, // One per mip level + pub irradiance: BindGroup, + pub copy: BindGroup, +} + +/// Prepares bind groups for environment map generation pipelines +pub fn prepare_generated_environment_map_bind_groups( + light_probes: Query< + (Entity, &IntermediateTextures, &RenderEnvironmentMap), + With, + >, + render_device: Res, + pipeline_cache: Res, + queue: Res, + layouts: Res, + samplers: Res, + render_images: Res>, + bluenoise: Res, + config: Res, + mut commands: Commands, +) { + // Skip until the blue-noise texture is available to avoid panicking. + // The system will retry next frame once the asset has loaded. + let Some(stbn_texture) = render_images.get(&bluenoise.texture) else { + return; + }; + + assert!(stbn_texture.size.width.is_power_of_two()); + assert!(stbn_texture.size.height.is_power_of_two()); + let noise_size_bits = UVec2::new( + stbn_texture.size.width.trailing_zeros(), + stbn_texture.size.height.trailing_zeros(), + ); + + for (entity, textures, env_map_light) in &light_probes { + // Determine mip chain based on input size + let base_size = env_map_light.environment_map.size.width; + let mip_count = compute_mip_count(base_size); + let last_mip = mip_count - 1; + let env_map_texture = env_map_light.environment_map.texture.clone(); + + // Create downsampling constants + let downsampling_constants = DownsamplingConstants { + mips: mip_count - 1, // Number of mips we are generating (excluding mip 0) + inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32), + _padding: 0, + }; + + let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants); + downsampling_constants_buffer.write_buffer(&render_device, &queue); + + let input_env_map_first = env_map_texture.clone().create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + // Utility closure to get a unique storage view for a given mip level. + let mip_storage = |level: u32| { + if level <= last_mip { + create_storage_view(&textures.environment_map.texture, level, &render_device) + } else { + // Return a fresh 1×1 placeholder view so each binding has its own sub-resource and cannot alias. + create_placeholder_storage_view(&render_device) + } + }; + + // Depending on device limits, build either a combined or split bind group layout + let (downsampling_first_bind_group, downsampling_second_bind_group) = + if config.combine_bind_group { + // Combined layout expects destinations 1–12 in both bind groups + let bind_group = render_device.create_bind_group( + "downsampling_bind_group_combined_first", + &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first), + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_first, + &mip_storage(1), + &mip_storage(2), + &mip_storage(3), + &mip_storage(4), + &mip_storage(5), + &mip_storage(6), + &mip_storage(7), + &mip_storage(8), + &mip_storage(9), + &mip_storage(10), + &mip_storage(11), + &mip_storage(12), + )), + ); + + (bind_group.clone(), bind_group) + } else { + // Split path requires a separate view for mip6 input + let input_env_map_second = + textures + .environment_map + .texture + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + base_mip_level: min(6, last_mip), + mip_level_count: Some(1), + ..Default::default() + }); + + // Split layout (current behavior) + let first = render_device.create_bind_group( + "downsampling_first_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.downsampling_first), + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_first, + &mip_storage(1), + &mip_storage(2), + &mip_storage(3), + &mip_storage(4), + &mip_storage(5), + &mip_storage(6), + )), + ); + + let second = render_device.create_bind_group( + "downsampling_second_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.downsampling_second), + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_second, + &mip_storage(7), + &mip_storage(8), + &mip_storage(9), + &mip_storage(10), + &mip_storage(11), + &mip_storage(12), + )), + ); + + (first, second) + }; + + // create a 2d array view of the bluenoise texture + let stbn_texture_view = stbn_texture + .texture + .clone() + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + // Create radiance map bind groups for each mip level + let num_mips = mip_count as usize; + let mut radiance_bind_groups = Vec::with_capacity(num_mips); + + for mip in 0..num_mips { + // Calculate roughness from 0.0 (mip 0) to 0.889 (mip 8) + // We don't need roughness=1.0 as a mip level because it's handled by the separate diffuse irradiance map + let roughness = mip as f32 / (num_mips - 1) as f32; + let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32); + + let radiance_constants = FilteringConstants { + mip_level: mip as f32, + sample_count, + roughness, + noise_size_bits, + }; + + let mut radiance_constants_buffer = UniformBuffer::from(radiance_constants); + radiance_constants_buffer.write_buffer(&render_device, &queue); + + let mip_storage_view = create_storage_view( + &env_map_light.specular_map.texture, + mip as u32, + &render_device, + ); + let bind_group = render_device.create_bind_group( + Some(format!("radiance_bind_group_mip_{mip}").as_str()), + &pipeline_cache.get_bind_group_layout(&layouts.radiance), + &BindGroupEntries::sequential(( + &textures.environment_map.default_view, + &samplers.linear, + &mip_storage_view, + &radiance_constants_buffer, + &stbn_texture_view, + )), + ); + + radiance_bind_groups.push(bind_group); + } + + // Create irradiance bind group + let irradiance_constants = FilteringConstants { + mip_level: 0.0, + // 32 phi, 32 theta = 1024 samples total + sample_count: 1024, + roughness: 1.0, + noise_size_bits, + }; + + let mut irradiance_constants_buffer = UniformBuffer::from(irradiance_constants); + irradiance_constants_buffer.write_buffer(&render_device, &queue); + + // create a 2d array view + let irradiance_map = + env_map_light + .diffuse_map + .texture + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + let irradiance_bind_group = render_device.create_bind_group( + "irradiance_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.irradiance), + &BindGroupEntries::sequential(( + &textures.environment_map.default_view, + &samplers.linear, + &irradiance_map, + &irradiance_constants_buffer, + &stbn_texture_view, + )), + ); + + // Create copy bind group (source env map → destination mip0) + let src_view = env_map_light + .environment_map + .texture + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + let dst_view = create_storage_view(&textures.environment_map.texture, 0, &render_device); + + let copy_bind_group = render_device.create_bind_group( + "copy_bind_group", + &pipeline_cache.get_bind_group_layout(&layouts.copy), + &BindGroupEntries::with_indices(((0, &src_view), (1, &dst_view))), + ); + + commands.entity(entity).insert(GeneratorBindGroups { + downsampling_first: downsampling_first_bind_group, + downsampling_second: downsampling_second_bind_group, + radiance: radiance_bind_groups, + irradiance: irradiance_bind_group, + copy: copy_bind_group, + }); + } +} + +/// Helper function to create a storage texture view for a specific mip level +fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView { + texture.create_view(&TextureViewDescriptor { + label: Some(format!("storage_view_mip_{mip}").as_str()), + format: Some(texture.format()), + dimension: Some(TextureViewDimension::D2Array), + aspect: TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(texture.depth_or_array_layers()), + usage: Some(TextureUsages::STORAGE_BINDING), + }) +} + +/// To ensure compatibility in web browsers, each call returns a unique resource so that multiple missing mip +/// bindings in the same bind-group never alias. +fn create_placeholder_storage_view(render_device: &RenderDevice) -> TextureView { + let tex = render_device.create_texture(&TextureDescriptor { + label: Some("lightprobe_placeholder"), + size: Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + tex.create_view(&TextureViewDescriptor::default()) +} + +/// Downsampling node implementation that handles all parts of the mip chain +pub struct DownsamplingNode { + query: QueryState<( + Entity, + Read, + Read, + )>, +} + +impl FromWorld for DownsamplingNode { + fn from_world(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for DownsamplingNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + + let Some(downsample_first_pipeline) = + pipeline_cache.get_compute_pipeline(pipelines.downsample_first) + else { + return Ok(()); + }; + + let Some(downsample_second_pipeline) = + pipeline_cache.get_compute_pipeline(pipelines.downsample_second) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + for (_, bind_groups, env_map_light) in self.query.iter_manual(world) { + // Copy base mip using compute shader with pre-built bind group + let Some(copy_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.copy) else { + return Ok(()); + }; + + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_copy"), + timestamp_writes: None, + }); + + let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_copy"); + + compute_pass.set_pipeline(copy_pipeline); + compute_pass.set_bind_group(0, &bind_groups.copy, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(8); + let wg_y = tex_size.height.div_ceil(8); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); + + pass_span.end(&mut compute_pass); + } + + // First pass - process mips 0-5 + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_downsampling_first_pass"), + timestamp_writes: None, + }); + + let pass_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_first_pass"); + + compute_pass.set_pipeline(downsample_first_pipeline); + compute_pass.set_bind_group(0, &bind_groups.downsampling_first, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(64); + let wg_y = tex_size.height.div_ceil(64); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); // 6 faces + + pass_span.end(&mut compute_pass); + } + + // Second pass - process mips 6-12 + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_downsampling_second_pass"), + timestamp_writes: None, + }); + + let pass_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_second_pass"); + + compute_pass.set_pipeline(downsample_second_pipeline); + compute_pass.set_bind_group(0, &bind_groups.downsampling_second, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(256); + let wg_y = tex_size.height.div_ceil(256); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); + + pass_span.end(&mut compute_pass); + } + } + + Ok(()) + } +} + +/// Radiance map node for generating specular environment maps +pub struct FilteringNode { + query: QueryState<( + Entity, + Read, + Read, + )>, +} + +impl FromWorld for FilteringNode { + fn from_world(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for FilteringNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + + let Some(radiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.radiance) + else { + return Ok(()); + }; + let Some(irradiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.irradiance) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + for (_, bind_groups, env_map_light) in self.query.iter_manual(world) { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_radiance_map"), + timestamp_writes: None, + }); + + let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_radiance_map"); + + compute_pass.set_pipeline(radiance_pipeline); + + let base_size = env_map_light.specular_map.size.width; + + // Radiance convolution pass + // Process each mip at different roughness levels + for (mip, bind_group) in bind_groups.radiance.iter().enumerate() { + compute_pass.set_bind_group(0, bind_group, &[]); + + // Calculate dispatch size based on mip level + let mip_size = base_size >> mip; + let workgroup_count = mip_size.div_ceil(8); + + // Dispatch for all 6 faces + compute_pass.dispatch_workgroups(workgroup_count, workgroup_count, 6); + } + pass_span.end(&mut compute_pass); + // End the compute pass before starting the next one + drop(compute_pass); + + // Irradiance convolution pass + // Generate the diffuse environment map + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_irradiance_map"), + timestamp_writes: None, + }); + + let irr_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_irradiance_map"); + + compute_pass.set_pipeline(irradiance_pipeline); + compute_pass.set_bind_group(0, &bind_groups.irradiance, &[]); + + // 32×32 texture processed with 8×8 workgroups for all 6 faces + compute_pass.dispatch_workgroups(4, 4, 6); + + irr_span.end(&mut compute_pass); + } + } + + Ok(()) + } +} + +/// System that generates an `EnvironmentMapLight` component based on the `GeneratedEnvironmentMapLight` component +pub fn generate_environment_map_light( + mut commands: Commands, + mut images: ResMut>, + query: Query<(Entity, &GeneratedEnvironmentMapLight), Without>, +) { + for (entity, filtered_env_map) in &query { + // Validate and fetch the source cubemap so we can size our targets correctly + let Some(src_image) = images.get(&filtered_env_map.environment_map) else { + // Texture not ready yet – try again next frame + continue; + }; + + let base_size = src_image.texture_descriptor.size.width; + + // Sanity checks – square, power-of-two, ≤ 8192 + if src_image.texture_descriptor.size.height != base_size + || !base_size.is_power_of_two() + || base_size > 8192 + { + panic!( + "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}", + base_size, src_image.texture_descriptor.size.height + ); + } + + let mip_count = compute_mip_count(base_size); + + // Create a placeholder for the irradiance map + let mut diffuse = Image::new_fill( + Extent3d { + width: 32, + height: 32, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + diffuse.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING; + + diffuse.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + let diffuse_handle = images.add(diffuse); + + // Create a placeholder for the specular map. It matches the input cubemap resolution. + let mut specular = Image::new_fill( + Extent3d { + width: base_size, + height: base_size, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + // Set up for mipmaps + specular.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING; + specular.texture_descriptor.mip_level_count = mip_count; + + // When setting mip_level_count, we need to allocate appropriate data size + // For GPU-generated mipmaps, we can set data to None since the GPU will generate the data + specular.data = None; + + specular.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + mip_level_count: Some(mip_count), + ..Default::default() + }); + + let specular_handle = images.add(specular); + + // Add the EnvironmentMapLight component with the placeholder handles + commands.entity(entity).insert(EnvironmentMapLight { + diffuse_map: diffuse_handle, + specular_map: specular_handle, + intensity: filtered_env_map.intensity, + rotation: filtered_env_map.rotation, + affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse, + }); + } +} diff --git a/vendor/bevy_pbr/src/light_probe/irradiance_volume.rs b/vendor/bevy_pbr/src/light_probe/irradiance_volume.rs new file mode 100644 index 00000000..417f2208 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -0,0 +1,325 @@ +//! Irradiance volumes, also known as voxel global illumination. +//! +//! An *irradiance volume* is a cuboid voxel region consisting of +//! regularly-spaced precomputed samples of diffuse indirect light. They're +//! ideal if you have a dynamic object such as a character that can move about +//! static non-moving geometry such as a level in a game, and you want that +//! dynamic object to be affected by the light bouncing off that static +//! geometry. +//! +//! To use irradiance volumes, you need to precompute, or *bake*, the indirect +//! light in your scene. Bevy doesn't currently come with a way to do this. +//! Fortunately, [Blender] provides a [baking tool] as part of the Eevee +//! renderer, and its irradiance volumes are compatible with those used by Bevy. +//! The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that can +//! extract the baked irradiance volumes from the Blender `.blend` file and +//! package them up into a `.ktx2` texture for use by the engine. See the +//! documentation in the `bevy-baked-gi` project for more details on this +//! workflow. +//! +//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered +//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a +//! scene with the [`bevy_transform::components::Transform`] component. The 3D +//! voxel grid will be stretched to fill the interior of the cube, with linear +//! interpolation, and the illumination from the irradiance volume will apply to +//! all fragments within that bounding region. +//! +//! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in +//! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of +//! light from the six 3D cardinal directions and blend the sides together +//! according to the surface normal. For an explanation of why ambient cubes +//! were chosen over spherical harmonics, see [Why ambient cubes?] below. +//! +//! If you wish to use a tool other than `export-blender-gi` to produce the +//! irradiance volumes, you'll need to pack the irradiance volumes in the +//! following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is +//! expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The unnormalized +//! texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)* with +//! side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows: +//! +//! ```text +//! s = x +//! +//! t = y + ⎰ 0 if S ∈ {-X, -Y, -Z} +//! ⎱ Ry if S ∈ {+X, +Y, +Z} +//! +//! ⎧ 0 if S ∈ {-X, +X} +//! p = z + ⎨ Rz if S ∈ {-Y, +Y} +//! ⎩ 2Rz if S ∈ {-Z, +Z} +//! ``` +//! +//! Visually, in a left-handed coordinate system with Y up, viewed from the +//! right, the 3D texture looks like a stacked series of voxel grids, one for +//! each cube side, in this order: +//! +//! | **+X** | **+Y** | **+Z** | +//! | ------ | ------ | ------ | +//! | **-X** | **-Y** | **-Z** | +//! +//! A terminology note: Other engines may refer to irradiance volumes as *voxel +//! global illumination*, *VXGI*, or simply as *light probes*. Sometimes *light +//! probe* refers to what Bevy calls a reflection probe. In Bevy, *light probe* +//! is a generic term that encompasses all cuboid bounding regions that capture +//! indirect illumination, whether based on voxels or not. +//! +//! Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL 2), +//! then only the closest irradiance volume to the view will be taken into +//! account during rendering. The required `wgpu` features are +//! [`bevy_render::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY`] and +//! [`bevy_render::settings::WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`]. +//! +//! ## Why ambient cubes? +//! +//! This section describes the motivation behind the decision to use ambient +//! cubes in Bevy. It's not needed to use the feature; feel free to skip it +//! unless you're interested in its internal design. +//! +//! Bevy uses *Half-Life 2*-style ambient cubes (usually abbreviated as *HL2*) +//! as the representation of irradiance for light probes instead of the +//! more-popular spherical harmonics (*SH*). This might seem to be a surprising +//! choice, but it turns out to work well for the specific case of voxel +//! sampling on the GPU. Spherical harmonics have two problems that make them +//! less ideal for this use case: +//! +//! 1. The level 1 spherical harmonic coefficients can be negative. That +//! prevents the use of the efficient [RGB9E5 texture format], which only +//! encodes unsigned floating point numbers, and forces the use of the +//! less-efficient [RGBA16F format] if hardware interpolation is desired. +//! +//! 2. As an alternative to RGBA16F, level 1 spherical harmonics can be +//! normalized and scaled to the SH0 base color, as [Frostbite] does. This +//! allows them to be packed in standard LDR RGBA8 textures. However, this +//! prevents the use of hardware trilinear filtering, as the nonuniform scale +//! factor means that hardware interpolation no longer produces correct results. +//! The 8 texture fetches needed to interpolate between voxels can be upwards of +//! twice as slow as the hardware interpolation. +//! +//! The following chart summarizes the costs and benefits of ambient cubes, +//! level 1 spherical harmonics, and level 2 spherical harmonics: +//! +//! | Technique | HW-interpolated samples | Texel fetches | Bytes per voxel | Quality | +//! | ------------------------ | ----------------------- | ------------- | --------------- | ------- | +//! | Ambient cubes | 3 | 0 | 24 | Medium | +//! | Level 1 SH, compressed | 0 | 36 | 16 | Low | +//! | Level 1 SH, uncompressed | 4 | 0 | 24 | Low | +//! | Level 2 SH, compressed | 0 | 72 | 28 | High | +//! | Level 2 SH, uncompressed | 9 | 0 | 54 | High | +//! +//! (Note that the number of bytes per voxel can be reduced using various +//! texture compression methods, but the overall ratios remain similar.) +//! +//! From these data, we can see that ambient cubes balance fast lookups (from +//! leveraging hardware interpolation) with relatively-small storage +//! requirements and acceptable quality. Hence, they were chosen for irradiance +//! volumes in Bevy. +//! +//! [*ambient cubes*]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf +//! +//! [spherical harmonics]: https://en.wikipedia.org/wiki/Spherical_harmonic_lighting +//! +//! [RGB9E5 texture format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#RGB9_E5 +//! +//! [RGBA16F format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#Low-bitdepth_floats +//! +//! [Frostbite]: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf#page=53 +//! +//! [Mitchell 2006, slide 27]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf#page=27 +//! +//! [Blender]: http://blender.org/ +//! +//! [baking tool]: https://docs.blender.org/manual/en/latest/render/eevee/light_probes/volume.html +//! +//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi +//! +//! [Why ambient cubes?]: #why-ambient-cubes + +use bevy_image::Image; +use bevy_light::IrradianceVolume; +use bevy_render::{ + render_asset::RenderAssets, + render_resource::{ + binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType, + TextureView, + }, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, GpuImage}, +}; +use core::{num::NonZero, ops::Deref}; + +use bevy_asset::AssetId; + +use crate::{ + add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes, + MAX_VIEW_LIGHT_PROBES, +}; + +use super::LightProbeComponent; + +/// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can +/// overflow the number of texture bindings when deferred rendering is in use +/// (see issue #11885). +pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32")); + +/// All the bind group entries necessary for PBR shaders to access the +/// irradiance volumes exposed to a view. +pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> { + /// The version used when binding arrays aren't available on the current platform. + Single { + /// The texture view of the closest light probe. + texture_view: &'a TextureView, + /// A sampler used to sample voxels of the irradiance volume. + sampler: &'a Sampler, + }, + + /// The version used when binding arrays are available on the current + /// platform. + Multiple { + /// A texture view of the voxels of each irradiance volume, in the same + /// order that they are supplied to the view (i.e. in the same order as + /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]). + /// + /// This is a vector of `wgpu::TextureView`s. But we don't want to import + /// `wgpu` in this crate, so we refer to it indirectly like this. + texture_views: Vec<&'a ::Target>, + + /// A sampler used to sample voxels of the irradiance volumes. + sampler: &'a Sampler, + }, +} + +impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> { + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. + pub(crate) fn get( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + if binding_arrays_are_usable(render_device, render_adapter) { + RenderViewIrradianceVolumeBindGroupEntries::get_multiple( + render_view_irradiance_volumes, + images, + fallback_image, + ) + } else { + RenderViewIrradianceVolumeBindGroupEntries::single( + render_view_irradiance_volumes, + images, + fallback_image, + ) + } + } + + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. This is the version used when binding + /// arrays are available on the current platform. + fn get_multiple( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + let mut texture_views = vec![]; + let mut sampler = None; + + if let Some(irradiance_volumes) = render_view_irradiance_volumes { + for &cubemap_id in &irradiance_volumes.binding_index_to_textures { + add_cubemap_texture_view( + &mut texture_views, + &mut sampler, + cubemap_id, + images, + fallback_image, + ); + } + } + + // Pad out the bindings to the size of the binding array using fallback + // textures. This is necessary on D3D12 and Metal. + texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.d3.texture_view); + + RenderViewIrradianceVolumeBindGroupEntries::Multiple { + texture_views, + sampler: sampler.unwrap_or(&fallback_image.d3.sampler), + } + } + + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. This is the version used when binding + /// arrays aren't available on the current platform. + fn single( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + if let Some(irradiance_volumes) = render_view_irradiance_volumes + && let Some(irradiance_volume) = irradiance_volumes.render_light_probes.first() + && irradiance_volume.texture_index >= 0 + && let Some(image_id) = irradiance_volumes + .binding_index_to_textures + .get(irradiance_volume.texture_index as usize) + && let Some(image) = images.get(*image_id) + { + return RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view: &image.texture_view, + sampler: &image.sampler, + }; + } + + RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view: &fallback_image.d3.texture_view, + sampler: &fallback_image.d3.sampler, + } + } +} + +/// Returns the bind group layout entries for the voxel texture and sampler +/// respectively. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [BindGroupLayoutEntryBuilder; 2] { + let mut texture_3d_binding = + binding_types::texture_3d(TextureSampleType::Float { filterable: true }); + if binding_arrays_are_usable(render_device, render_adapter) { + texture_3d_binding = + texture_3d_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + } + + [ + texture_3d_binding, + binding_types::sampler(SamplerBindingType::Filtering), + ] +} + +impl LightProbeComponent for IrradianceVolume { + type AssetId = AssetId; + + // Irradiance volumes can't be attached to the view, so we store nothing + // here. + type ViewLightProbeInfo = (); + + fn id(&self, image_assets: &RenderAssets) -> Option { + if image_assets.get(&self.voxels).is_none() { + None + } else { + Some(self.voxels.id()) + } + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn affects_lightmapped_mesh_diffuse(&self) -> bool { + self.affects_lightmapped_meshes + } + + fn create_render_view_light_probes( + _: Option<&Self>, + _: &RenderAssets, + ) -> RenderViewLightProbes { + RenderViewLightProbes::new() + } +} diff --git a/vendor/bevy_pbr/src/light_probe/irradiance_volume.wgsl b/vendor/bevy_pbr/src/light_probe/irradiance_volume.wgsl new file mode 100644 index 00000000..f079bd6a --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/irradiance_volume.wgsl @@ -0,0 +1,73 @@ +#define_import_path bevy_pbr::irradiance_volume + +#import bevy_pbr::light_probe::query_light_probe +#import bevy_pbr::mesh_view_bindings::{ + irradiance_volumes, + irradiance_volume, + irradiance_volume_sampler, + light_probes, +}; +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges + +#ifdef IRRADIANCE_VOLUMES_ARE_USABLE + +// See: +// https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf +// Slide 28, "Ambient Cube Basis" +fn irradiance_volume_light( + world_position: vec3, + N: vec3, + clusterable_object_index_ranges: ptr, +) -> vec3 { + // Search for an irradiance volume that contains the fragment. + let query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ true, + clusterable_object_index_ranges, + ); + + // If there was no irradiance volume found, bail out. + if (query_result.texture_index < 0) { + return vec3(0.0f); + } + + // If we're lightmapped, and the irradiance volume contributes no diffuse + // light, then bail out. +#ifdef LIGHTMAP + if (!query_result.affects_lightmapped_mesh_diffuse) { + return vec3(0.0f); + } +#endif // LIGHTMAP + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY + let irradiance_volume_texture = irradiance_volumes[query_result.texture_index]; +#else + let irradiance_volume_texture = irradiance_volume; +#endif + + let atlas_resolution = vec3(textureDimensions(irradiance_volume_texture)); + let resolution = vec3(textureDimensions(irradiance_volume_texture) / vec3(1u, 2u, 3u)); + + // Make sure to clamp to the edges to avoid texture bleed. + var unit_pos = (query_result.light_from_world * vec4(world_position, 1.0f)).xyz; + let stp = clamp((unit_pos + 0.5) * resolution, vec3(0.5f), resolution - vec3(0.5f)); + let uvw = stp / atlas_resolution; + + // The bottom half of each cube slice is the negative part, so choose it if applicable on each + // slice. + let neg_offset = select(vec3(0.0f), vec3(0.5f), N < vec3(0.0f)); + + let uvw_x = uvw + vec3(0.0f, neg_offset.x, 0.0f); + let uvw_y = uvw + vec3(0.0f, neg_offset.y, 1.0f / 3.0f); + let uvw_z = uvw + vec3(0.0f, neg_offset.z, 2.0f / 3.0f); + + let rgb_x = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_x, 0.0).rgb; + let rgb_y = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_y, 0.0).rgb; + let rgb_z = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_z, 0.0).rgb; + + // Use Valve's formula to sample. + let NN = N * N; + return (rgb_x * NN.x + rgb_y * NN.y + rgb_z * NN.z) * query_result.intensity; +} + +#endif // IRRADIANCE_VOLUMES_ARE_USABLE diff --git a/vendor/bevy_pbr/src/light_probe/light_probe.wgsl b/vendor/bevy_pbr/src/light_probe/light_probe.wgsl new file mode 100644 index 00000000..16a21125 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/light_probe.wgsl @@ -0,0 +1,154 @@ +#define_import_path bevy_pbr::light_probe + +#import bevy_pbr::clustered_forward +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges +#import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::mesh_view_types::LightProbe + +// The result of searching for a light probe. +struct LightProbeQueryResult { + // The index of the light probe texture or textures in the binding array or + // arrays. + texture_index: i32, + // A scale factor that's applied to the diffuse and specular light from the + // light probe. This is in units of cd/m² (candela per square meter). + intensity: f32, + // Transform from world space to the light probe model space. In light probe + // model space, the light probe is a 1×1×1 cube centered on the origin. + light_from_world: mat4x4, + // Whether this light probe contributes diffuse light to lightmapped meshes. + affects_lightmapped_mesh_diffuse: bool, +}; + +fn transpose_affine_matrix(matrix: mat3x4) -> mat4x4 { + let matrix4x4 = mat4x4( + matrix[0], + matrix[1], + matrix[2], + vec4(0.0, 0.0, 0.0, 1.0)); + return transpose(matrix4x4); +} + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + +// Searches for a light probe that contains the fragment. +// +// This is the version that's used when storage buffers are available and +// light probes are clustered. +// +// TODO: Interpolate between multiple light probes. +fn query_light_probe( + world_position: vec3, + is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, +) -> LightProbeQueryResult { + var result: LightProbeQueryResult; + result.texture_index = -1; + + // Reflection probe indices are followed by irradiance volume indices in the + // cluster index list. Use this fact to create our bracketing range of + // indices. + var start_offset: u32; + var end_offset: u32; + if is_irradiance_volume { + start_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + end_offset = (*clusterable_object_index_ranges).first_decal_offset; + } else { + start_offset = (*clusterable_object_index_ranges).first_reflection_probe_index_offset; + end_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + } + + for (var light_probe_index_offset: u32 = start_offset; + light_probe_index_offset < end_offset && result.texture_index < 0; + light_probe_index_offset += 1u) { + let light_probe_index = i32(clustered_forward::get_clusterable_object_id( + light_probe_index_offset)); + + var light_probe: LightProbe; + if is_irradiance_volume { + light_probe = light_probes.irradiance_volumes[light_probe_index]; + } else { + light_probe = light_probes.reflection_probes[light_probe_index]; + } + + // Unpack the inverse transform. + let light_from_world = + transpose_affine_matrix(light_probe.light_from_world_transposed); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (light_from_world * vec4(world_position, 1.0f)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5f))) { + result.texture_index = light_probe.cubemap_index; + result.intensity = light_probe.intensity; + result.light_from_world = light_from_world; + result.affects_lightmapped_mesh_diffuse = + light_probe.affects_lightmapped_mesh_diffuse != 0u; + break; + } + } + + return result; +} + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + +// Searches for a light probe that contains the fragment. +// +// This is the version that's used when storage buffers aren't available and +// light probes aren't clustered. It simply does a brute force search of all +// light probes. Because platforms without sufficient SSBO bindings typically +// lack bindless shaders, there will usually only be one of each type of light +// probe present anyway. +fn query_light_probe( + world_position: vec3, + is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, +) -> LightProbeQueryResult { + var result: LightProbeQueryResult; + result.texture_index = -1; + + var light_probe_count: i32; + if is_irradiance_volume { + light_probe_count = light_probes.irradiance_volume_count; + } else { + light_probe_count = light_probes.reflection_probe_count; + } + + for (var light_probe_index: i32 = 0; + light_probe_index < light_probe_count && result.texture_index < 0; + light_probe_index += 1) { + var light_probe: LightProbe; + if is_irradiance_volume { + light_probe = light_probes.irradiance_volumes[light_probe_index]; + } else { + light_probe = light_probes.reflection_probes[light_probe_index]; + } + + // Unpack the inverse transform. + let light_from_world = + transpose_affine_matrix(light_probe.light_from_world_transposed); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (light_from_world * vec4(world_position, 1.0f)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5f))) { + result.texture_index = light_probe.cubemap_index; + result.intensity = light_probe.intensity; + result.light_from_world = light_from_world; + result.affects_lightmapped_mesh_diffuse = + light_probe.affects_lightmapped_mesh_diffuse != 0u; + + // TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183 + // We can't use `break` here because of the ICE. + // So instead we rely on the fact that we set `result.texture_index` + // above and check its value in the `for` loop header before + // looping. + // break; + } + } + + return result; +} + +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 diff --git a/vendor/bevy_pbr/src/light_probe/mod.rs b/vendor/bevy_pbr/src/light_probe/mod.rs new file mode 100644 index 00000000..1e0088a8 --- /dev/null +++ b/vendor/bevy_pbr/src/light_probe/mod.rs @@ -0,0 +1,731 @@ +//! Light probes for baked global illumination. + +use bevy_app::{App, Plugin}; +use bevy_asset::AssetId; +use bevy_camera::{ + primitives::{Aabb, Frustum}, + Camera3d, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Local, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, LightProbe}; +use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + extract_instances::ExtractInstancesPlugin, + render_asset::RenderAssets, + render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView}, + renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}, + settings::WgpuFeatures, + sync_world::RenderEntity, + texture::{FallbackImage, GpuImage}, + view::ExtractedView, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_transform::{components::Transform, prelude::GlobalTransform}; +use tracing::error; + +use core::{hash::Hash, ops::Deref}; + +use crate::{ + generate::EnvironmentMapGenerationPlugin, light_probe::environment_map::EnvironmentMapIds, +}; + +pub mod environment_map; +pub mod generate; +pub mod irradiance_volume; + +/// The maximum number of each type of light probe that each view will consider. +/// +/// Because the fragment shader does a linear search through the list for each +/// fragment, this number needs to be relatively small. +pub const MAX_VIEW_LIGHT_PROBES: usize = 8; + +/// How many texture bindings are used in the fragment shader, *not* counting +/// environment maps or irradiance volumes. +const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16; + +/// Adds support for light probes: cuboid bounding regions that apply global +/// illumination to objects within them. +/// +/// This also adds support for view environment maps: diffuse and specular +/// cubemaps applied to all objects that a view renders. +pub struct LightProbePlugin; + +/// A GPU type that stores information about a light probe. +#[derive(Clone, Copy, ShaderType, Default)] +struct RenderLightProbe { + /// The transform from the world space to the model space. This is used to + /// efficiently check for bounding box intersection. + light_from_world_transposed: [Vec4; 3], + + /// The index of the texture or textures in the appropriate binding array or + /// arrays. + /// + /// For example, for reflection probes this is the index of the cubemap in + /// the diffuse and specular texture arrays. + texture_index: i32, + + /// Scale factor applied to the light generated by this light probe. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, + + /// Whether this light probe adds to the diffuse contribution of the + /// irradiance for meshes with lightmaps. + affects_lightmapped_mesh_diffuse: u32, +} + +/// A per-view shader uniform that specifies all the light probes that the view +/// takes into account. +#[derive(ShaderType)] +pub struct LightProbesUniform { + /// The list of applicable reflection probes, sorted from nearest to the + /// camera to the farthest away from the camera. + reflection_probes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + + /// The list of applicable irradiance volumes, sorted from nearest to the + /// camera to the farthest away from the camera. + irradiance_volumes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + + /// The number of reflection probes in the list. + reflection_probe_count: i32, + + /// The number of irradiance volumes in the list. + irradiance_volume_count: i32, + + /// The index of the diffuse and specular environment maps associated with + /// the view itself. This is used as a fallback if no reflection probe in + /// the list contains the fragment. + view_cubemap_index: i32, + + /// The smallest valid mipmap level for the specular environment cubemap + /// associated with the view. + smallest_specular_mip_level_for_view: u32, + + /// The intensity of the environment cubemap associated with the view. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity_for_view: f32, + + /// Whether the environment map attached to the view affects the diffuse + /// lighting for lightmapped meshes. + /// + /// This will be 1 if the map does affect lightmapped meshes or 0 otherwise. + view_environment_map_affects_lightmapped_mesh_diffuse: u32, +} + +/// A GPU buffer that stores information about all light probes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct LightProbesBuffer(DynamicUniformBuffer); + +/// A component attached to each camera in the render world that stores the +/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`]. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewLightProbesUniformOffset(u32); + +/// Information that [`gather_light_probes`] keeps about each light probe. +/// +/// This information is parameterized by the [`LightProbeComponent`] type. This +/// will either be [`EnvironmentMapLight`] for reflection probes or +/// [`IrradianceVolume`] for irradiance volumes. +struct LightProbeInfo +where + C: LightProbeComponent, +{ + // The transform from world space to light probe space. + // Stored as the transpose of the inverse transform to compress the structure + // on the GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it + // to recover the original inverse transform. + light_from_world: [Vec4; 3], + + // The transform from light probe space to world space. + world_from_light: Affine3A, + + // Scale factor applied to the diffuse and specular light generated by this + // reflection probe. + // + // See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, + + // Whether this light probe adds to the diffuse contribution of the + // irradiance for meshes with lightmaps. + affects_lightmapped_mesh_diffuse: bool, + + // The IDs of all assets associated with this light probe. + // + // Because each type of light probe component may reference different types + // of assets (e.g. a reflection probe references two cubemap assets while an + // irradiance volume references a single 3D texture asset), this is generic. + asset_id: C::AssetId, +} + +/// A component, part of the render world, that stores the mapping from asset ID +/// or IDs to the texture index in the appropriate binding arrays. +/// +/// Cubemap textures belonging to environment maps are collected into binding +/// arrays, and the index of each texture is presented to the shader for runtime +/// lookup. 3D textures belonging to reflection probes are likewise collected +/// into binding arrays, and the shader accesses the 3D texture by index. +/// +/// This component is attached to each view in the render world, because each +/// view may have a different set of light probes that it considers and therefore +/// the texture indices are per-view. +#[derive(Component, Default)] +pub struct RenderViewLightProbes +where + C: LightProbeComponent, +{ + /// The list of environment maps presented to the shader, in order. + binding_index_to_textures: Vec, + + /// The reverse of `binding_index_to_cubemap`: a map from the texture ID to + /// the index in `binding_index_to_cubemap`. + cubemap_to_binding_index: HashMap, + + /// Information about each light probe, ready for upload to the GPU, sorted + /// in order from closest to the camera to farthest. + /// + /// Note that this is not necessarily ordered by binding index. So don't + /// write code like + /// `render_light_probes[cubemap_to_binding_index[asset_id]]`; instead + /// search for the light probe with the appropriate binding index in this + /// array. + render_light_probes: Vec, + + /// Information needed to render the light probe attached directly to the + /// view, if applicable. + /// + /// A light probe attached directly to a view represents a "global" light + /// probe that affects all objects not in the bounding region of any light + /// probe. Currently, the only light probe type that supports this is the + /// [`EnvironmentMapLight`]. + view_light_probe_info: C::ViewLightProbeInfo, +} + +/// A trait implemented by all components that represent light probes. +/// +/// Currently, the two light probe types are [`EnvironmentMapLight`] and +/// [`IrradianceVolume`], for reflection probes and irradiance volumes +/// respectively. +/// +/// Most light probe systems are written to be generic over the type of light +/// probe. This allows much of the code to be shared and enables easy addition +/// of more light probe types (e.g. real-time reflection planes) in the future. +pub trait LightProbeComponent: Send + Sync + Component + Sized { + /// Holds [`AssetId`]s of the texture or textures that this light probe + /// references. + /// + /// This can just be [`AssetId`] if the light probe only references one + /// texture. If it references multiple textures, it will be a structure + /// containing those asset IDs. + type AssetId: Send + Sync + Clone + Eq + Hash; + + /// If the light probe can be attached to the view itself (as opposed to a + /// cuboid region within the scene), this contains the information that will + /// be passed to the GPU in order to render it. Otherwise, this will be + /// `()`. + /// + /// Currently, only reflection probes (i.e. [`EnvironmentMapLight`]) can be + /// attached directly to views. + type ViewLightProbeInfo: Send + Sync + Default; + + /// Returns the asset ID or asset IDs of the texture or textures referenced + /// by this light probe. + fn id(&self, image_assets: &RenderAssets) -> Option; + + /// Returns the intensity of this light probe. + /// + /// This is a scaling factor that will be multiplied by the value or values + /// sampled from the texture. + fn intensity(&self) -> f32; + + /// Returns true if this light probe contributes diffuse lighting to meshes + /// with lightmaps or false otherwise. + fn affects_lightmapped_mesh_diffuse(&self) -> bool; + + /// Creates an instance of [`RenderViewLightProbes`] containing all the + /// information needed to render this light probe. + /// + /// This is called for every light probe in view every frame. + fn create_render_view_light_probes( + view_component: Option<&Self>, + image_assets: &RenderAssets, + ) -> RenderViewLightProbes; +} + +/// The uniform struct extracted from [`EnvironmentMapLight`]. +/// Will be available for use in the Environment Map shader. +#[derive(Component, ShaderType, Clone)] +pub struct EnvironmentMapUniform { + /// The world space transformation matrix of the sample ray for environment cubemaps. + transform: Mat4, +} + +impl Default for EnvironmentMapUniform { + fn default() -> Self { + EnvironmentMapUniform { + transform: Mat4::IDENTITY, + } + } +} + +/// A GPU buffer that stores the environment map settings for each view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer); + +/// A component that stores the offset within the +/// [`EnvironmentMapUniformBuffer`] for each view. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewEnvironmentMapUniformOffset(u32); + +impl Plugin for LightProbePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "light_probe.wgsl"); + load_shader_library!(app, "environment_map.wgsl"); + load_shader_library!(app, "irradiance_volume.wgsl"); + + app.add_plugins(( + EnvironmentMapGenerationPlugin, + ExtractInstancesPlugin::::new(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, gather_environment_map_uniform) + .add_systems(ExtractSchedule, gather_light_probes::) + .add_systems(ExtractSchedule, gather_light_probes::) + .add_systems( + Render, + (upload_light_probes, prepare_environment_uniform_buffer) + .in_set(RenderSystems::PrepareResources), + ); + } +} + +/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them. +/// +/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance +/// if one does not already exist. +fn gather_environment_map_uniform( + view_query: Extract), With>>, + mut commands: Commands, +) { + for (view_entity, environment_map_light) in view_query.iter() { + let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { + EnvironmentMapUniform { + transform: Transform::from_rotation(environment_map_light.rotation) + .to_matrix() + .inverse(), + } + } else { + EnvironmentMapUniform::default() + }; + commands + .get_entity(view_entity) + .expect("Environment map light entity wasn't synced.") + .insert(environment_map_uniform); + } +} + +/// Gathers up all light probes of a single type in the scene and assigns them +/// to views, performing frustum culling and distance sorting in the process. +fn gather_light_probes( + image_assets: Res>, + light_probe_query: Extract>>, + view_query: Extract< + Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With>, + >, + mut reflection_probes: Local>>, + mut view_reflection_probes: Local>>, + mut commands: Commands, +) where + C: LightProbeComponent, +{ + // Create [`LightProbeInfo`] for every light probe in the scene. + reflection_probes.clear(); + reflection_probes.extend( + light_probe_query + .iter() + .filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)), + ); + // Build up the light probes uniform and the key table. + for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() { + // Cull light probes outside the view frustum. + view_reflection_probes.clear(); + view_reflection_probes.extend( + reflection_probes + .iter() + .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum)) + .cloned(), + ); + + // Sort by distance to camera. + view_reflection_probes.sort_by_cached_key(|light_probe_info| { + light_probe_info.camera_distance_sort_key(view_transform) + }); + + // Create the light probes list. + let mut render_view_light_probes = + C::create_render_view_light_probes(view_component, &image_assets); + + // Gather up the light probes in the list. + render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes); + + // Record the per-view light probes. + if render_view_light_probes.is_empty() { + commands + .get_entity(view_entity) + .expect("View entity wasn't synced.") + .remove::>(); + } else { + commands + .get_entity(view_entity) + .expect("View entity wasn't synced.") + .insert(render_view_light_probes); + } + } +} + +/// Gathers up environment map settings for each applicable view and +/// writes them into a GPU buffer. +pub fn prepare_environment_uniform_buffer( + mut commands: Commands, + views: Query<(Entity, Option<&EnvironmentMapUniform>), With>, + mut environment_uniform_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = + environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue) + else { + return; + }; + + for (view, environment_uniform) in views.iter() { + let uniform_offset = match environment_uniform { + None => 0, + Some(environment_uniform) => writer.write(environment_uniform), + }; + commands + .entity(view) + .insert(ViewEnvironmentMapUniformOffset(uniform_offset)); + } +} + +// A system that runs after [`gather_light_probes`] and populates the GPU +// uniforms with the results. +// +// Note that, unlike [`gather_light_probes`], this system is not generic over +// the type of light probe. It collects light probes of all types together into +// a single structure, ready to be passed to the shader. +fn upload_light_probes( + mut commands: Commands, + views: Query>, + mut light_probes_buffer: ResMut, + mut view_light_probes_query: Query<( + Option<&RenderViewLightProbes>, + Option<&RenderViewLightProbes>, + )>, + render_device: Res, + render_queue: Res, +) { + // If there are no views, bail. + if views.is_empty() { + return; + } + + // Initialize the uniform buffer writer. + let mut writer = light_probes_buffer + .get_writer(views.iter().len(), &render_device, &render_queue) + .unwrap(); + + // Process each view. + for view_entity in views.iter() { + let Ok((render_view_environment_maps, render_view_irradiance_volumes)) = + view_light_probes_query.get_mut(view_entity) + else { + error!("Failed to find `RenderViewLightProbes` for the view!"); + continue; + }; + + // Initialize the uniform with only the view environment map, if there + // is one. + let mut light_probes_uniform = LightProbesUniform { + reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + reflection_probe_count: render_view_environment_maps + .map(RenderViewLightProbes::len) + .unwrap_or_default() + .min(MAX_VIEW_LIGHT_PROBES) as i32, + irradiance_volume_count: render_view_irradiance_volumes + .map(RenderViewLightProbes::len) + .unwrap_or_default() + .min(MAX_VIEW_LIGHT_PROBES) as i32, + view_cubemap_index: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.cubemap_index) + .unwrap_or(-1), + smallest_specular_mip_level_for_view: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.smallest_specular_mip_level) + .unwrap_or(0), + intensity_for_view: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.intensity) + .unwrap_or(1.0), + view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32) + .unwrap_or(1), + }; + + // Add any environment maps that [`gather_light_probes`] found to the + // uniform. + if let Some(render_view_environment_maps) = render_view_environment_maps { + render_view_environment_maps.add_to_uniform( + &mut light_probes_uniform.reflection_probes, + &mut light_probes_uniform.reflection_probe_count, + ); + } + + // Add any irradiance volumes that [`gather_light_probes`] found to the + // uniform. + if let Some(render_view_irradiance_volumes) = render_view_irradiance_volumes { + render_view_irradiance_volumes.add_to_uniform( + &mut light_probes_uniform.irradiance_volumes, + &mut light_probes_uniform.irradiance_volume_count, + ); + } + + // Queue the view's uniforms to be written to the GPU. + let uniform_offset = writer.write(&light_probes_uniform); + + commands + .entity(view_entity) + .insert(ViewLightProbesUniformOffset(uniform_offset)); + } +} + +impl Default for LightProbesUniform { + fn default() -> Self { + Self { + reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + reflection_probe_count: 0, + irradiance_volume_count: 0, + view_cubemap_index: -1, + smallest_specular_mip_level_for_view: 0, + intensity_for_view: 1.0, + view_environment_map_affects_lightmapped_mesh_diffuse: 1, + } + } +} + +impl LightProbeInfo +where + C: LightProbeComponent, +{ + /// Given the set of light probe components, constructs and returns + /// [`LightProbeInfo`]. This is done for every light probe in the scene + /// every frame. + fn new( + (light_probe_transform, environment_map): (&GlobalTransform, &C), + image_assets: &RenderAssets, + ) -> Option> { + let light_from_world_transposed = + Mat4::from(light_probe_transform.affine().inverse()).transpose(); + environment_map.id(image_assets).map(|id| LightProbeInfo { + world_from_light: light_probe_transform.affine(), + light_from_world: [ + light_from_world_transposed.x_axis, + light_from_world_transposed.y_axis, + light_from_world_transposed.z_axis, + ], + asset_id: id, + intensity: environment_map.intensity(), + affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), + }) + } + + /// Returns true if this light probe is in the viewing frustum of the camera + /// or false if it isn't. + fn frustum_cull(&self, view_frustum: &Frustum) -> bool { + view_frustum.intersects_obb( + &Aabb { + center: Vec3A::default(), + half_extents: Vec3A::splat(0.5), + }, + &self.world_from_light, + true, + false, + ) + } + + /// Returns the squared distance from this light probe to the camera, + /// suitable for distance sorting. + fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd { + FloatOrd( + (self.world_from_light.translation - view_transform.translation_vec3a()) + .length_squared(), + ) + } +} + +impl RenderViewLightProbes +where + C: LightProbeComponent, +{ + /// Creates a new empty list of light probes. + fn new() -> RenderViewLightProbes { + RenderViewLightProbes { + binding_index_to_textures: vec![], + cubemap_to_binding_index: HashMap::default(), + render_light_probes: vec![], + view_light_probe_info: C::ViewLightProbeInfo::default(), + } + } + + /// Returns true if there are no light probes in the list. + pub(crate) fn is_empty(&self) -> bool { + self.binding_index_to_textures.is_empty() + } + + /// Returns the number of light probes in the list. + pub(crate) fn len(&self) -> usize { + self.binding_index_to_textures.len() + } + + /// Adds a cubemap to the list of bindings, if it wasn't there already, and + /// returns its index within that list. + pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &C::AssetId) -> u32 { + *self + .cubemap_to_binding_index + .entry((*cubemap_id).clone()) + .or_insert_with(|| { + let index = self.binding_index_to_textures.len() as u32; + self.binding_index_to_textures.push((*cubemap_id).clone()); + index + }) + } + + /// Adds all the light probes in this structure to the supplied array, which + /// is expected to be shipped to the GPU. + fn add_to_uniform( + &self, + render_light_probes: &mut [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + render_light_probe_count: &mut i32, + ) { + render_light_probes[0..self.render_light_probes.len()] + .copy_from_slice(&self.render_light_probes[..]); + *render_light_probe_count = self.render_light_probes.len() as i32; + } + + /// Gathers up all light probes of the given type in the scene and records + /// them in this structure. + fn maybe_gather_light_probes(&mut self, light_probes: &[LightProbeInfo]) { + for light_probe in light_probes.iter().take(MAX_VIEW_LIGHT_PROBES) { + // Determine the index of the cubemap in the binding array. + let cubemap_index = self.get_or_insert_cubemap(&light_probe.asset_id); + + // Write in the light probe data. + self.render_light_probes.push(RenderLightProbe { + light_from_world_transposed: light_probe.light_from_world, + texture_index: cubemap_index as i32, + intensity: light_probe.intensity, + affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse + as u32, + }); + } + } +} + +impl Clone for LightProbeInfo +where + C: LightProbeComponent, +{ + fn clone(&self) -> Self { + Self { + light_from_world: self.light_from_world, + world_from_light: self.world_from_light, + intensity: self.intensity, + affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse, + asset_id: self.asset_id.clone(), + } + } +} + +/// Adds a diffuse or specular texture view to the `texture_views` list, and +/// populates `sampler` if this is the first such view. +pub(crate) fn add_cubemap_texture_view<'a>( + texture_views: &mut Vec<&'a ::Target>, + sampler: &mut Option<&'a Sampler>, + image_id: AssetId, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, +) { + match images.get(image_id) { + None => { + // Use the fallback image if the cubemap isn't loaded yet. + texture_views.push(&*fallback_image.cube.texture_view); + } + Some(image) => { + // If this is the first texture view, populate `sampler`. + if sampler.is_none() { + *sampler = Some(&image.sampler); + } + + texture_views.push(&*image.texture_view); + } + } +} + +/// Many things can go wrong when attempting to use texture binding arrays +/// (a.k.a. bindless textures). This function checks for these pitfalls: +/// +/// 1. If GLSL support is enabled at the feature level, then in debug mode +/// `naga_oil` will attempt to compile all shader modules under GLSL to check +/// validity of names, even if GLSL isn't actually used. This will cause a crash +/// if binding arrays are enabled, because binding arrays are currently +/// unimplemented in the GLSL backend of Naga. Therefore, we disable binding +/// arrays if the `shader_format_glsl` feature is present. +/// +/// 2. If there aren't enough texture bindings available to accommodate all the +/// binding arrays, the driver will panic. So we also bail out if there aren't +/// enough texture bindings available in the fragment shader. +/// +/// 3. If binding arrays aren't supported on the hardware, then we obviously +/// can't use them. Adreno <= 610 claims to support bindless, but seems to be +/// too buggy to be usable. +/// +/// 4. If binding arrays are supported on the hardware, but they can only be +/// accessed by uniform indices, that's not good enough, and we bail out. +/// +/// If binding arrays aren't usable, we disable reflection probes and limit the +/// number of irradiance volumes in the scene to 1. +pub(crate) fn binding_arrays_are_usable( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> bool { + let adapter_info = RenderAdapterInfo(WgpuWrapper::new(render_adapter.get_info())); + + !cfg!(feature = "shader_format_glsl") + && bevy_render::get_adreno_model(&adapter_info).is_none_or(|model| model > 610) + && render_device.limits().max_storage_textures_per_shader_stage + >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES) + as u32 + && render_device.features().contains( + WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + ) +} diff --git a/vendor/bevy_pbr/src/lightmap/cache_tests.rs b/vendor/bevy_pbr/src/lightmap/cache_tests.rs new file mode 100644 index 00000000..f26299b1 --- /dev/null +++ b/vendor/bevy_pbr/src/lightmap/cache_tests.rs @@ -0,0 +1,181 @@ +//! GPU-backed regression test for the local lightmap binding cache. +use super::*; +use bevy_render::{render_resource::*, renderer::initialize_renderer, settings::*}; +use std::{ + future::Future, + sync::Arc, + task::{Context, Poll, Wake, Waker}, +}; + +fn block_on(future: impl Future) -> T { + struct ThreadWake(std::thread::Thread); + impl Wake for ThreadWake { + fn wake(self: Arc) { + self.0.unpark(); + } + } + let waker = Waker::from(Arc::new(ThreadWake(std::thread::current()))); + let mut context = Context::from_waker(&waker); + let mut future = std::pin::pin!(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(value) => return value, + Poll::Pending => std::thread::park(), + } + } +} + +#[test] +#[ignore = "requires a Vulkan GPU; run explicitly with --ignored"] +fn cache_tracks_binding_identity_and_texture_replacement() { + let resources = block_on(initialize_renderer( + Backends::VULKAN, + None, + &WgpuSettings { + instance_flags: InstanceFlags::empty(), + ..Default::default() + }, + )); + let device = resources.0; + let queue = resources.1; + let buffer = || { + device.create_buffer(&BufferDescriptor { + label: None, + size: 1024, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + }; + let a = buffer(); + let b = buffer(); + fn binding(buffer: &Buffer, offset: u64, size: u64) -> BindingResource<'_> { + BindingResource::Buffer(BufferBinding { + buffer, + offset, + size: BufferSize::new(size), + }) + } + let layout = device.create_bind_group_layout(None, &[]); + let different_layout = device.create_bind_group_layout(None, &[]); + let group = device.create_bind_group(None, &layout, &[]); + let extent = Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }; + let texture = device.create_texture(&TextureDescriptor { + label: None, + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let image = GpuImage { + texture_view: texture.create_view(&TextureViewDescriptor::default()), + texture, + texture_format: TextureFormat::Rgba8Unorm, + texture_view_format: None, + sampler: device.create_sampler(&SamplerDescriptor::default()), + size: extent, + mip_level_count: 1, + had_data: false, + }; + let fallback = FallbackImage { + d1: image.clone(), + d2: image.clone(), + d2_array: image.clone(), + cube: image.clone(), + cube_array: image.clone(), + d3: image.clone(), + }; + // Exercise both single-texture and binding-array slab storage. + for bindless in [false, true] { + let mut slab = LightmapSlab::new(&fallback, bindless); + slab.cache_bind_group(&binding(&a, 0, 256), layout.id(), group.clone()); + assert_eq!( + slab.cached_bind_group(&binding(&a.clone(), 0, 256), layout.id()) + .unwrap() + .id(), + group.id() + ); + // Updating buffer contents must keep the binding valid. + queue.write_buffer(&a, 0, &[0; 16]); + assert!( + slab.cached_bind_group(&binding(&a, 0, 256), layout.id()) + .is_some() + ); + assert!( + slab.cached_bind_group(&binding(&b, 0, 256), layout.id()) + .is_none() + ); + assert!( + slab.cached_bind_group(&binding(&a, 256, 256), layout.id()) + .is_none() + ); + assert!( + slab.cached_bind_group(&binding(&a, 0, 512), layout.id()) + .is_none() + ); + assert!( + slab.cached_bind_group(&binding(&a, 0, 256), different_layout.id()) + .is_none() + ); + slab.insert(0u32.into(), image.clone()); + assert!( + slab.cached_bind_group(&binding(&a, 0, 256), layout.id()) + .is_none() + ); + slab.cache_bind_group(&binding(&a, 0, 256), layout.id(), group.clone()); + slab.remove(&fallback, 0u32.into()); + assert!( + slab.cached_bind_group(&binding(&a, 0, 256), layout.id()) + .is_none() + ); + // Changing buffers repeatedly must not retain an unbounded history. + for _ in 0..32 { + slab.cache_bind_group(&binding(&buffer(), 0, 256), layout.id(), group.clone()); + } + assert_eq!(slab.cached_bind_groups.len(), 8); + } + // Exercise the real binding constructor, not only cache lookups. + let adapter = resources.3; + let bindless = binding_arrays_are_usable(&device, &adapter); + let layouts = crate::render::MeshLayouts::new(&device, &adapter); + let pipeline_cache = PipelineCache::new(device.clone(), adapter, true); + let mut slab = LightmapSlab::new(&fallback, bindless); + let first = layouts.lightmapped( + &device, + &pipeline_cache, + &binding(&a, 0, 256), + &mut slab, + bindless, + ); + let reused = layouts.lightmapped( + &device, + &pipeline_cache, + &binding(&a, 0, 256), + &mut slab, + bindless, + ); + assert_eq!(first.id(), reused.id()); + let resized = layouts.lightmapped( + &device, + &pipeline_cache, + &binding(&b, 0, 256), + &mut slab, + bindless, + ); + assert_ne!(first.id(), resized.id()); + slab.insert(0u32.into(), image); + let replaced = layouts.lightmapped( + &device, + &pipeline_cache, + &binding(&a, 0, 256), + &mut slab, + bindless, + ); + assert_ne!(first.id(), replaced.id()); +} diff --git a/vendor/bevy_pbr/src/lightmap/lightmap.wgsl b/vendor/bevy_pbr/src/lightmap/lightmap.wgsl new file mode 100644 index 00000000..4ba6f51b --- /dev/null +++ b/vendor/bevy_pbr/src/lightmap/lightmap.wgsl @@ -0,0 +1,99 @@ +#define_import_path bevy_pbr::lightmap + +#import bevy_pbr::mesh_bindings::mesh + +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY +@group(2) @binding(4) var lightmaps_textures: binding_array, 4>; +@group(2) @binding(5) var lightmaps_samplers: binding_array; +#else // MULTIPLE_LIGHTMAPS_IN_ARRAY +@group(2) @binding(4) var lightmaps_texture: texture_2d; +@group(2) @binding(5) var lightmaps_sampler: sampler; +#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY + +// Samples the lightmap, if any, and returns indirect illumination from it. +fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { + let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; + let uv_rect = vec4( + unpack2x16unorm(packed_uv_rect.x), + unpack2x16unorm(packed_uv_rect.y), + ); + let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv); + let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; + + // Bicubic 4-tap + // https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering + // https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2 +#ifdef LIGHTMAP_BICUBIC_SAMPLING + let texture_size = vec2(lightmap_size(lightmap_slot)); + let texel_size = 1.0 / texture_size; + let puv = lightmap_uv * texture_size + 0.5; + let iuv = floor(puv); + let fuv = fract(puv); + let g0x = g0(fuv.x); + let g1x = g1(fuv.x); + let h0x = h0_approx(fuv.x); + let h1x = h1_approx(fuv.x); + let h0y = h0_approx(fuv.y); + let h1y = h1_approx(fuv.y); + let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size; + let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size; + let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size; + let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size; + let color = g0(fuv.y) * (g0x * sample(p0, lightmap_slot) + g1x * sample(p1, lightmap_slot)) + g1(fuv.y) * (g0x * sample(p2, lightmap_slot) + g1x * sample(p3, lightmap_slot)); +#else + let color = sample(lightmap_uv, lightmap_slot); +#endif + + return color * exposure; +} + +fn lightmap_size(lightmap_slot: u32) -> vec2 { +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureDimensions(lightmaps_textures[lightmap_slot]); +#else + return textureDimensions(lightmaps_texture); +#endif +} + +fn sample(uv: vec2, lightmap_slot: u32) -> vec3 { + // Mipmapping lightmaps is usually a bad idea due to leaking across UV + // islands, so there's no harm in using mip level 0 and it lets us avoid + // control flow uniformity problems. +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb; +#else + return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb; +#endif +} + +fn w0(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0); +} + +fn w1(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0); +} + +fn w2(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0); +} + +fn w3(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * a); +} + +fn g0(a: f32) -> f32 { + return w0(a) + w1(a); +} + +fn g1(a: f32) -> f32 { + return w2(a) + w3(a); +} + +fn h0_approx(a: f32) -> f32 { + return -0.2 - a * (0.24 * a - 0.44); +} + +fn h1_approx(a: f32) -> f32 { + return 1.0 + a * (0.24 * a - 0.04); +} diff --git a/vendor/bevy_pbr/src/lightmap/mod.rs b/vendor/bevy_pbr/src/lightmap/mod.rs new file mode 100644 index 00000000..ede5763f --- /dev/null +++ b/vendor/bevy_pbr/src/lightmap/mod.rs @@ -0,0 +1,585 @@ +//! Lightmaps, baked lighting textures that can be applied at runtime to provide +//! diffuse global illumination. +//! +//! Bevy doesn't currently have any way to actually bake lightmaps, but they can +//! be baked in an external tool like [Blender](http://blender.org), for example +//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`] +//! project support other lightmap baking methods. +//! +//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a +//! [`MeshMaterial3d`], Bevy applies the lightmap when rendering. The brightness +//! of the lightmap may be controlled with the `lightmap_exposure` field on +//! [`StandardMaterial`]. +//! +//! During the rendering extraction phase, we extract all lightmaps into the +//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup +//! and mesh uniform creation consults this table to determine which lightmap to +//! supply to the shader. Essentially, the lightmap is a special type of texture +//! that is part of the mesh instance rather than part of the material (because +//! multiple meshes can share the same material, whereas sharing lightmaps is +//! nonsensical). +//! +//! Note that multiple meshes can't be drawn in a single drawcall if they use +//! different lightmap textures, unless bindless textures are in use. If you +//! want to instance a lightmapped mesh, and your platform doesn't support +//! bindless textures, combine the lightmap textures into a single atlas, and +//! set the `uv_rect` field on [`Lightmap`] appropriately. +//! +//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper +//! [`Mesh3d`]: bevy_mesh::Mesh3d +//! [`MeshMaterial3d`]: crate::StandardMaterial +//! [`StandardMaterial`]: crate::StandardMaterial +//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi + +use bevy_app::{App, Plugin}; +use bevy_asset::{AssetId, Handle}; +use bevy_camera::visibility::ViewVisibility; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + lifecycle::RemovedComponents, + query::{Changed, Or}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_math::{Rect, UVec2, uvec2, vec4}; +use bevy_platform::collections::HashSet; +use bevy_reflect::{Reflect, std_traits::ReflectDefault}; +use bevy_render::{ + Extract, ExtractSchedule, RenderApp, RenderStartup, + render_asset::RenderAssets, + render_resource::{ + BindGroup, BindGroupLayoutId, BindingResource, Buffer, BufferSize, Sampler, TextureView, + WgpuSampler, WgpuTextureView, + }, + renderer::RenderAdapter, + sync_world::MainEntity, + texture::{FallbackImage, GpuImage}, +}; +use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap}; +use bevy_shader::load_shader_library; +use bevy_utils::default; +use fixedbitset::FixedBitSet; +use nonmax::{NonMaxU16, NonMaxU32}; +use tracing::error; + +use crate::{MeshExtractionSystems, binding_arrays_are_usable}; + +#[cfg(test)] +mod cache_tests; + +/// The number of lightmaps that we store in a single slab, if bindless textures +/// are in use. +/// +/// If bindless textures aren't in use, then only a single lightmap can be bound +/// at a time. +pub const LIGHTMAPS_PER_SLAB: usize = 4; + +/// A plugin that provides an implementation of lightmaps. +pub struct LightmapPlugin; + +/// A component that applies baked indirect diffuse global illumination from a +/// lightmap. +/// +/// When assigned to an entity that contains a [`Mesh3d`](bevy_mesh::Mesh3d) and a +/// [`MeshMaterial3d`](crate::StandardMaterial), if the mesh +/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_mesh::Mesh::ATTRIBUTE_UV_1)), +/// then the lightmap will render using those UVs. +#[derive(Component, Clone, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct Lightmap { + /// The lightmap texture. + pub image: Handle, + + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + /// + /// This field allows lightmaps for a variety of meshes to be packed into a + /// single atlas. + pub uv_rect: Rect, + + /// Whether bicubic sampling should be used for sampling this lightmap. + /// + /// Bicubic sampling is higher quality, but slower, and may lead to light leaks. + /// + /// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`]. + pub bicubic_sampling: bool, +} + +/// Lightmap data stored in the render world. +/// +/// There is one of these per visible lightmapped mesh instance. +#[derive(Debug)] +pub(crate) struct RenderLightmap { + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + pub(crate) uv_rect: Rect, + + /// The index of the slab (i.e. binding array) in which the lightmap is + /// located. + pub(crate) slab_index: LightmapSlabIndex, + + /// The index of the slot (i.e. element within the binding array) in which + /// the lightmap is located. + /// + /// If bindless lightmaps aren't in use, this will be 0. + pub(crate) slot_index: LightmapSlotIndex, + + // Whether or not bicubic sampling should be used for this lightmap. + pub(crate) bicubic_sampling: bool, +} + +/// Stores data for all lightmaps in the render world. +/// +/// This is cleared and repopulated each frame during the `extract_lightmaps` +/// system. +#[derive(Resource)] +pub struct RenderLightmaps { + /// The mapping from every lightmapped entity to its lightmap info. + /// + /// Entities without lightmaps, or for which the mesh or lightmap isn't + /// loaded, won't have entries in this table. + pub(crate) render_lightmaps: MainEntityHashMap, + + /// The slabs (binding arrays) containing the lightmaps. + pub(crate) slabs: Vec, + + free_slabs: FixedBitSet, + + pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>, + + /// Whether bindless textures are supported on this platform. + pub(crate) bindless_supported: bool, +} + +/// A binding array that contains lightmaps. +/// +/// This will have a single binding if bindless lightmaps aren't in use. +pub struct LightmapSlab { + /// The GPU images in this slab. + lightmaps: Vec, + free_slots_bitmask: u32, + // A slab's textures are static between insert/remove calls. The model + // buffer contents may change without changing its binding identity. + cached_bind_groups: Vec, +} + +struct CachedLightmapBindGroup { + model: Buffer, + offset: u64, + size: Option, + layout: BindGroupLayoutId, + bind_group: BindGroup, +} + +struct AllocatedLightmap { + gpu_image: GpuImage, + // This will only be present if the lightmap is allocated but not loaded. + asset_id: Option>, +} + +/// The index of the slab (binding array) in which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlabIndex(pub(crate) NonMaxU32); + +/// The index of the slot (element within the binding array) in the slab in +/// which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlotIndex(pub(crate) NonMaxU16); + +impl Plugin for LightmapPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "lightmap.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .add_systems(RenderStartup, init_render_lightmaps) + .add_systems( + ExtractSchedule, + extract_lightmaps.after(MeshExtractionSystems), + ); + } +} + +/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`] +/// resource. +fn extract_lightmaps( + render_lightmaps: ResMut, + changed_lightmaps_query: Extract< + Query< + (Entity, &ViewVisibility, &Lightmap), + Or<(Changed, Changed)>, + >, + >, + mut removed_lightmaps_query: Extract>, + images: Res>, + fallback_images: Res, +) { + let render_lightmaps = render_lightmaps.into_inner(); + + // Loop over each entity. + for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() { + if render_lightmaps + .render_lightmaps + .contains_key(&MainEntity::from(entity)) + { + continue; + } + + // Only process visible entities. + if !view_visibility.get() { + continue; + } + + let (slab_index, slot_index) = + render_lightmaps.allocate(&fallback_images, lightmap.image.id()); + render_lightmaps.render_lightmaps.insert( + entity.into(), + RenderLightmap::new( + lightmap.uv_rect, + slab_index, + slot_index, + lightmap.bicubic_sampling, + ), + ); + + render_lightmaps + .pending_lightmaps + .insert((slab_index, slot_index)); + } + + for entity in removed_lightmaps_query.read() { + if changed_lightmaps_query.contains(entity) { + continue; + } + + let Some(RenderLightmap { + slab_index, + slot_index, + .. + }) = render_lightmaps + .render_lightmaps + .remove(&MainEntity::from(entity)) + else { + continue; + }; + + render_lightmaps.remove(&fallback_images, slab_index, slot_index); + render_lightmaps + .pending_lightmaps + .remove(&(slab_index, slot_index)); + } + + render_lightmaps + .pending_lightmaps + .retain(|&(slab_index, slot_index)| { + let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps + [usize::from(slot_index)] + .asset_id + else { + error!( + "Allocated lightmap should have been removed from `pending_lightmaps` by now" + ); + return false; + }; + + let Some(gpu_image) = images.get(asset_id) else { + return true; + }; + render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone()); + false + }); +} + +impl RenderLightmap { + /// Creates a new lightmap from a texture, a UV rect, and a slab and slot + /// index pair. + fn new( + uv_rect: Rect, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + bicubic_sampling: bool, + ) -> Self { + Self { + uv_rect, + slab_index, + slot_index, + bicubic_sampling, + } + } +} + +/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers). +pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option) -> UVec2 { + match maybe_rect { + Some(rect) => { + let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0) + .round() + .as_uvec4(); + uvec2( + rect_uvec4.x | (rect_uvec4.y << 16), + rect_uvec4.z | (rect_uvec4.w << 16), + ) + } + None => UVec2::ZERO, + } +} + +impl Default for Lightmap { + fn default() -> Self { + Self { + image: Default::default(), + uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), + bicubic_sampling: false, + } + } +} + +pub fn init_render_lightmaps( + mut commands: Commands, + render_device: Res, + render_adapter: Res, +) { + let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter); + + commands.insert_resource(RenderLightmaps { + render_lightmaps: default(), + slabs: vec![], + free_slabs: FixedBitSet::new(), + pending_lightmaps: default(), + bindless_supported, + }); +} + +impl RenderLightmaps { + /// Creates a new slab, appends it to the end of the list, and returns its + /// slab index. + fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex { + let slab_index = LightmapSlabIndex::from(self.slabs.len()); + self.free_slabs.grow_and_insert(slab_index.into()); + self.slabs + .push(LightmapSlab::new(fallback_images, self.bindless_supported)); + slab_index + } + + fn allocate( + &mut self, + fallback_images: &FallbackImage, + image_id: AssetId, + ) -> (LightmapSlabIndex, LightmapSlotIndex) { + let slab_index = match self.free_slabs.minimum() { + None => self.create_slab(fallback_images), + Some(slab_index) => slab_index.into(), + }; + + let slab = &mut self.slabs[usize::from(slab_index)]; + let slot_index = slab.allocate(image_id); + if slab.is_full() { + self.free_slabs.remove(slab_index.into()); + } + + (slab_index, slot_index) + } + + fn remove( + &mut self, + fallback_images: &FallbackImage, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + ) { + let slab = &mut self.slabs[usize::from(slab_index)]; + slab.remove(fallback_images, slot_index); + + if !slab.is_full() { + self.free_slabs.grow_and_insert(slab_index.into()); + } + } +} + +impl LightmapSlab { + fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab { + let count = if bindless_supported { + LIGHTMAPS_PER_SLAB + } else { + 1 + }; + + LightmapSlab { + lightmaps: (0..count) + .map(|_| AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }) + .collect(), + free_slots_bitmask: (1 << count) - 1, + cached_bind_groups: Vec::new(), + } + } + + fn is_full(&self) -> bool { + self.free_slots_bitmask == 0 + } + + fn allocate(&mut self, image_id: AssetId) -> LightmapSlotIndex { + assert!( + !self.is_full(), + "Attempting to allocate on a full lightmap slab" + ); + let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros()); + self.free_slots_bitmask &= !(1 << u32::from(index)); + self.lightmaps[usize::from(index)].asset_id = Some(image_id); + index + } + + fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) { + self.cached_bind_groups.clear(); + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image, + asset_id: None, + } + } + + fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) { + self.cached_bind_groups.clear(); + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }; + self.free_slots_bitmask |= 1 << u32::from(index); + } + + /// Returns the texture views and samplers for the lightmaps in this slab, + /// ready to be placed into a bind group. + /// + /// This is used when constructing bind groups in bindless mode. Before + /// returning, this function pads out the arrays with fallback images in + /// order to fulfill requirements of platforms that require full binding + /// arrays (e.g. DX12). + pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) { + ( + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view) + .collect(), + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler) + .collect(), + ) + } + + /// Returns the texture view and sampler corresponding to the first + /// lightmap, which must exist. + /// + /// This is used when constructing bind groups in non-bindless mode. + pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) { + ( + &self.lightmaps[0].gpu_image.texture_view, + &self.lightmaps[0].gpu_image.sampler, + ) + } + + pub(crate) fn cached_bind_group( + &self, + model: &BindingResource, + layout: BindGroupLayoutId, + ) -> Option { + let BindingResource::Buffer(model) = model else { + return None; + }; + self.cached_bind_groups + .iter() + .find(|cached| { + &*cached.model == model.buffer + && cached.offset == model.offset + && cached.size == model.size + && cached.layout == layout + }) + .map(|cached| cached.bind_group.clone()) + } + + pub(crate) fn cache_bind_group( + &mut self, + model: &BindingResource, + layout: BindGroupLayoutId, + bind_group: BindGroup, + ) { + let BindingResource::Buffer(model) = model else { + return; + }; + // Bound retained buffers even when phases disappear or buffers resize. + // Eviction only rebuilds a binding; it cannot change rendering. + if self.cached_bind_groups.len() == 8 { + self.cached_bind_groups.remove(0); + } + self.cached_bind_groups.push(CachedLightmapBindGroup { + model: Buffer::from(model.buffer.clone()), + offset: model.offset, + size: model.size, + layout, + bind_group, + }); + } +} + +impl From for LightmapSlabIndex { + fn from(value: u32) -> Self { + Self(NonMaxU32::new(value).unwrap()) + } +} + +impl From for LightmapSlabIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for LightmapSlotIndex { + fn from(value: u32) -> Self { + Self(NonMaxU16::new(value as u16).unwrap()) + } +} + +impl From for LightmapSlotIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for usize { + fn from(value: LightmapSlabIndex) -> Self { + value.0.get() as usize + } +} + +impl From for usize { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as usize + } +} + +impl From for u16 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() + } +} + +impl From for u32 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as u32 + } +} diff --git a/vendor/bevy_pbr/src/material.rs b/vendor/bevy_pbr/src/material.rs new file mode 100644 index 00000000..42717b57 --- /dev/null +++ b/vendor/bevy_pbr/src/material.rs @@ -0,0 +1,1937 @@ +use crate::material_bind_groups::{ + FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, +}; +use crate::*; +use alloc::sync::Arc; +use bevy_asset::prelude::AssetChanged; +use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; +use bevy_camera::visibility::ViewVisibility; +use bevy_camera::ScreenSpaceTransmissionQuality; +use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; +use bevy_core_pipeline::prepass::{AlphaMask3dPrepass, Opaque3dPrepass}; +use bevy_core_pipeline::{ + core_3d::{ + AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, Transmissive3d, Transparent3d, + }, + prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}, + tonemapping::Tonemapping, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::change_detection::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{ + prelude::*, + system::{ + lifetimeless::{SRes, SResMut}, + SystemParamItem, + }, +}; +use bevy_mesh::{ + mark_3d_meshes_as_changed_if_their_assets_changed, Mesh3d, MeshVertexBufferLayoutRef, +}; +use bevy_platform::collections::hash_map::Entry; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::hash::FixedHasher; +use bevy_reflect::std_traits::ReflectDefault; +use bevy_reflect::Reflect; +use bevy_render::camera::extract_cameras; +use bevy_render::erased_render_asset::{ + ErasedRenderAsset, ErasedRenderAssetPlugin, ErasedRenderAssets, PrepareAssetError, +}; +use bevy_render::render_asset::{prepare_assets, RenderAssets}; +use bevy_render::renderer::RenderQueue; +use bevy_render::RenderStartup; +use bevy_render::{ + batching::gpu_preprocessing::GpuPreprocessingSupport, + extract_resource::ExtractResource, + mesh::RenderMesh, + prelude::*, + render_phase::*, + render_resource::*, + renderer::RenderDevice, + sync_world::MainEntity, + view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity}, + Extract, +}; +use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; +use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; +use bevy_shader::{Shader, ShaderDefVal}; +use bevy_utils::Parallel; +use core::any::{Any, TypeId}; +use core::hash::{BuildHasher, Hasher}; +use core::{hash::Hash, marker::PhantomData}; +use smallvec::SmallVec; +use tracing::error; + +pub const MATERIAL_BIND_GROUP_INDEX: usize = 3; + +/// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] +/// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level +/// way to render [`Mesh3d`] entities with custom shader logic. +/// +/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// # Example +/// +/// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; +/// # use bevy_reflect::TypePath; +/// # use bevy_mesh::{Mesh, Mesh3d}; +/// # use bevy_render::render_resource::AsBindGroup; +/// # use bevy_shader::ShaderRef; +/// # use bevy_color::LinearRgba; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: LinearRgba, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `Material` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl Material for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity with a mesh using `CustomMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// asset_server: Res +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(CustomMaterial { +/// color: RED.into(), +/// color_texture: asset_server.load("some_image.png"), +/// })), +/// )); +/// } +/// ``` +/// +/// In WGSL shaders, the material's binding would look like this: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var color: vec4; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(1) var color_texture: texture_2d; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(2) var color_sampler: sampler; +/// ``` +pub trait Material: Asset + AsBindGroup + Clone + Sized { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader + /// will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. + #[inline] + fn alpha_mode(&self) -> AlphaMode { + AlphaMode::Opaque + } + + /// Returns if this material should be rendered by the deferred or forward renderer. + /// for `AlphaMode::Opaque` or `AlphaMode::Mask` materials. + /// If `OpaqueRendererMethod::Auto`, it will default to what is selected in the `DefaultOpaqueRendererMethod` resource. + #[inline] + fn opaque_render_method(&self) -> OpaqueRendererMethod { + OpaqueRendererMethod::Forward + } + + #[inline] + /// Add a bias to the view depth of the mesh which can be used to force a specific render order. + /// for meshes with similar depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. + fn depth_bias(&self) -> f32 { + 0.0 + } + + #[inline] + /// Returns whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + fn reads_view_transmission_texture(&self) -> bool { + false + } + + /// Controls if the prepass is enabled for the Material. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + #[inline] + fn enable_prepass() -> bool { + true + } + + /// Controls if shadows are enabled for the Material. + #[inline] + fn enable_shadows() -> bool { + true + } + + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader + /// will be used. + /// + /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps + /// required for shadow mapping. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader + /// will be used. + /// + /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps + /// required for shadow mapping. + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the default deferred vertex shader + /// will be used. + fn deferred_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred fragment shader. If [`ShaderRef::Default`] is returned, the default deferred fragment shader + /// will be used. + fn deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's + /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + Ok(()) + } +} + +#[derive(Default)] +pub struct MaterialsPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for MaterialsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((PrepassPipelinePlugin, PrepassPlugin::new(self.debug_flags))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems(RenderStartup, init_material_pipeline) + .add_systems( + Render, + ( + specialize_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ) + .add_systems( + Render, + ( + prepare_material_bind_groups, + write_material_bind_group_buffers, + ) + .chain() + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSystems::PrepareAssets), + // specialize_shadows also needs to run after prepare_assets::, + // which is fine since ManageViews is after PrepareAssets + specialize_shadows + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + queue_shadows.in_set(RenderSystems::QueueMeshes), + ), + ); + } + } +} + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] +/// asset type. +pub struct MaterialPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, + pub _marker: PhantomData, +} + +impl Default for MaterialPlugin { + fn default() -> Self { + Self { + debug_flags: RenderDebugFlags::default(), + _marker: Default::default(), + } + } +} + +impl Plugin for MaterialPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut App) { + app.init_asset::() + .register_type::>() + .init_resource::>() + .add_plugins((ErasedRenderAssetPlugin::>::default(),)) + .add_systems( + PostUpdate, + ( + mark_meshes_as_changed_if_their_materials_changed::.ambiguous_with_all(), + check_entities_needing_specialization::.after(AssetEventSystems), + ) + .after(mark_3d_meshes_as_changed_if_their_assets_changed), + ); + + if M::enable_shadows() { + app.add_systems( + PostUpdate, + check_light_entities_needing_specialization:: + .after(check_entities_needing_specialization::), + ); + } + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_systems(RenderStartup, add_material_bind_group_allocator::) + .add_systems( + ExtractSchedule, + ( + extract_mesh_materials::.in_set(MaterialExtractionSystems), + early_sweep_material_instances:: + .after(MaterialExtractionSystems) + .before(late_sweep_material_instances), + // See the comments in + // `sweep_entities_needing_specialization` for an + // explanation of why the systems are ordered this way. + extract_entities_needs_specialization:: + .in_set(MaterialExtractEntitiesNeedingSpecializationSystems), + sweep_entities_needing_specialization:: + .after(MaterialExtractEntitiesNeedingSpecializationSystems) + .after(MaterialExtractionSystems) + .after(extract_cameras) + .before(late_sweep_material_instances), + ), + ); + } + } +} + +fn add_material_bind_group_allocator( + render_device: Res, + mut bind_group_allocators: ResMut, +) { + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + M::label(), + material_uses_bindless_resources::(&render_device) + .then(|| M::bindless_descriptor()) + .flatten(), + M::bind_group_layout_descriptor(&render_device), + M::bindless_slot_count(), + ), + ); +} + +/// A dummy [`AssetId`] that we use as a placeholder whenever a mesh doesn't +/// have a material. +/// +/// See the comments in [`RenderMaterialInstances::mesh_material`] for more +/// information. +pub(crate) static DUMMY_MESH_MATERIAL: AssetId = + AssetId::::invalid(); + +/// A key uniquely identifying a specialized [`MaterialPipeline`]. +pub struct MaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub bind_group_data: M::Data, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ErasedMaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub material_key: ErasedMaterialKey, + pub type_id: TypeId, +} + +/// Render pipeline data for a given [`Material`]. +#[derive(Resource, Clone)] +pub struct MaterialPipeline { + pub mesh_pipeline: MeshPipeline, +} + +pub struct MaterialPipelineSpecializer { + pub(crate) pipeline: MaterialPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for MaterialPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self + .pipeline + .mesh_pipeline + .specialize(key.mesh_key, layout)?; + descriptor.vertex.shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + MATERIAL_BIND_GROUP_INDEX as u32, + )); + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + MATERIAL_BIND_GROUP_INDEX as u32, + )); + }; + if let Some(vertex_shader) = self.properties.get_shader(MaterialVertexShader) { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = self.properties.get_shader(MaterialFragmentShader) { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + + descriptor + .layout + .insert(3, self.properties.material_layout.as_ref().unwrap().clone()); + + if let Some(specialize) = self.properties.specialize { + specialize(&self.pipeline, &mut descriptor, layout, key)?; + } + + // If bindless mode is on, add a `BINDLESS` define. + if self.properties.bindless { + descriptor.vertex.shader_defs.push("BINDLESS".into()); + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push("BINDLESS".into()); + } + } + + Ok(descriptor) + } +} + +pub fn init_material_pipeline(mut commands: Commands, mesh_pipeline: Res) { + commands.insert_resource(MaterialPipeline { + mesh_pipeline: mesh_pipeline.clone(), + }); +} + +pub type DrawMaterial = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup, + DrawMesh, +); + +/// Sets the bind group for a given [`Material`] at the configured `I` index. +pub struct SetMaterialBindGroup; +impl RenderCommand

for SetMaterialBindGroup { + type Param = ( + SRes>, + SRes, + SRes, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (materials, material_instances, material_bind_group_allocator): SystemParamItem< + 'w, + '_, + Self::Param, + >, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let materials = materials.into_inner(); + let material_instances = material_instances.into_inner(); + let material_bind_group_allocators = material_bind_group_allocator.into_inner(); + + let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { + return RenderCommandResult::Skip; + }; + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_instance.asset_id.type_id()) + else { + return RenderCommandResult::Skip; + }; + let Some(material) = materials.get(material_instance.asset_id) else { + return RenderCommandResult::Skip; + }; + let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) + else { + return RenderCommandResult::Skip; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group(I, bind_group, &[]); + RenderCommandResult::Success + } +} + +/// Stores all extracted instances of all [`Material`]s in the render world. +#[derive(Resource, Default)] +pub struct RenderMaterialInstances { + /// Maps from each entity in the main world to the + /// [`RenderMaterialInstance`] associated with it. + pub instances: MainEntityHashMap, + /// A monotonically-increasing counter, which we use to sweep + /// [`RenderMaterialInstances::instances`] when the entities and/or required + /// components are removed. + pub current_change_tick: Tick, +} + +impl RenderMaterialInstances { + /// Returns the mesh material ID for the entity with the given mesh, or a + /// dummy mesh material ID if the mesh has no material ID. + /// + /// Meshes almost always have materials, but in very specific circumstances + /// involving custom pipelines they won't. (See the + /// `specialized_mesh_pipelines` example.) + pub(crate) fn mesh_material(&self, entity: MainEntity) -> UntypedAssetId { + match self.instances.get(&entity) { + Some(render_instance) => render_instance.asset_id, + None => DUMMY_MESH_MATERIAL.into(), + } + } +} + +/// The material associated with a single mesh instance in the main world. +/// +/// Note that this uses an [`UntypedAssetId`] and isn't generic over the +/// material type, for simplicity. +pub struct RenderMaterialInstance { + /// The material asset. + pub asset_id: UntypedAssetId, + /// The [`RenderMaterialInstances::current_change_tick`] at which this + /// material instance was last modified. + pub last_change_tick: Tick, +} + +/// A [`SystemSet`] that contains all `extract_mesh_materials` systems. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct MaterialExtractionSystems; + +/// A [`SystemSet`] that contains all `extract_entities_needs_specialization` +/// systems. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct MaterialExtractEntitiesNeedingSpecializationSystems; + +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey { + match alpha_mode { + // Premultiplied and Add share the same pipeline key + // They're made distinct in the PBR shader, via `premultiply_alpha()` + AlphaMode::Premultiplied | AlphaMode::Add => MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA, + AlphaMode::Blend => MeshPipelineKey::BLEND_ALPHA, + AlphaMode::Multiply => MeshPipelineKey::BLEND_MULTIPLY, + AlphaMode::Mask(_) => MeshPipelineKey::MAY_DISCARD, + AlphaMode::AlphaToCoverage => match *msaa { + Msaa::Off => MeshPipelineKey::MAY_DISCARD, + _ => MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE, + }, + _ => MeshPipelineKey::NONE, + } +} + +pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { + match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE, + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + } +} + +pub const fn screen_space_specular_transmission_pipeline_key( + screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality, +) -> MeshPipelineKey { + match screen_space_transmissive_blur_quality { + ScreenSpaceTransmissionQuality::Low => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW + } + ScreenSpaceTransmissionQuality::Medium => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM + } + ScreenSpaceTransmissionQuality::High => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH + } + ScreenSpaceTransmissionQuality::Ultra => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA + } + } +} + +/// A system that ensures that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes +/// whose materials changed. +/// +/// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers +/// meshes that were newly extracted, and it writes information from the +/// [`RenderMaterialInstances`] into the +/// [`crate::render::mesh::MeshInputUniform`], we must tell +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a +/// mesh if its material changed. Otherwise, the material binding information in +/// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly. +/// The easiest way to ensure that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh +/// is to mark its [`Mesh3d`] as changed, so that's what this system does. +fn mark_meshes_as_changed_if_their_materials_changed( + mut changed_meshes_query: Query< + &mut Mesh3d, + Or<(Changed>, AssetChanged>)>, + >, +) where + M: Material, +{ + for mut mesh in &mut changed_meshes_query { + mesh.set_changed(); + } +} + +/// Fills the [`RenderMaterialInstances`] resources from the meshes in the +/// scene. +fn extract_mesh_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &MeshMaterial3d), + Or<(Changed, Changed>)>, + >, + >, +) { + let last_change_tick = material_instances.current_change_tick; + + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.instances.insert( + entity.into(), + RenderMaterialInstance { + asset_id: material.id().untyped(), + last_change_tick, + }, + ); + } else { + material_instances + .instances + .remove(&MainEntity::from(entity)); + } + } +} + +/// Removes mesh materials from [`RenderMaterialInstances`] when their +/// [`MeshMaterial3d`] components are removed. +/// +/// This is tricky because we have to deal with the case in which a material of +/// type A was removed and replaced with a material of type B in the same frame +/// (which is actually somewhat common of an operation). In this case, even +/// though an entry will be present in `RemovedComponents>`, +/// we must not remove the entry in `RenderMaterialInstances` which corresponds +/// to material B. To handle this case, we use change ticks to avoid removing +/// the entry if it was updated this frame. +/// +/// This is the first of two sweep phases. Because this phase runs once per +/// material type, we need a second phase in order to guarantee that we only +/// bump [`RenderMaterialInstances::current_change_tick`] once. +fn early_sweep_material_instances( + mut material_instances: ResMut, + mut removed_materials_query: Extract>>, +) where + M: Material, +{ + let last_change_tick = material_instances.current_change_tick; + + for entity in removed_materials_query.read() { + if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { + // Only sweep the entry if it wasn't updated this frame. + if occupied_entry.get().last_change_tick != last_change_tick { + occupied_entry.remove(); + } + } + } +} + +/// Removes mesh materials from [`RenderMaterialInstances`] when their +/// [`ViewVisibility`] components are removed. +/// +/// This runs after all invocations of `early_sweep_material_instances` and is +/// responsible for bumping [`RenderMaterialInstances::current_change_tick`] in +/// preparation for a new frame. +pub fn late_sweep_material_instances( + mut material_instances: ResMut, + mut removed_meshes_query: Extract>, +) { + let last_change_tick = material_instances.current_change_tick; + + for entity in removed_meshes_query.read() { + if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { + // Only sweep the entry if it wasn't updated this frame. It's + // possible that a `ViewVisibility` component was removed and + // re-added in the same frame. + if occupied_entry.get().last_change_tick != last_change_tick { + occupied_entry.remove(); + } + } + } + + material_instances + .current_change_tick + .set(last_change_tick.get() + 1); +} + +pub fn extract_entities_needs_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut, + render_material_instances: Res, + ticks: SystemChangeTick, +) where + M: Material, +{ + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert( + (*entity).into(), + EntitySpecializationTickPair { + system_tick: ticks.this_run(), + material_instances_tick: render_material_instances.current_change_tick, + }, + ); + } +} + +/// A system that runs after all instances of +/// [`extract_entities_needs_specialization`] in order to delete specialization +/// ticks for entities that are no longer renderable. +/// +/// We delete entities from the [`EntitySpecializationTicks`] table *after* +/// updating it with newly-discovered renderable entities in order to handle the +/// case in which a single entity changes material types. If we naïvely removed +/// entities from that table when their [`MeshMaterial3d`] components were +/// removed, and an entity changed material types, we might end up adding a new +/// set of [`EntitySpecializationTickPair`] for the new material and then +/// deleting it upon detecting the removed component for the old material. +/// Deferring [`sweep_entities_needing_specialization`] to the end allows us to +/// detect the case in which another material type updated the entity +/// specialization ticks this frame and avoid deleting it if so. +pub fn sweep_entities_needing_specialization( + mut entity_specialization_ticks: ResMut, + mut removed_mesh_material_components: Extract>>, + mut specialized_material_pipeline_cache: ResMut, + mut specialized_prepass_material_pipeline_cache: Option< + ResMut, + >, + mut specialized_shadow_material_pipeline_cache: Option< + ResMut, + >, + render_material_instances: Res, + views: Query<&ExtractedView>, +) where + M: Material, +{ + // Clean up any despawned entities, we do this first in case the removed material was re-added + // the same frame, thus will appear both in the removed components list and have been added to + // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter + // + // Additionally, we need to make sure that we are careful about materials + // that could have changed type, e.g. from a `StandardMaterial` to a + // `CustomMaterial`, as this will also appear in the removed components + // list. As such, we make sure that this system runs after + // `extract_entities_needs_specialization` so that the entity specialization + // tick bookkeeping has already been done, and we can check if the entity's + // tick was updated this frame. + for entity in removed_mesh_material_components.read() { + // If the entity's specialization tick was updated this frame, that + // means that that entity changed materials this frame. Don't remove the + // entity from the table in that case. + if entity_specialization_ticks + .get(&MainEntity::from(entity)) + .is_some_and(|ticks| { + ticks.material_instances_tick == render_material_instances.current_change_tick + }) + { + continue; + } + + entity_specialization_ticks.remove(&MainEntity::from(entity)); + for view in views { + if let Some(cache) = + specialized_material_pipeline_cache.get_mut(&view.retained_view_entity) + { + cache.remove(&MainEntity::from(entity)); + } + if let Some(cache) = specialized_prepass_material_pipeline_cache + .as_mut() + .and_then(|c| c.get_mut(&view.retained_view_entity)) + { + cache.remove(&MainEntity::from(entity)); + } + if let Some(cache) = specialized_shadow_material_pipeline_cache + .as_mut() + .and_then(|c| c.get_mut(&view.retained_view_entity)) + { + cache.remove(&MainEntity::from(entity)); + } + } + } +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug)] +pub struct EntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, + _marker: PhantomData, +} + +impl Default for EntitiesNeedingSpecialization { + fn default() -> Self { + Self { + entities: Default::default(), + _marker: Default::default(), + } + } +} + +/// Stores ticks specifying the last time Bevy specialized the pipelines of each +/// entity. +/// +/// Every entity that has a mesh and material must be present in this table, +/// even if that mesh isn't visible. +#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)] +pub struct EntitySpecializationTicks { + /// A mapping from each main entity to ticks that specify the last time this + /// entity's pipeline was specialized. + /// + /// Every entity that has a mesh and material must be present in this table, + /// even if that mesh isn't visible. + #[deref] + pub entities: MainEntityHashMap, +} + +/// Ticks that specify the last time an entity's pipeline was specialized. +/// +/// We need two different types of ticks here for a subtle reason. First, we +/// need the [`Self::system_tick`], which maps to Bevy's [`SystemChangeTick`], +/// because that's what we use in [`specialize_material_meshes`] to check +/// whether pipelines need specialization. But we also need +/// [`Self::material_instances_tick`], which maps to the +/// [`RenderMaterialInstances::current_change_tick`]. That's because the latter +/// only changes once per frame, which is a guarantee we need to handle the +/// following case: +/// +/// 1. The app removes material A from a mesh and replaces it with material B. +/// Both A and B are of different [`Material`] types entirely. +/// +/// 2. [`extract_entities_needs_specialization`] runs for material B and marks +/// the mesh as up to date by recording the current tick. +/// +/// 3. [`sweep_entities_needing_specialization`] runs for material A and checks +/// to ensure it's safe to remove the [`EntitySpecializationTickPair`] for the mesh +/// from the [`EntitySpecializationTicks`]. To do this, it needs to know +/// whether [`extract_entities_needs_specialization`] for some *different* +/// material (in this case, material B) ran earlier in the frame and updated the +/// change tick, and to skip removing the [`EntitySpecializationTickPair`] if so. +/// It can't reliably use the [`Self::system_tick`] to determine this because +/// the [`SystemChangeTick`] can be updated multiple times in the same frame. +/// Instead, it needs a type of tick that's updated only once per frame, after +/// all materials' versions of [`sweep_entities_needing_specialization`] have +/// run. The [`RenderMaterialInstances`] tick satisfies this criterion, and so +/// that's what [`sweep_entities_needing_specialization`] uses. +#[derive(Clone, Copy, Debug)] +pub struct EntitySpecializationTickPair { + /// The standard Bevy system tick. + pub system_tick: Tick, + /// The tick in [`RenderMaterialInstances`], which is updated in + /// `late_sweep_material_instances`. + pub material_instances_tick: Tick, +} + +/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedMaterialPipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +pub fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + ( + Or<( + Changed, + AssetChanged, + Changed>, + AssetChanged>, + )>, + With>, + ), + >, + mut par_local: Local>>, + mut entities_needing_specialization: ResMut>, +) where + M: Material, +{ + entities_needing_specialization.clear(); + + needs_specialization + .par_iter() + .for_each(|entity| par_local.borrow_local_mut().push(entity)); + + par_local.drain_into(&mut entities_needing_specialization); +} + +pub fn specialize_material_meshes( + render_meshes: Res>, + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + render_lightmaps: Res, + render_visibility_ranges: Res, + ( + opaque_render_phases, + alpha_mask_render_phases, + transmissive_render_phases, + transparent_render_phases, + ): ( + Res>, + Res>, + Res>, + Res>, + ), + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !transparent_render_phases.contains_key(&view.retained_view_entity) + && !opaque_render_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) + && !transmissive_render_phases.contains_key(&view.retained_view_entity) + { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks + .get(visible_entity) + .unwrap() + .system_tick; + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + + let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( + material.properties.alpha_mode, + &Msaa::from_samples(view_key.msaa_samples()), + )); + let mut mesh_key = *view_key + | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) + | mesh_pipeline_key_bits; + + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } + } + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let erased_key = ErasedMaterialPipelineKey { + type_id: material_instance.asset_id.type_id(), + mesh_key, + material_key: material.properties.material_key.clone(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +/// For each view, iterates over all the meshes visible from that view and adds +/// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. +pub fn queue_material_meshes( + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, + mut transmissive_render_phases: ResMut>, + mut transparent_render_phases: ResMut>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: ResMut, +) { + for (view, visible_entities) in &views { + let ( + Some(opaque_phase), + Some(alpha_mask_phase), + Some(transmissive_phase), + Some(transparent_phase), + ) = ( + opaque_render_phases.get_mut(&view.retained_view_entity), + alpha_mask_render_phases.get_mut(&view.retained_view_entity), + transmissive_render_phases.get_mut(&view.retained_view_entity), + transparent_render_phases.get_mut(&view.retained_view_entity), + ) + else { + continue; + }; + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + let rangefinder = view.rangefinder3d(); + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick) + || alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick) + { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + + // Fetch the slabs that this mesh resides in. + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + + match material.properties.render_phase_type { + RenderPhaseType::Transmissive => { + let distance = rangefinder.distance(&mesh_instance.center) + + material.properties.depth_bias; + let Some(draw_function) = material + .properties + .get_draw_function(MainPassTransmissiveDrawFunction) + else { + continue; + }; + transmissive_phase.add(Transmissive3d { + entity: (*render_entity, *visible_entity), + draw_function, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), + }); + } + RenderPhaseType::Opaque => { + if material.properties.render_method == OpaqueRendererMethod::Deferred { + // Even though we aren't going to insert the entity into + // a bin, we still want to update its cache entry. That + // way, we know we don't need to re-examine it in future + // frames. + opaque_phase.update_cache(*visible_entity, None, current_change_tick); + continue; + } + let Some(draw_function) = material + .properties + .get_draw_function(MainPassOpaqueDrawFunction) + else { + continue; + }; + let batch_set_key = Opaque3dBatchSetKey { + pipeline: pipeline_id, + draw_function, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + lightmap_slab: mesh_instance.shared.lightmap_slab_index.map(|index| *index), + }; + let bin_key = Opaque3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + opaque_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + // Alpha mask + RenderPhaseType::AlphaMask => { + let Some(draw_function) = material + .properties + .get_draw_function(MainPassAlphaMaskDrawFunction) + else { + continue; + }; + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + RenderPhaseType::Transparent => { + let distance = rangefinder.distance(&mesh_instance.center) + + material.properties.depth_bias; + let Some(draw_function) = material + .properties + .get_draw_function(MainPassTransparentDrawFunction) + else { + continue; + }; + transparent_phase.add(Transparent3d { + entity: (*render_entity, *visible_entity), + draw_function, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), + }); + } + } + } + } +} + +/// Default render method used for opaque materials. +#[derive(Default, Resource, Clone, Debug, ExtractResource, Reflect)] +#[reflect(Resource, Default, Debug, Clone)] +pub struct DefaultOpaqueRendererMethod(OpaqueRendererMethod); + +impl DefaultOpaqueRendererMethod { + pub fn forward() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Forward) + } + + pub fn deferred() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Deferred) + } + + pub fn set_to_forward(&mut self) { + self.0 = OpaqueRendererMethod::Forward; + } + + pub fn set_to_deferred(&mut self) { + self.0 = OpaqueRendererMethod::Deferred; + } +} + +/// Render method used for opaque materials. +/// +/// The forward rendering main pass draws each mesh entity and shades it according to its +/// corresponding material and the lights that affect it. Some render features like Screen Space +/// Ambient Occlusion require running depth and normal prepasses, that are 'deferred'-like +/// prepasses over all mesh entities to populate depth and normal textures. This means that when +/// using render features that require running prepasses, multiple passes over all visible geometry +/// are required. This can be slow if there is a lot of geometry that cannot be batched into few +/// draws. +/// +/// Deferred rendering runs a prepass to gather not only geometric information like depth and +/// normals, but also all the material properties like base color, emissive color, reflectance, +/// metalness, etc, and writes them into a deferred 'g-buffer' texture. The deferred main pass is +/// then a fullscreen pass that reads data from these textures and executes shading. This allows +/// for one pass over geometry, but is at the cost of not being able to use MSAA, and has heavier +/// bandwidth usage which can be unsuitable for low end mobile or other bandwidth-constrained devices. +/// +/// If a material indicates `OpaqueRendererMethod::Auto`, `DefaultOpaqueRendererMethod` will be used. +#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect)] +#[reflect(Default, Clone, PartialEq)] +pub enum OpaqueRendererMethod { + #[default] + Forward, + Deferred, + Auto, +} + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletPrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletDeferredFragmentShader; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MainPassOpaqueDrawFunction; +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MainPassAlphaMaskDrawFunction; +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MainPassTransmissiveDrawFunction; +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MainPassTransparentDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassOpaqueDrawFunction; +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassAlphaMaskDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredOpaqueDrawFunction; +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredAlphaMaskDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct ShadowsDrawFunction; + +#[derive(Debug)] +pub struct ErasedMaterialKey { + type_id: TypeId, + hash: u64, + value: Box, + vtable: Arc, +} + +#[derive(Debug)] +pub struct ErasedMaterialKeyVTable { + clone_fn: fn(&dyn Any) -> Box, + partial_eq_fn: fn(&dyn Any, &dyn Any) -> bool, +} + +impl ErasedMaterialKey { + pub fn new(material_key: T) -> Self + where + T: Clone + Hash + PartialEq + Send + Sync + 'static, + { + let type_id = TypeId::of::(); + let hash = FixedHasher::hash_one(&FixedHasher, &material_key); + + fn clone(any: &dyn Any) -> Box { + Box::new(any.downcast_ref::().unwrap().clone()) + } + fn partial_eq(a: &dyn Any, b: &dyn Any) -> bool { + a.downcast_ref::().unwrap() == b.downcast_ref::().unwrap() + } + + Self { + type_id, + hash, + value: Box::new(material_key), + vtable: Arc::new(ErasedMaterialKeyVTable { + clone_fn: clone::, + partial_eq_fn: partial_eq::, + }), + } + } + + pub fn to_key(&self) -> T { + debug_assert_eq!(self.type_id, TypeId::of::()); + self.value.downcast_ref::().unwrap().clone() + } +} + +impl PartialEq for ErasedMaterialKey { + fn eq(&self, other: &Self) -> bool { + self.type_id == other.type_id + && (self.vtable.partial_eq_fn)(self.value.as_ref(), other.value.as_ref()) + } +} + +impl Eq for ErasedMaterialKey {} + +impl Clone for ErasedMaterialKey { + fn clone(&self) -> Self { + Self { + type_id: self.type_id, + hash: self.hash, + value: (self.vtable.clone_fn)(self.value.as_ref()), + vtable: self.vtable.clone(), + } + } +} + +impl Hash for ErasedMaterialKey { + fn hash(&self, state: &mut H) { + self.type_id.hash(state); + self.hash.hash(state); + } +} + +impl Default for ErasedMaterialKey { + fn default() -> Self { + Self::new(()) + } +} + +/// Common [`Material`] properties, calculated for a specific material instance. +#[derive(Default)] +pub struct MaterialProperties { + /// Is this material should be rendered by the deferred renderer when. + /// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`] + pub render_method: OpaqueRendererMethod, + /// The [`AlphaMode`] of this material. + pub alpha_mode: AlphaMode, + /// The bits in the [`MeshPipelineKey`] for this material. + /// + /// These are precalculated so that we can just "or" them together in + /// [`queue_material_meshes`]. + pub mesh_pipeline_key_bits: MeshPipelineKey, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. + pub depth_bias: f32, + /// Whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + pub reads_view_transmission_texture: bool, + pub render_phase_type: RenderPhaseType, + pub material_layout: Option, + /// Backing array is a size of 4 because the `StandardMaterial` needs 4 draw functions by default + pub draw_functions: SmallVec<[(InternedDrawFunctionLabel, DrawFunctionId); 4]>, + /// Backing array is a size of 3 because the `StandardMaterial` has 3 custom shaders (`frag`, `prepass_frag`, `deferred_frag`) which is the + /// most common use case + pub shaders: SmallVec<[(InternedShaderLabel, Handle); 3]>, + /// Whether this material *actually* uses bindless resources, taking the + /// platform support (or lack thereof) of bindless resources into account. + pub bindless: bool, + pub specialize: Option< + fn( + &MaterialPipeline, + &mut RenderPipelineDescriptor, + &MeshVertexBufferLayoutRef, + ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError>, + >, + /// The key for this material, typically a bitfield of flags that are used to modify + /// the pipeline descriptor used for this material. + pub material_key: ErasedMaterialKey, + /// Whether shadows are enabled for this material + pub shadows_enabled: bool, + /// Whether prepass is enabled for this material + pub prepass_enabled: bool, +} + +impl MaterialProperties { + pub fn get_shader(&self, label: impl ShaderLabel) -> Option> { + self.shaders + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_shader(&mut self, label: impl ShaderLabel, shader: Handle) { + self.shaders.push((label.intern(), shader)); + } + + pub fn get_draw_function(&self, label: impl DrawFunctionLabel) -> Option { + self.draw_functions + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_draw_function( + &mut self, + label: impl DrawFunctionLabel, + draw_function: DrawFunctionId, + ) { + self.draw_functions.push((label.intern(), draw_function)); + } +} + +#[derive(Clone, Copy, Default)] +pub enum RenderPhaseType { + #[default] + Opaque, + AlphaMask, + Transmissive, + Transparent, +} + +/// A resource that maps each untyped material ID to its binding. +/// +/// This duplicates information in `RenderAssets`, but it doesn't have the +/// `M` type parameter, so it can be used in untyped contexts like +/// [`crate::render::mesh::collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMaterialBindings(HashMap); + +/// Data prepared for a [`Material`] instance. +pub struct PreparedMaterial { + pub binding: MaterialBindingId, + pub properties: Arc, +} + +// orphan rules T_T +impl ErasedRenderAsset for MeshMaterial3d +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type SourceAsset = M; + type ErasedAsset = PreparedMaterial; + + type Param = ( + SRes, + SRes, + SRes, + SResMut, + SResMut, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes, + M::Param, + ); + + fn prepare_asset( + material: Self::SourceAsset, + material_id: AssetId, + ( + render_device, + pipeline_cache, + default_opaque_render_method, + bind_group_allocators, + render_material_bindings, + opaque_draw_functions, + alpha_mask_draw_functions, + transmissive_draw_functions, + transparent_draw_functions, + opaque_prepass_draw_functions, + alpha_mask_prepass_draw_functions, + opaque_deferred_draw_functions, + alpha_mask_deferred_draw_functions, + shadow_draw_functions, + asset_server, + material_param, + ): &mut SystemParamItem, + ) -> Result> { + let shadows_enabled = M::enable_shadows(); + let prepass_enabled = M::enable_prepass(); + + let draw_opaque_pbr = opaque_draw_functions.read().id::(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::(); + let draw_transparent_pbr = transparent_draw_functions.read().id::(); + let draw_opaque_prepass = opaque_prepass_draw_functions.read().id::(); + let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions.read().id::(); + let draw_opaque_deferred = opaque_deferred_draw_functions.read().id::(); + let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions + .read() + .id::(); + let draw_shadows = shadow_draw_functions.read().id::(); + + let draw_functions = SmallVec::from_iter([ + (MainPassOpaqueDrawFunction.intern(), draw_opaque_pbr), + (MainPassAlphaMaskDrawFunction.intern(), draw_alpha_mask_pbr), + ( + MainPassTransmissiveDrawFunction.intern(), + draw_transmissive_pbr, + ), + ( + MainPassTransparentDrawFunction.intern(), + draw_transparent_pbr, + ), + (PrepassOpaqueDrawFunction.intern(), draw_opaque_prepass), + ( + PrepassAlphaMaskDrawFunction.intern(), + draw_alpha_mask_prepass, + ), + (DeferredOpaqueDrawFunction.intern(), draw_opaque_deferred), + ( + DeferredAlphaMaskDrawFunction.intern(), + draw_alpha_mask_deferred, + ), + (ShadowsDrawFunction.intern(), draw_shadows), + ]); + + let render_method = match material.opaque_render_method() { + OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, + OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, + OpaqueRendererMethod::Auto => default_opaque_render_method.0, + }; + + let mut mesh_pipeline_key_bits = MeshPipelineKey::empty(); + mesh_pipeline_key_bits.set( + MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, + material.reads_view_transmission_texture(), + ); + + let reads_view_transmission_texture = + mesh_pipeline_key_bits.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + + let render_phase_type = match material.alpha_mode() { + AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => { + RenderPhaseType::Transparent + } + _ if reads_view_transmission_texture => RenderPhaseType::Transmissive, + AlphaMode::Opaque | AlphaMode::AlphaToCoverage => RenderPhaseType::Opaque, + AlphaMode::Mask(_) => RenderPhaseType::AlphaMask, + }; + + let mut shaders = SmallVec::new(); + let mut add_shader = |label: InternedShaderLabel, shader_ref: ShaderRef| { + let mayber_shader = match shader_ref { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }; + if let Some(shader) = mayber_shader { + shaders.push((label, shader)); + } + }; + add_shader(MaterialVertexShader.intern(), M::vertex_shader()); + add_shader(MaterialFragmentShader.intern(), M::fragment_shader()); + add_shader(PrepassVertexShader.intern(), M::prepass_vertex_shader()); + add_shader(PrepassFragmentShader.intern(), M::prepass_fragment_shader()); + add_shader(DeferredVertexShader.intern(), M::deferred_vertex_shader()); + add_shader( + DeferredFragmentShader.intern(), + M::deferred_fragment_shader(), + ); + + #[cfg(feature = "meshlet")] + { + add_shader( + MeshletFragmentShader.intern(), + M::meshlet_mesh_fragment_shader(), + ); + add_shader( + MeshletPrepassFragmentShader.intern(), + M::meshlet_mesh_prepass_fragment_shader(), + ); + add_shader( + MeshletDeferredFragmentShader.intern(), + M::meshlet_mesh_deferred_fragment_shader(), + ); + } + + let bindless = material_uses_bindless_resources::(render_device); + let bind_group_data = material.bind_group_data(); + let material_key = ErasedMaterialKey::new(bind_group_data); + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + mesh_layout: &MeshVertexBufferLayoutRef, + erased_key: ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> + where + M::Data: Hash + Clone, + { + let material_key = erased_key.material_key.to_key(); + M::specialize( + pipeline, + descriptor, + mesh_layout, + MaterialPipelineKey { + mesh_key: erased_key.mesh_key, + bind_group_data: material_key, + }, + ) + } + + let material_layout = M::bind_group_layout_descriptor(render_device); + let actual_material_layout = pipeline_cache.get_bind_group_layout(&material_layout); + + match material.unprepared_bind_group( + &actual_material_layout, + render_device, + material_param, + false, + ) { + Ok(unprepared) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + // Allocate or update the material. + let binding = match render_material_bindings.entry(material_id.into()) { + Entry::Occupied(mut occupied_entry) => { + // TODO: Have a fast path that doesn't require + // recreating the bind group if only buffer contents + // change. For now, we just delete and recreate the bind + // group. + bind_group_allocator.free(*occupied_entry.get()); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); + *occupied_entry.get_mut() = new_binding; + new_binding + } + Entry::Vacant(vacant_entry) => *vacant_entry.insert( + bind_group_allocator.allocate_unprepared(unprepared, &material_layout), + ), + }; + + Ok(PreparedMaterial { + binding, + properties: Arc::new(MaterialProperties { + alpha_mode: material.alpha_mode(), + depth_bias: material.depth_bias(), + reads_view_transmission_texture, + render_phase_type, + render_method, + mesh_pipeline_key_bits, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), + }) + } + + Err(AsBindGroupError::RetryNextUpdate) => { + Err(PrepareAssetError::RetryNextUpdate(material)) + } + + Err(AsBindGroupError::CreateBindGroupDirectly) => { + // This material has opted out of automatic bind group creation + // and is requesting a fully-custom bind group. Invoke + // `as_bind_group` as requested, and store the resulting bind + // group in the slot. + match material.as_bind_group( + &material_layout, + render_device, + pipeline_cache, + material_param, + ) { + Ok(prepared_bind_group) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + // Store the resulting bind group directly in the slot. + let material_binding_id = + bind_group_allocator.allocate_prepared(prepared_bind_group); + render_material_bindings.insert(material_id.into(), material_binding_id); + + Ok(PreparedMaterial { + binding: material_binding_id, + properties: Arc::new(MaterialProperties { + alpha_mode: material.alpha_mode(), + depth_bias: material.depth_bias(), + reads_view_transmission_texture, + render_phase_type, + render_method, + mesh_pipeline_key_bits, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), + }) + } + + Err(AsBindGroupError::RetryNextUpdate) => { + Err(PrepareAssetError::RetryNextUpdate(material)) + } + + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), + } + } + + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), + } + } + + fn unload_asset( + source_asset: AssetId, + (_, _, _, bind_group_allocators, render_material_bindings, ..): &mut SystemParamItem< + Self::Param, + >, + ) { + let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped()) + else { + return; + }; + let bind_group_allactor = bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + bind_group_allactor.free(material_binding_id); + } +} + +/// Creates and/or recreates any bind groups that contain materials that were +/// modified this frame. +pub fn prepare_material_bind_groups( + mut allocators: ResMut, + render_device: Res, + pipeline_cache: Res, + fallback_image: Res, + fallback_resources: Res, +) { + for (_, allocator) in allocators.iter_mut() { + allocator.prepare_bind_groups( + &render_device, + &pipeline_cache, + &fallback_resources, + &fallback_image, + ); + } +} + +/// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] +/// manages to the GPU. +/// +/// Non-bindless allocators don't currently manage any buffers, so this method +/// only has an effect for bindless allocators. +pub fn write_material_bind_group_buffers( + mut allocators: ResMut, + render_device: Res, + render_queue: Res, +) { + for (_, allocator) in allocators.iter_mut() { + allocator.write_buffers(&render_device, &render_queue); + } +} diff --git a/vendor/bevy_pbr/src/material_bind_groups.rs b/vendor/bevy_pbr/src/material_bind_groups.rs new file mode 100644 index 00000000..d987725b --- /dev/null +++ b/vendor/bevy_pbr/src/material_bind_groups.rs @@ -0,0 +1,2050 @@ +//! Material bind group management for bindless resources. +//! +//! In bindless mode, Bevy's renderer groups materials into bind groups. This +//! allocator manages each bind group, assigning slots to materials as +//! appropriate. + +use crate::Material; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::render_resource::{BindlessSlabResourceLimit, PipelineCache}; +use bevy_render::{ + render_resource::{ + BindGroup, BindGroupEntry, BindGroupLayoutDescriptor, BindingNumber, BindingResource, + BindingResources, BindlessDescriptor, BindlessIndex, BindlessIndexTableDescriptor, + BindlessResourceType, Buffer, BufferBinding, BufferDescriptor, BufferId, + BufferInitDescriptor, BufferUsages, CompareFunction, FilterMode, OwnedBindingResource, + PreparedBindGroup, RawBufferVec, Sampler, SamplerDescriptor, SamplerId, TextureView, + TextureViewDimension, TextureViewId, UnpreparedBindGroup, WgpuSampler, WgpuTextureView, + }, + renderer::{RenderDevice, RenderQueue}, + settings::WgpuFeatures, + texture::FallbackImage, +}; +use bevy_utils::{default, TypeIdMap}; +use bytemuck::Pod; +use core::hash::Hash; +use core::{cmp::Ordering, iter, mem, ops::Range}; +use tracing::{error, trace}; + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct MaterialBindGroupAllocators(TypeIdMap); + +/// A resource that places materials into bind groups and tracks their +/// resources. +/// +/// Internally, Bevy has separate allocators for bindless and non-bindless +/// materials. This resource provides a common interface to the specific +/// allocator in use. +pub enum MaterialBindGroupAllocator { + /// The allocator used when the material is bindless. + Bindless(Box), + /// The allocator used when the material is non-bindless. + NonBindless(Box), +} + +/// The allocator that places bindless materials into bind groups and tracks +/// their resources. +pub struct MaterialBindGroupBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: &'static str, + /// The slabs, each of which contains a bind group. + slabs: Vec, + /// The layout of the bind groups that we produce. + bind_group_layout: BindGroupLayoutDescriptor, + /// Information about the bindless resources in the material. + /// + /// We use this information to create and maintain bind groups. + bindless_descriptor: BindlessDescriptor, + + /// Dummy buffers that we use to fill empty slots in buffer binding arrays. + /// + /// There's one fallback buffer for each buffer in the bind group, each + /// appropriately sized. Each buffer contains one uninitialized element of + /// the applicable type. + fallback_buffers: HashMap, + + /// The maximum number of resources that can be stored in a slab. + /// + /// This corresponds to `SLAB_CAPACITY` in the `#[bindless(SLAB_CAPACITY)]` + /// attribute, when deriving `AsBindGroup`. + slab_capacity: u32, +} + +/// A single bind group and the bookkeeping necessary to allocate into it. +pub struct MaterialBindlessSlab { + /// The current bind group, if it's up to date. + /// + /// If this is `None`, then the bind group is dirty and needs to be + /// regenerated. + bind_group: Option, + + /// The GPU-accessible buffers that hold the mapping from binding index to + /// bindless slot. + /// + /// This is conventionally assigned to bind group binding 0, but it can be + /// changed using the `#[bindless(index_table(binding(B)))]` attribute on + /// `AsBindGroup`. + /// + /// Because the slab binary searches this table, the entries within must be + /// sorted by bindless index. + bindless_index_tables: Vec, + + /// The binding arrays containing samplers. + samplers: HashMap>, + /// The binding arrays containing textures. + textures: HashMap>, + /// The binding arrays containing buffers. + buffers: HashMap>, + /// The buffers that contain plain old data (i.e. the structure-level + /// `#[data]` attribute of `AsBindGroup`). + data_buffers: HashMap, + + /// A list of free slot IDs. + free_slots: Vec, + /// The total number of materials currently allocated in this slab. + live_allocation_count: u32, + /// The total number of resources currently allocated in the binding arrays. + allocated_resource_count: u32, +} + +/// A GPU-accessible buffer that holds the mapping from binding index to +/// bindless slot. +/// +/// This is conventionally assigned to bind group binding 0, but it can be +/// changed by altering the [`Self::binding_number`], which corresponds to the +/// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`. +struct MaterialBindlessIndexTable { + /// The buffer containing the mappings. + buffer: RetainedRawBufferVec, + /// The range of bindless indices that this bindless index table covers. + /// + /// If this range is M..N, then the field at index $i$ maps to bindless + /// index $i$ + M. The size of this table is N - M. + /// + /// This corresponds to the `#[bindless(index_table(range(M..N)))]` + /// attribute in `AsBindGroup`. + index_range: Range, + /// The binding number that this index table is assigned to in the shader. + binding_number: BindingNumber, +} + +/// A single binding array for storing bindless resources and the bookkeeping +/// necessary to allocate into it. +struct MaterialBindlessBindingArray +where + R: GetBindingResourceId, +{ + /// The number of the binding that we attach this binding array to. + binding_number: BindingNumber, + /// A mapping from bindless slot index to the resource stored in that slot, + /// if any. + bindings: Vec>>, + /// The type of resource stored in this binding array. + resource_type: BindlessResourceType, + /// Maps a resource ID to the slot in which it's stored. + /// + /// This is essentially the inverse mapping of [`Self::bindings`]. + resource_to_slot: HashMap, + /// A list of free slots in [`Self::bindings`] that contain no binding. + free_slots: Vec, + /// The number of allocated objects in this binding array. + len: u32, +} + +/// A single resource (sampler, texture, or buffer) in a binding array. +/// +/// Resources hold a reference count, which specifies the number of materials +/// currently allocated within the slab that refer to this resource. When the +/// reference count drops to zero, the resource is freed. +struct MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// The sampler, texture, or buffer. + resource: R, + /// The number of materials currently allocated within the containing slab + /// that use this resource. + ref_count: u32, +} + +/// The allocator that stores bind groups for non-bindless materials. +pub struct MaterialBindGroupNonBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: &'static str, + /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in + /// each slot. + bind_groups: Vec>, + /// The bind groups that are dirty and need to be prepared. + /// + /// To prepare the bind groups, call + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. + to_prepare: HashSet, + /// A list of free bind group indices. + free_indices: Vec, +} + +/// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is +/// currently managing. +enum MaterialNonBindlessAllocatedBindGroup { + /// An unprepared bind group. + /// + /// The allocator prepares all outstanding unprepared bind groups when + /// [`MaterialBindGroupNonBindlessAllocator::prepare_bind_groups`] is + /// called. + Unprepared { + /// The unprepared bind group, including extra data. + bind_group: UnpreparedBindGroup, + /// The layout of that bind group. + layout: BindGroupLayoutDescriptor, + }, + /// A bind group that's already been prepared. + Prepared { + bind_group: PreparedBindGroup, + #[expect(dead_code, reason = "These buffers are only referenced by bind groups")] + uniform_buffers: Vec, + }, +} + +/// Dummy instances of various resources that we fill unused slots in binding +/// arrays with. +#[derive(Resource)] +pub struct FallbackBindlessResources { + /// A dummy filtering sampler. + filtering_sampler: Sampler, + /// A dummy non-filtering sampler. + non_filtering_sampler: Sampler, + /// A dummy comparison sampler. + comparison_sampler: Sampler, +} + +/// The `wgpu` ID of a single bindless or non-bindless resource. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum BindingResourceId { + /// A buffer. + Buffer(BufferId), + /// A texture view, with the given dimension. + TextureView(TextureViewDimension, TextureViewId), + /// A sampler. + Sampler(SamplerId), + /// A buffer containing plain old data. + /// + /// This corresponds to the `#[data]` structure-level attribute on + /// `AsBindGroup`. + DataBuffer, +} + +/// A temporary list of references to `wgpu` bindless resources. +/// +/// We need this because the `wgpu` bindless API takes a slice of references. +/// Thus we need to create intermediate vectors of bindless resources in order +/// to satisfy `wgpu`'s lifetime requirements. +enum BindingResourceArray<'a> { + /// A list of bindings. + Buffers(Vec>), + /// A list of texture views. + TextureViews(Vec<&'a WgpuTextureView>), + /// A list of samplers. + Samplers(Vec<&'a WgpuSampler>), +} + +/// The location of a material (either bindless or non-bindless) within the +/// slabs. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Clone, Default)] +pub struct MaterialBindingId { + /// The index of the bind group (slab) where the GPU data is located. + pub group: MaterialBindGroupIndex, + /// The slot within that bind group. + /// + /// Non-bindless materials will always have a slot of 0. + pub slot: MaterialBindGroupSlot, +} + +/// The index of each material bind group. +/// +/// In bindless mode, each bind group contains multiple materials. In +/// non-bindless mode, each bind group contains only one material. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect, Deref, DerefMut)] +#[reflect(Default, Clone, PartialEq, Hash)] +pub struct MaterialBindGroupIndex(pub u32); + +impl From for MaterialBindGroupIndex { + fn from(value: u32) -> Self { + MaterialBindGroupIndex(value) + } +} + +/// The index of the slot containing material data within each material bind +/// group. +/// +/// In bindless mode, this slot is needed to locate the material data in each +/// bind group, since multiple materials are packed into a single slab. In +/// non-bindless mode, this slot is always 0. +#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect, Deref, DerefMut)] +#[reflect(Default, Clone, PartialEq)] +pub struct MaterialBindGroupSlot(pub u32); + +/// The CPU/GPU synchronization state of a buffer that we maintain. +/// +/// Currently, the only buffer that we maintain is the +/// [`MaterialBindlessIndexTable`]. +enum BufferDirtyState { + /// The buffer is currently synchronized between the CPU and GPU. + Clean, + /// The buffer hasn't been created yet. + NeedsReserve, + /// The buffer exists on both CPU and GPU, but the GPU data is out of date. + NeedsUpload, +} + +/// Information that describes a potential allocation of an +/// [`UnpreparedBindGroup`] into a slab. +struct BindlessAllocationCandidate { + /// A map that, for every resource in the [`UnpreparedBindGroup`] that + /// already existed in this slab, maps bindless index of that resource to + /// its slot in the appropriate binding array. + pre_existing_resources: HashMap, + /// Stores the number of free slots that are needed to satisfy this + /// allocation. + needed_free_slots: u32, +} + +/// A trait that allows fetching the [`BindingResourceId`] from a +/// [`BindlessResourceType`]. +/// +/// This is used when freeing bindless resources, in order to locate the IDs +/// assigned to each resource so that they can be removed from the appropriate +/// maps. +trait GetBindingResourceId { + /// Returns the [`BindingResourceId`] for this resource. + /// + /// `resource_type` specifies this resource's type. This is used for + /// textures, as a `wgpu` [`TextureView`] doesn't store enough information + /// itself to determine its dimension. + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId; +} + +/// The public interface to a slab, which represents a single bind group. +pub struct MaterialSlab<'a>(MaterialSlabImpl<'a>); + +/// The actual implementation of a material slab. +/// +/// This has bindless and non-bindless variants. +enum MaterialSlabImpl<'a> { + /// The implementation of the slab interface we use when the slab + /// is bindless. + Bindless(&'a MaterialBindlessSlab), + /// The implementation of the slab interface we use when the slab + /// is non-bindless. + NonBindless(MaterialNonBindlessSlab<'a>), +} + +/// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] +/// manages. +enum MaterialNonBindlessSlab<'a> { + /// A slab that has a bind group. + Prepared(&'a PreparedBindGroup), + /// A slab that doesn't yet have a bind group. + Unprepared, +} + +/// Manages an array of untyped plain old data on GPU and allocates individual +/// slots within that array. +/// +/// This supports the `#[data]` attribute of `AsBindGroup`. +struct MaterialDataBuffer { + /// The number of the binding that we attach this storage buffer to. + binding_number: BindingNumber, + /// The actual data. + /// + /// Note that this is untyped (`u8`); the actual aligned size of each + /// element is given by [`Self::aligned_element_size`]; + buffer: RetainedRawBufferVec, + /// The size of each element in the buffer, including padding and alignment + /// if any. + aligned_element_size: u32, + /// A list of free slots within the buffer. + free_slots: Vec, + /// The actual number of slots that have been allocated. + len: u32, +} + +/// A buffer containing plain old data, already packed into the appropriate GPU +/// format, and that can be updated incrementally. +/// +/// This structure exists in order to encapsulate the lazy update +/// ([`BufferDirtyState`]) logic in a single place. +#[derive(Deref, DerefMut)] +struct RetainedRawBufferVec +where + T: Pod, +{ + /// The contents of the buffer. + #[deref] + buffer: RawBufferVec, + /// Whether the contents of the buffer have been uploaded to the GPU. + dirty: BufferDirtyState, +} + +/// The size of the buffer that we assign to unused buffer slots, in bytes. +/// +/// This is essentially arbitrary, as it doesn't seem to matter to `wgpu` what +/// the size is. +const DEFAULT_BINDLESS_FALLBACK_BUFFER_SIZE: u64 = 16; + +impl From for MaterialBindGroupSlot { + fn from(value: u32) -> Self { + MaterialBindGroupSlot(value) + } +} + +impl From for u32 { + fn from(value: MaterialBindGroupSlot) -> Self { + value.0 + } +} + +impl<'a> From<&'a OwnedBindingResource> for BindingResourceId { + fn from(value: &'a OwnedBindingResource) -> Self { + match *value { + OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()), + OwnedBindingResource::Data(_) => BindingResourceId::DataBuffer, + OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => { + BindingResourceId::TextureView(*texture_view_dimension, texture_view.id()) + } + OwnedBindingResource::Sampler(_, ref sampler) => { + BindingResourceId::Sampler(sampler.id()) + } + } + } +} + +impl GetBindingResourceId for Buffer { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Buffer(self.id()) + } +} + +impl GetBindingResourceId for Sampler { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Sampler(self.id()) + } +} + +impl GetBindingResourceId for TextureView { + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId { + let texture_view_dimension = match resource_type { + BindlessResourceType::Texture1d => TextureViewDimension::D1, + BindlessResourceType::Texture2d => TextureViewDimension::D2, + BindlessResourceType::Texture2dArray => TextureViewDimension::D2Array, + BindlessResourceType::Texture3d => TextureViewDimension::D3, + BindlessResourceType::TextureCube => TextureViewDimension::Cube, + BindlessResourceType::TextureCubeArray => TextureViewDimension::CubeArray, + _ => panic!("Resource type is not a texture"), + }; + BindingResourceId::TextureView(texture_view_dimension, self.id()) + } +} + +impl MaterialBindGroupAllocator { + /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a + /// single material. + pub fn new( + render_device: &RenderDevice, + label: &'static str, + bindless_descriptor: Option, + bind_group_layout: BindGroupLayoutDescriptor, + slab_capacity: Option, + ) -> MaterialBindGroupAllocator { + if let Some(bindless_descriptor) = bindless_descriptor { + MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( + render_device, + label, + bindless_descriptor, + bind_group_layout, + slab_capacity, + ))) + } else { + MaterialBindGroupAllocator::NonBindless(Box::new( + MaterialBindGroupNonBindlessAllocator::new(label), + )) + } + } + + /// Returns the slab with the given index, if one exists. + pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + match *self { + MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator + .get(group) + .map(|bindless_slab| MaterialSlab(MaterialSlabImpl::Bindless(bindless_slab))), + MaterialBindGroupAllocator::NonBindless(ref non_bindless_allocator) => { + non_bindless_allocator.get(group).map(|non_bindless_slab| { + MaterialSlab(MaterialSlabImpl::NonBindless(non_bindless_slab)) + }) + } + } + } + + /// Allocates an [`UnpreparedBindGroup`] and returns the resulting binding ID. + /// + /// This method should generally be preferred over + /// [`Self::allocate_prepared`], because this method supports both bindless + /// and non-bindless bind groups. Only use [`Self::allocate_prepared`] if + /// you need to prepare the bind group yourself. + pub fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: &BindGroupLayoutDescriptor, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.allocate_unprepared(unprepared_bind_group), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator + .allocate_unprepared(unprepared_bind_group, (*bind_group_layout).clone()), + } + } + + /// Places a pre-prepared bind group into a slab. + /// + /// For bindless materials, the allocator internally manages the bind + /// groups, so calling this method will panic if this is a bindless + /// allocator. Only non-bindless allocators support this method. + /// + /// It's generally preferred to use [`Self::allocate_unprepared`], because + /// that method supports both bindless and non-bindless allocators. Only use + /// this method if you need to prepare the bind group yourself. + pub fn allocate_prepared( + &mut self, + prepared_bind_group: PreparedBindGroup, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless(_) => { + panic!( + "Bindless resources are incompatible with implementing `as_bind_group` \ + directly; implement `unprepared_bind_group` instead or disable bindless" + ) + } + MaterialBindGroupAllocator::NonBindless(ref mut non_bindless_allocator) => { + non_bindless_allocator.allocate_prepared(prepared_bind_group) + } + } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + pub fn free(&mut self, material_binding_id: MaterialBindingId) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.free(material_binding_id), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator.free(material_binding_id), + } + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling [`MaterialBindGroupAllocator::prepare_bind_groups`]. + pub fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_image: &FallbackImage, + ) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.prepare_bind_groups( + render_device, + pipeline_cache, + fallback_bindless_resources, + fallback_image, + ), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator + .prepare_bind_groups(render_device, pipeline_cache), + } + } + + /// Uploads the contents of all buffers that this + /// [`MaterialBindGroupAllocator`] manages to the GPU. + /// + /// Non-bindless allocators don't currently manage any buffers, so this + /// method only has an effect for bindless allocators. + pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.write_buffers(render_device, render_queue), + MaterialBindGroupAllocator::NonBindless(_) => { + // Not applicable. + } + } + } + + /// Get number of allocated slabs for bindless material, returns 0 if it is + /// [`Self::NonBindless`]. + pub fn slab_count(&self) -> usize { + match self { + Self::Bindless(bless) => bless.slabs.len(), + Self::NonBindless(_) => 0, + } + } + + /// Get total size of slabs allocated for bindless material, returns 0 if it is + /// [`Self::NonBindless`]. + pub fn slabs_size(&self) -> usize { + match self { + Self::Bindless(bless) => bless + .slabs + .iter() + .flat_map(|slab| { + slab.data_buffers + .iter() + .map(|(_, buffer)| buffer.buffer.len()) + }) + .sum(), + Self::NonBindless(_) => 0, + } + } + + /// Get number of bindless material allocations in slabs, returns 0 if it is + /// [`Self::NonBindless`]. + pub fn allocations(&self) -> u64 { + match self { + Self::Bindless(bless) => bless + .slabs + .iter() + .map(|slab| u64::from(slab.allocated_resource_count)) + .sum(), + Self::NonBindless(_) => 0, + } + } +} + +impl MaterialBindlessIndexTable { + /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. + fn new( + bindless_index_table_descriptor: &BindlessIndexTableDescriptor, + ) -> MaterialBindlessIndexTable { + // Preallocate space for one bindings table, so that there will always be a buffer. + let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE); + for _ in *bindless_index_table_descriptor.indices.start + ..*bindless_index_table_descriptor.indices.end + { + buffer.push(0); + } + + MaterialBindlessIndexTable { + buffer, + index_range: bindless_index_table_descriptor.indices.clone(), + binding_number: bindless_index_table_descriptor.binding_number, + } + } + + /// Returns the bindings in the binding index table. + /// + /// If the current [`MaterialBindlessIndexTable::index_range`] is M..N, then + /// element *i* of the returned binding index table contains the slot of the + /// bindless resource with bindless index *i* + M. + fn get(&self, slot: MaterialBindGroupSlot) -> &[u32] { + let struct_size = *self.index_range.end as usize - *self.index_range.start as usize; + let start = struct_size * slot.0 as usize; + &self.buffer.values()[start..(start + struct_size)] + } + + /// Returns a single binding from the binding index table. + fn get_binding( + &self, + slot: MaterialBindGroupSlot, + bindless_index: BindlessIndex, + ) -> Option { + if bindless_index < self.index_range.start || bindless_index >= self.index_range.end { + return None; + } + self.get(slot) + .get((*bindless_index - *self.index_range.start) as usize) + .copied() + } + + fn table_length(&self) -> u32 { + self.index_range.end.0 - self.index_range.start.0 + } + + /// Updates the binding index table for a single material. + /// + /// The `allocated_resource_slots` map contains a mapping from the + /// [`BindlessIndex`] of each resource that the material references to the + /// slot that that resource occupies in the appropriate binding array. This + /// method serializes that map into a binding index table that the shader + /// can read. + fn set( + &mut self, + slot: MaterialBindGroupSlot, + allocated_resource_slots: &HashMap, + ) { + let table_len = self.table_length() as usize; + let range = (slot.0 as usize * table_len)..((slot.0 as usize + 1) * table_len); + while self.buffer.len() < range.end { + self.buffer.push(0); + } + + for (&bindless_index, &resource_slot) in allocated_resource_slots { + if self.index_range.contains(&bindless_index) { + self.buffer.set( + *bindless_index + range.start as u32 - *self.index_range.start, + resource_slot, + ); + } + } + + // Mark the buffer as needing to be recreated, in case we grew it. + self.buffer.dirty = BufferDirtyState::NeedsReserve; + } + + /// Returns the [`BindGroupEntry`] for the index table itself. + fn bind_group_entry(&self) -> BindGroupEntry<'_> { + BindGroupEntry { + binding: *self.binding_number, + resource: self + .buffer + .buffer() + .expect("Bindings buffer must exist") + .as_entire_binding(), + } + } +} + +impl RetainedRawBufferVec +where + T: Pod, +{ + /// Creates a new empty [`RetainedRawBufferVec`] supporting the given + /// [`BufferUsages`]. + fn new(buffer_usages: BufferUsages) -> RetainedRawBufferVec { + RetainedRawBufferVec { + buffer: RawBufferVec::new(buffer_usages), + dirty: BufferDirtyState::NeedsUpload, + } + } + + /// Recreates the GPU backing buffer if needed. + fn prepare(&mut self, render_device: &RenderDevice) { + match self.dirty { + BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {} + BufferDirtyState::NeedsReserve => { + let capacity = self.buffer.len(); + self.buffer.reserve(capacity, render_device); + self.dirty = BufferDirtyState::NeedsUpload; + } + } + } + + /// Writes the current contents of the buffer to the GPU if necessary. + fn write(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match self.dirty { + BufferDirtyState::Clean => {} + BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => { + self.buffer.write_buffer(render_device, render_queue); + self.dirty = BufferDirtyState::Clean; + } + } + } +} + +impl MaterialBindGroupBindlessAllocator { + /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data + /// for a single bindless material. + fn new( + render_device: &RenderDevice, + label: &'static str, + bindless_descriptor: BindlessDescriptor, + bind_group_layout: BindGroupLayoutDescriptor, + slab_capacity: Option, + ) -> MaterialBindGroupBindlessAllocator { + let fallback_buffers = bindless_descriptor + .buffers + .iter() + .map(|bindless_buffer_descriptor| { + ( + bindless_buffer_descriptor.bindless_index, + render_device.create_buffer(&BufferDescriptor { + label: Some("bindless fallback buffer"), + size: match bindless_buffer_descriptor.size { + Some(size) => size as u64, + None => DEFAULT_BINDLESS_FALLBACK_BUFFER_SIZE, + }, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ) + }) + .collect(); + + MaterialBindGroupBindlessAllocator { + label, + slabs: vec![], + bind_group_layout, + bindless_descriptor, + fallback_buffers, + slab_capacity: slab_capacity + .expect("Non-bindless materials should use the non-bindless allocator") + .resolve(), + } + } + + /// Allocates the resources for a single material into a slab and returns + /// the resulting ID. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the slab + /// that was used. + /// + /// This function can't fail. If all slabs are full, then a new slab is + /// created, and the material is allocated into it. + fn allocate_unprepared( + &mut self, + mut unprepared_bind_group: UnpreparedBindGroup, + ) -> MaterialBindingId { + for (slab_index, slab) in self.slabs.iter_mut().enumerate() { + trace!("Trying to allocate in slab {}", slab_index); + match slab.try_allocate(unprepared_bind_group, self.slab_capacity) { + Ok(slot) => { + return MaterialBindingId { + group: MaterialBindGroupIndex(slab_index as u32), + slot, + }; + } + Err(bind_group) => unprepared_bind_group = bind_group, + } + } + + let group = MaterialBindGroupIndex(self.slabs.len() as u32); + self.slabs + .push(MaterialBindlessSlab::new(&self.bindless_descriptor)); + + // Allocate into the newly-pushed slab. + let Ok(slot) = self + .slabs + .last_mut() + .expect("We just pushed a slab") + .try_allocate(unprepared_bind_group, self.slab_capacity) + else { + panic!("An allocation into an empty slab should always succeed") + }; + + MaterialBindingId { group, slot } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + fn free(&mut self, material_binding_id: MaterialBindingId) { + self.slabs + .get_mut(material_binding_id.group.0 as usize) + .expect("Slab should exist") + .free(material_binding_id.slot, &self.bindless_descriptor); + } + + /// Returns the slab with the given bind group index. + /// + /// A [`MaterialBindGroupIndex`] can be fetched from a + /// [`MaterialBindingId`]. + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + self.slabs.get(group.0 as usize) + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling + /// [`MaterialBindGroupBindlessAllocator::prepare_bind_groups`]. + fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_image: &FallbackImage, + ) { + for slab in &mut self.slabs { + slab.prepare( + render_device, + pipeline_cache, + self.label, + &self.bind_group_layout, + fallback_bindless_resources, + &self.fallback_buffers, + fallback_image, + &self.bindless_descriptor, + self.slab_capacity, + ); + } + } + + /// Writes any buffers that we're managing to the GPU. + /// + /// Currently, this only consists of the bindless index tables. + fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + for slab in &mut self.slabs { + slab.write_buffer(render_device, render_queue); + } + } +} + +impl MaterialBindlessSlab { + /// Attempts to allocate the given unprepared bind group in this slab. + /// + /// If the allocation succeeds, this method returns the slot that the + /// allocation was placed in. If the allocation fails because the slab was + /// full, this method returns the unprepared bind group back to the caller + /// so that it can try to allocate again. + fn try_allocate( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + slot_capacity: u32, + ) -> Result { + // Locate pre-existing resources, and determine how many free slots we need. + let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { + return Err(unprepared_bind_group); + }; + + // Check to see if we have enough free space. + // + // As a special case, note that if *nothing* is allocated in this slab, + // then we always allow a material to be placed in it, regardless of the + // number of bindings the material has. This is so that, if the + // platform's maximum bindless count is set too low to hold even a + // single material, we can still place each material into a separate + // slab instead of failing outright. + if self.allocated_resource_count > 0 + && self.allocated_resource_count + allocation_candidate.needed_free_slots + > slot_capacity + { + trace!("Slab is full, can't allocate"); + return Err(unprepared_bind_group); + } + + // OK, we can allocate in this slab. Assign a slot ID. + let slot = self + .free_slots + .pop() + .unwrap_or(MaterialBindGroupSlot(self.live_allocation_count)); + + // Bump the live allocation count. + self.live_allocation_count += 1; + + // Insert the resources into the binding arrays. + let allocated_resource_slots = + self.insert_resources(unprepared_bind_group.bindings, allocation_candidate); + + // Serialize the allocated resource slots. + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table.set(slot, &allocated_resource_slots); + } + + // Invalidate the cached bind group. + self.bind_group = None; + + Ok(slot) + } + + /// Gathers the information needed to determine whether the given unprepared + /// bind group can be allocated in this slab. + fn check_allocation( + &self, + unprepared_bind_group: &UnpreparedBindGroup, + ) -> Option { + let mut allocation_candidate = BindlessAllocationCandidate { + pre_existing_resources: HashMap::default(), + needed_free_slots: 0, + }; + + for &(bindless_index, ref owned_binding_resource) in unprepared_bind_group.bindings.iter() { + let bindless_index = BindlessIndex(bindless_index); + match *owned_binding_resource { + OwnedBindingResource::Buffer(ref buffer) => { + let Some(binding_array) = self.buffers.get(&bindless_index) else { + error!( + "Binding array wasn't present for buffer at index {:?}", + bindless_index + ); + return None; + }; + match binding_array.find(BindingResourceId::Buffer(buffer.id())) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => allocation_candidate.needed_free_slots += 1, + } + } + + OwnedBindingResource::Data(_) => { + // The size of a data buffer is unlimited. + } + + OwnedBindingResource::TextureView(texture_view_dimension, ref texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + match self + .textures + .get(&bindless_resource_type) + .expect("Missing binding array for texture") + .find(BindingResourceId::TextureView( + texture_view_dimension, + texture_view.id(), + )) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + + OwnedBindingResource::Sampler(sampler_binding_type, ref sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + match self + .samplers + .get(&bindless_resource_type) + .expect("Missing binding array for sampler") + .find(BindingResourceId::Sampler(sampler.id())) + { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + } + } + + Some(allocation_candidate) + } + + /// Inserts the given [`BindingResources`] into this slab. + /// + /// Returns a table that maps the bindless index of each resource to its + /// slot in its binding array. + fn insert_resources( + &mut self, + mut binding_resources: BindingResources, + allocation_candidate: BindlessAllocationCandidate, + ) -> HashMap { + let mut allocated_resource_slots = HashMap::default(); + + for (bindless_index, owned_binding_resource) in binding_resources.drain(..) { + let bindless_index = BindlessIndex(bindless_index); + + let pre_existing_slot = allocation_candidate + .pre_existing_resources + .get(&bindless_index); + + // Otherwise, we need to insert it anew. + let binding_resource_id = BindingResourceId::from(&owned_binding_resource); + let increment_allocated_resource_count = match owned_binding_resource { + OwnedBindingResource::Buffer(buffer) => { + let slot = self + .buffers + .get_mut(&bindless_index) + .expect("Buffer binding array should exist") + .insert(binding_resource_id, buffer); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + OwnedBindingResource::Data(data) => { + if pre_existing_slot.is_some() { + panic!("Data buffers can't be deduplicated") + } + + let slot = self + .data_buffers + .get_mut(&bindless_index) + .expect("Data buffer binding array should exist") + .insert(&data); + allocated_resource_slots.insert(bindless_index, slot); + false + } + OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + let slot = self + .textures + .get_mut(&bindless_resource_type) + .expect("Texture array should exist") + .insert(binding_resource_id, texture_view); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + OwnedBindingResource::Sampler(sampler_binding_type, sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + let slot = self + .samplers + .get_mut(&bindless_resource_type) + .expect("Sampler should exist") + .insert(binding_resource_id, sampler); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + }; + + // Bump the allocated resource count. + if increment_allocated_resource_count { + self.allocated_resource_count += 1; + } + } + + allocated_resource_slots + } + + /// Removes the material allocated in the given slot, with the given + /// descriptor, from this slab. + fn free(&mut self, slot: MaterialBindGroupSlot, bindless_descriptor: &BindlessDescriptor) { + // Loop through each binding. + for (bindless_index, bindless_resource_type) in + bindless_descriptor.resources.iter().enumerate() + { + let bindless_index = BindlessIndex::from(bindless_index as u32); + let Some(bindless_index_table) = self.get_bindless_index_table(bindless_index) else { + continue; + }; + let Some(bindless_binding) = bindless_index_table.get_binding(slot, bindless_index) + else { + continue; + }; + + // Free the binding. If the resource in question was anything other + // than a data buffer, then it has a reference count and + // consequently we need to decrement it. + let decrement_allocated_resource_count = match *bindless_resource_type { + BindlessResourceType::None => false, + BindlessResourceType::Buffer => self + .buffers + .get_mut(&bindless_index) + .expect("Buffer should exist with that bindless index") + .remove(bindless_binding), + BindlessResourceType::DataBuffer => { + self.data_buffers + .get_mut(&bindless_index) + .expect("Data buffer should exist with that bindless index") + .remove(bindless_binding); + false + } + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => self + .samplers + .get_mut(bindless_resource_type) + .expect("Sampler array should exist") + .remove(bindless_binding), + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => self + .textures + .get_mut(bindless_resource_type) + .expect("Texture array should exist") + .remove(bindless_binding), + }; + + // If the slot is now free, decrement the allocated resource + // count. + if decrement_allocated_resource_count { + self.allocated_resource_count -= 1; + } + } + + // Invalidate the cached bind group. + self.bind_group = None; + + // Release the slot ID. + self.free_slots.push(slot); + self.live_allocation_count -= 1; + } + + /// Recreates the bind group and bindless index table buffer if necessary. + fn prepare( + &mut self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + label: &'static str, + bind_group_layout: &BindGroupLayoutDescriptor, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + slab_capacity: u32, + ) { + // Create the bindless index table buffers if needed. + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table.buffer.prepare(render_device); + } + + // Create any data buffers we were managing if necessary. + for data_buffer in self.data_buffers.values_mut() { + data_buffer.buffer.prepare(render_device); + } + + // Create the bind group if needed. + self.prepare_bind_group( + render_device, + pipeline_cache, + label, + bind_group_layout, + fallback_bindless_resources, + fallback_buffers, + fallback_image, + bindless_descriptor, + slab_capacity, + ); + } + + /// Recreates the bind group if this slab has been changed since the last + /// time we created it. + fn prepare_bind_group( + &mut self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + label: &'static str, + bind_group_layout: &BindGroupLayoutDescriptor, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + slab_capacity: u32, + ) { + // If the bind group is clean, then do nothing. + if self.bind_group.is_some() { + return; + } + + // Determine whether we need to pad out our binding arrays with dummy + // resources. + let required_binding_array_size = if render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + None + } else { + Some(slab_capacity) + }; + + let binding_resource_arrays = self.create_binding_resource_arrays( + fallback_bindless_resources, + fallback_buffers, + fallback_image, + bindless_descriptor, + required_binding_array_size, + ); + + let mut bind_group_entries: Vec<_> = self + .bindless_index_tables + .iter() + .map(|bindless_index_table| bindless_index_table.bind_group_entry()) + .collect(); + + for &(&binding, ref binding_resource_array) in binding_resource_arrays.iter() { + bind_group_entries.push(BindGroupEntry { + binding, + resource: match *binding_resource_array { + BindingResourceArray::Buffers(ref buffer_bindings) => { + BindingResource::BufferArray(&buffer_bindings[..]) + } + BindingResourceArray::TextureViews(ref texture_views) => { + BindingResource::TextureViewArray(&texture_views[..]) + } + BindingResourceArray::Samplers(ref samplers) => { + BindingResource::SamplerArray(&samplers[..]) + } + }, + }); + } + + // Create bind group entries for any data buffers we're managing. + for data_buffer in self.data_buffers.values() { + bind_group_entries.push(BindGroupEntry { + binding: *data_buffer.binding_number, + resource: data_buffer + .buffer + .buffer() + .expect("Backing data buffer must have been uploaded by now") + .as_entire_binding(), + }); + } + + self.bind_group = Some(render_device.create_bind_group( + Some(label), + &pipeline_cache.get_bind_group_layout(bind_group_layout), + &bind_group_entries, + )); + } + + /// Writes any buffers that we're managing to the GPU. + /// + /// Currently, this consists of the bindless index table plus any data + /// buffers we're managing. + fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table + .buffer + .write(render_device, render_queue); + } + + for data_buffer in self.data_buffers.values_mut() { + data_buffer.buffer.write(render_device, render_queue); + } + } + + /// Converts our binding arrays into binding resource arrays suitable for + /// passing to `wgpu`. + fn create_binding_resource_arrays<'a>( + &'a self, + fallback_bindless_resources: &'a FallbackBindlessResources, + fallback_buffers: &'a HashMap, + fallback_image: &'a FallbackImage, + bindless_descriptor: &'a BindlessDescriptor, + required_binding_array_size: Option, + ) -> Vec<(&'a u32, BindingResourceArray<'a>)> { + let mut binding_resource_arrays = vec![]; + + // Build sampler bindings. + self.create_sampler_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_bindless_resources, + required_binding_array_size, + ); + + // Build texture bindings. + self.create_texture_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_image, + required_binding_array_size, + ); + + // Build buffer bindings. + self.create_buffer_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_buffers, + bindless_descriptor, + required_binding_array_size, + ); + + binding_resource_arrays + } + + /// Accumulates sampler binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_sampler_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_bindless_resources: &'a FallbackBindlessResources, + required_binding_array_size: Option, + ) { + // We have one binding resource array per sampler type. + for (bindless_resource_type, fallback_sampler) in [ + ( + BindlessResourceType::SamplerFiltering, + &fallback_bindless_resources.filtering_sampler, + ), + ( + BindlessResourceType::SamplerNonFiltering, + &fallback_bindless_resources.non_filtering_sampler, + ), + ( + BindlessResourceType::SamplerComparison, + &fallback_bindless_resources.comparison_sampler, + ), + ] { + let mut sampler_bindings = vec![]; + + match self.samplers.get(&bindless_resource_type) { + Some(sampler_bindless_binding_array) => { + for maybe_bindless_binding in sampler_bindless_binding_array.bindings.iter() { + match *maybe_bindless_binding { + Some(ref bindless_binding) => { + sampler_bindings.push(&*bindless_binding.resource); + } + None => sampler_bindings.push(&**fallback_sampler), + } + } + } + + None => { + // Fill with a single fallback sampler. + sampler_bindings.push(&**fallback_sampler); + } + } + + if let Some(required_binding_array_size) = required_binding_array_size { + sampler_bindings.extend(iter::repeat_n( + &**fallback_sampler, + required_binding_array_size as usize - sampler_bindings.len(), + )); + } + + let binding_number = bindless_resource_type + .binding_number() + .expect("Sampler bindless resource type must have a binding number"); + + binding_resource_arrays.push(( + &**binding_number, + BindingResourceArray::Samplers(sampler_bindings), + )); + } + } + + /// Accumulates texture binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_texture_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_image: &'a FallbackImage, + required_binding_array_size: Option, + ) { + for (bindless_resource_type, fallback_image) in [ + (BindlessResourceType::Texture1d, &fallback_image.d1), + (BindlessResourceType::Texture2d, &fallback_image.d2), + ( + BindlessResourceType::Texture2dArray, + &fallback_image.d2_array, + ), + (BindlessResourceType::Texture3d, &fallback_image.d3), + (BindlessResourceType::TextureCube, &fallback_image.cube), + ( + BindlessResourceType::TextureCubeArray, + &fallback_image.cube_array, + ), + ] { + let mut texture_bindings = vec![]; + + let binding_number = bindless_resource_type + .binding_number() + .expect("Texture bindless resource type must have a binding number"); + + match self.textures.get(&bindless_resource_type) { + Some(texture_bindless_binding_array) => { + for maybe_bindless_binding in texture_bindless_binding_array.bindings.iter() { + match *maybe_bindless_binding { + Some(ref bindless_binding) => { + texture_bindings.push(&*bindless_binding.resource); + } + None => texture_bindings.push(&*fallback_image.texture_view), + } + } + } + + None => { + // Fill with a single fallback image. + texture_bindings.push(&*fallback_image.texture_view); + } + } + + if let Some(required_binding_array_size) = required_binding_array_size { + texture_bindings.extend(iter::repeat_n( + &*fallback_image.texture_view, + required_binding_array_size as usize - texture_bindings.len(), + )); + } + + binding_resource_arrays.push(( + binding_number, + BindingResourceArray::TextureViews(texture_bindings), + )); + } + } + + /// Accumulates buffer binding arrays into binding resource arrays suitable + /// for `wgpu`. + fn create_buffer_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_buffers: &'a HashMap, + bindless_descriptor: &'a BindlessDescriptor, + required_binding_array_size: Option, + ) { + for bindless_buffer_descriptor in bindless_descriptor.buffers.iter() { + let Some(buffer_bindless_binding_array) = + self.buffers.get(&bindless_buffer_descriptor.bindless_index) + else { + // This is OK, because index buffers are present in + // `BindlessDescriptor::buffers` but not in + // `BindlessDescriptor::resources`. + continue; + }; + + let fallback_buffer = fallback_buffers + .get(&bindless_buffer_descriptor.bindless_index) + .expect("Fallback buffer should exist"); + + let mut buffer_bindings: Vec<_> = buffer_bindless_binding_array + .bindings + .iter() + .map(|maybe_bindless_binding| { + let buffer = match *maybe_bindless_binding { + None => fallback_buffer, + Some(ref bindless_binding) => &bindless_binding.resource, + }; + BufferBinding { + buffer, + offset: 0, + size: None, + } + }) + .collect(); + + if let Some(required_binding_array_size) = required_binding_array_size { + buffer_bindings.extend(iter::repeat_n( + BufferBinding { + buffer: fallback_buffer, + offset: 0, + size: None, + }, + required_binding_array_size as usize - buffer_bindings.len(), + )); + } + + binding_resource_arrays.push(( + &*buffer_bindless_binding_array.binding_number, + BindingResourceArray::Buffers(buffer_bindings), + )); + } + } + + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + fn bind_group(&self) -> Option<&BindGroup> { + self.bind_group.as_ref() + } + + /// Returns the bindless index table containing the given bindless index. + fn get_bindless_index_table( + &self, + bindless_index: BindlessIndex, + ) -> Option<&MaterialBindlessIndexTable> { + let table_index = self + .bindless_index_tables + .binary_search_by(|bindless_index_table| { + if bindless_index < bindless_index_table.index_range.start { + Ordering::Less + } else if bindless_index >= bindless_index_table.index_range.end { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .ok()?; + self.bindless_index_tables.get(table_index) + } +} + +impl MaterialBindlessBindingArray +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBindingArray`] with the given binding + /// number, managing resources of the given type. + fn new( + binding_number: BindingNumber, + resource_type: BindlessResourceType, + ) -> MaterialBindlessBindingArray { + MaterialBindlessBindingArray { + binding_number, + bindings: vec![], + resource_type, + resource_to_slot: HashMap::default(), + free_slots: vec![], + len: 0, + } + } + + /// Returns the slot corresponding to the given resource, if that resource + /// is located in this binding array. + /// + /// If the resource isn't in this binding array, this method returns `None`. + fn find(&self, binding_resource_id: BindingResourceId) -> Option { + self.resource_to_slot.get(&binding_resource_id).copied() + } + + /// Inserts a bindless resource into a binding array and returns the index + /// of the slot it was inserted into. + fn insert(&mut self, binding_resource_id: BindingResourceId, resource: R) -> u32 { + match self.resource_to_slot.entry(binding_resource_id) { + bevy_platform::collections::hash_map::Entry::Occupied(o) => { + let slot = *o.get(); + + self.bindings[slot as usize] + .as_mut() + .expect("A slot in the resource_to_slot map should have a value") + .ref_count += 1; + + slot + } + bevy_platform::collections::hash_map::Entry::Vacant(v) => { + let slot = self.free_slots.pop().unwrap_or(self.len); + v.insert(slot); + + if self.bindings.len() < slot as usize + 1 { + self.bindings.resize_with(slot as usize + 1, || None); + } + self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); + + self.len += 1; + slot + } + } + } + + /// Removes a reference to an object from the slot. + /// + /// If the reference count dropped to 0 and the object was freed, this + /// method returns true. If the object was still referenced after removing + /// it, returns false. + fn remove(&mut self, slot: u32) -> bool { + let maybe_binding = &mut self.bindings[slot as usize]; + let binding = maybe_binding + .as_mut() + .expect("Attempted to free an already-freed binding"); + + binding.ref_count -= 1; + if binding.ref_count != 0 { + return false; + } + + let binding_resource_id = binding.resource.binding_resource_id(self.resource_type); + self.resource_to_slot.remove(&binding_resource_id); + + *maybe_binding = None; + self.free_slots.push(slot); + self.len -= 1; + true + } +} + +impl MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBinding`] for a freshly-added resource. + /// + /// The reference count is initialized to 1. + fn new(resource: R) -> MaterialBindlessBinding { + MaterialBindlessBinding { + resource, + ref_count: 1, + } + } +} + +/// Returns true if the material will *actually* use bindless resources or false +/// if it won't. +/// +/// This takes the platform support (or lack thereof) for bindless resources +/// into account. +pub fn material_uses_bindless_resources(render_device: &RenderDevice) -> bool +where + M: Material, +{ + M::bindless_slot_count().is_some_and(|bindless_slot_count| { + M::bindless_supported(render_device) && bindless_slot_count.resolve() > 1 + }) +} + +impl MaterialBindlessSlab { + /// Creates a new [`MaterialBindlessSlab`] for a material with the given + /// bindless descriptor. + /// + /// We use this when no existing slab could hold a material to be allocated. + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + let mut buffers = HashMap::default(); + let mut samplers = HashMap::default(); + let mut textures = HashMap::default(); + let mut data_buffers = HashMap::default(); + + for (bindless_index, bindless_resource_type) in + bindless_descriptor.resources.iter().enumerate() + { + let bindless_index = BindlessIndex(bindless_index as u32); + match *bindless_resource_type { + BindlessResourceType::None => {} + BindlessResourceType::Buffer => { + let binding_number = bindless_descriptor + .buffers + .iter() + .find(|bindless_buffer_descriptor| { + bindless_buffer_descriptor.bindless_index == bindless_index + }) + .expect( + "Bindless buffer descriptor matching that bindless index should be \ + present", + ) + .binding_number; + buffers.insert( + bindless_index, + MaterialBindlessBindingArray::new(binding_number, *bindless_resource_type), + ); + } + BindlessResourceType::DataBuffer => { + // Copy the data in. + let buffer_descriptor = bindless_descriptor + .buffers + .iter() + .find(|bindless_buffer_descriptor| { + bindless_buffer_descriptor.bindless_index == bindless_index + }) + .expect( + "Bindless buffer descriptor matching that bindless index should be \ + present", + ); + data_buffers.insert( + bindless_index, + MaterialDataBuffer::new( + buffer_descriptor.binding_number, + buffer_descriptor + .size + .expect("Data buffers should have a size") + as u32, + ), + ); + } + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => { + samplers.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => { + textures.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + } + } + + let bindless_index_tables = bindless_descriptor + .index_tables + .iter() + .map(MaterialBindlessIndexTable::new) + .collect(); + + MaterialBindlessSlab { + bind_group: None, + bindless_index_tables, + samplers, + textures, + buffers, + data_buffers, + free_slots: vec![], + live_allocation_count: 0, + allocated_resource_count: 0, + } + } +} + +pub fn init_fallback_bindless_resources(mut commands: Commands, render_device: Res) { + commands.insert_resource(FallbackBindlessResources { + filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback filtering sampler"), + ..default() + }), + non_filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback non-filtering sampler"), + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + ..default() + }), + comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback comparison sampler"), + compare: Some(CompareFunction::Always), + ..default() + }), + }); +} + +impl MaterialBindGroupNonBindlessAllocator { + /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the + /// bind groups for a single non-bindless material. + fn new(label: &'static str) -> MaterialBindGroupNonBindlessAllocator { + MaterialBindGroupNonBindlessAllocator { + label, + bind_groups: vec![], + to_prepare: HashSet::default(), + free_indices: vec![], + } + } + + /// Inserts a bind group, either unprepared or prepared, into this allocator + /// and returns a [`MaterialBindingId`]. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the bind + /// group. + fn allocate(&mut self, bind_group: MaterialNonBindlessAllocatedBindGroup) -> MaterialBindingId { + let group_id = self + .free_indices + .pop() + .unwrap_or(MaterialBindGroupIndex(self.bind_groups.len() as u32)); + if self.bind_groups.len() < *group_id as usize + 1 { + self.bind_groups + .resize_with(*group_id as usize + 1, || None); + } + + if matches!( + bind_group, + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } + ) { + self.to_prepare.insert(group_id); + } + + self.bind_groups[*group_id as usize] = Some(bind_group); + + MaterialBindingId { + group: group_id, + slot: default(), + } + } + + /// Inserts an unprepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: BindGroupLayoutDescriptor, + ) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) + } + + /// Inserts an prepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_prepared(&mut self, prepared_bind_group: PreparedBindGroup) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared { + bind_group: prepared_bind_group, + uniform_buffers: vec![], + }) + } + + /// Deallocates the bind group with the given binding ID. + fn free(&mut self, binding_id: MaterialBindingId) { + debug_assert_eq!(binding_id.slot, MaterialBindGroupSlot(0)); + debug_assert!(self.bind_groups[*binding_id.group as usize].is_some()); + self.bind_groups[*binding_id.group as usize] = None; + self.to_prepare.remove(&binding_id.group); + self.free_indices.push(binding_id.group); + } + + /// Returns a wrapper around the bind group with the given index. + fn get(&self, group: MaterialBindGroupIndex) -> Option> { + self.bind_groups[group.0 as usize] + .as_ref() + .map(|bind_group| match bind_group { + MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => { + MaterialNonBindlessSlab::Prepared(bind_group) + } + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } => { + MaterialNonBindlessSlab::Unprepared + } + }) + } + + /// Prepares any as-yet unprepared bind groups that this allocator is + /// managing. + /// + /// Unprepared bind groups can be added to this allocator with + /// [`Self::allocate_unprepared`]. Such bind groups will defer being + /// prepared until the next time this method is called. + fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + ) { + for bind_group_index in mem::take(&mut self.to_prepare) { + let Some(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) = mem::take(&mut self.bind_groups[*bind_group_index as usize]) + else { + panic!("Allocation didn't exist or was already prepared"); + }; + + // Pack any `Data` into uniform buffers. + let mut uniform_buffers = vec![]; + for (index, binding) in unprepared_bind_group.bindings.iter() { + let OwnedBindingResource::Data(ref owned_data) = *binding else { + continue; + }; + let label = format!("material uniform data {}", *index); + let uniform_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some(&label), + contents: &owned_data.0, + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + }); + uniform_buffers.push(uniform_buffer); + } + + // Create bind group entries. + let mut bind_group_entries = vec![]; + let mut uniform_buffers_iter = uniform_buffers.iter(); + for (index, binding) in unprepared_bind_group.bindings.iter() { + match *binding { + OwnedBindingResource::Data(_) => { + bind_group_entries.push(BindGroupEntry { + binding: *index, + resource: uniform_buffers_iter + .next() + .expect("We should have created uniform buffers for each `Data`") + .as_entire_binding(), + }); + } + _ => bind_group_entries.push(BindGroupEntry { + binding: *index, + resource: binding.get_binding(), + }), + } + } + + // Create the bind group. + let bind_group = render_device.create_bind_group( + self.label, + &pipeline_cache.get_bind_group_layout(&bind_group_layout), + &bind_group_entries, + ); + + self.bind_groups[*bind_group_index as usize] = + Some(MaterialNonBindlessAllocatedBindGroup::Prepared { + bind_group: PreparedBindGroup { + bindings: unprepared_bind_group.bindings, + bind_group, + }, + uniform_buffers, + }); + } + } +} + +impl<'a> MaterialSlab<'a> { + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + /// + /// You can prepare bind groups by calling + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. If the bind group + /// isn't ready, this method returns `None`. + pub fn bind_group(&self) -> Option<&'a BindGroup> { + match self.0 { + MaterialSlabImpl::Bindless(material_bindless_slab) => { + material_bindless_slab.bind_group() + } + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( + prepared_bind_group, + )) => Some(&prepared_bind_group.bind_group), + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared) => None, + } + } +} + +impl MaterialDataBuffer { + /// Creates a new [`MaterialDataBuffer`] managing a buffer of elements of + /// size `aligned_element_size` that will be bound to the given binding + /// number. + fn new(binding_number: BindingNumber, aligned_element_size: u32) -> MaterialDataBuffer { + MaterialDataBuffer { + binding_number, + buffer: RetainedRawBufferVec::new(BufferUsages::STORAGE), + aligned_element_size, + free_slots: vec![], + len: 0, + } + } + + /// Allocates a slot for a new piece of data, copies the data into that + /// slot, and returns the slot ID. + /// + /// The size of the piece of data supplied to this method must equal the + /// [`Self::aligned_element_size`] provided to [`MaterialDataBuffer::new`]. + fn insert(&mut self, data: &[u8]) -> u32 { + // Make sure the data is of the right length. + debug_assert_eq!(data.len(), self.aligned_element_size as usize); + + // Grab a slot. + let slot = self.free_slots.pop().unwrap_or(self.len); + + // Calculate the range we're going to copy to. + let start = slot as usize * self.aligned_element_size as usize; + let end = (slot as usize + 1) * self.aligned_element_size as usize; + + // Resize the buffer if necessary. + if self.buffer.len() < end { + self.buffer.reserve_internal(end); + } + while self.buffer.values().len() < end { + self.buffer.push(0); + } + + // Copy in the data. + self.buffer.values_mut()[start..end].copy_from_slice(data); + + // Mark the buffer dirty, and finish up. + self.len += 1; + self.buffer.dirty = BufferDirtyState::NeedsReserve; + slot + } + + /// Marks the given slot as free. + fn remove(&mut self, slot: u32) { + self.free_slots.push(slot); + self.len -= 1; + } +} diff --git a/vendor/bevy_pbr/src/medium.rs b/vendor/bevy_pbr/src/medium.rs new file mode 100644 index 00000000..b55fa1c1 --- /dev/null +++ b/vendor/bevy_pbr/src/medium.rs @@ -0,0 +1,579 @@ +use alloc::{borrow::Cow, sync::Arc}; +use core::f32::{self, consts::PI}; + +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, AssetApp, AssetId}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res, SystemParamItem}, +}; +use bevy_math::{ops, Curve, FloatPow, Vec3, Vec4}; +use bevy_reflect::TypePath; +use bevy_render::{ + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, + render_resource::{ + Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDataOrder, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, + TextureViewDescriptor, + }, + renderer::{RenderDevice, RenderQueue}, + RenderApp, RenderStartup, +}; +use smallvec::SmallVec; + +#[doc(hidden)] +pub struct ScatteringMediumPlugin; + +impl Plugin for ScatteringMediumPlugin { + fn build(&self, app: &mut App) { + app.init_asset::() + .add_plugins(RenderAssetPlugin::::default()); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(RenderStartup, init_scattering_medium_sampler); + } + } +} + +/// An asset that defines how a material scatters light. +/// +/// In order to calculate how light passes through a medium, +/// you need three pieces of information: +/// - how much light the medium *absorbs* per unit length +/// - how much light the medium *scatters* per unit length +/// - what *directions* the medium is likely to scatter light in. +/// +/// The first two are fairly simple, and are sometimes referred to together +/// (accurately enough) as the medium's [optical density]. +/// +/// The last, defined by a [phase function], is the most important in creating +/// the look of a medium. Our brains are very good at noticing (if unconsciously) +/// that a dust storm scatters light differently than a rain cloud, for example. +/// See the docs on [`PhaseFunction`] for more info. +/// +/// In reality, media are often composed of multiple elements that scatter light +/// independently, for Earth's atmosphere is composed of the gas itself, but also +/// suspended dust and particulate. These each scatter light differently, and are +/// distributed in different amounts at different altitudes. In a [`ScatteringMedium`], +/// these are each represented by a [`ScatteringTerm`] +/// +/// ## Technical Details +/// +/// A [`ScatteringMedium`] is represented on the GPU by a set of two LUTs, which +/// are re-created every time the asset is modified. See the docs on +/// [`GpuScatteringMedium`] for more info. +/// +/// [optical density]: https://en.wikipedia.org/wiki/Optical_Density +/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions +#[derive(TypePath, Asset, Clone)] +pub struct ScatteringMedium { + /// An optional label for the medium, used when creating the LUTs on the GPU. + pub label: Option>, + /// The resolution at which to sample the falloff distribution of each + /// scattering term. Custom or more detailed distributions may benefit + /// from a higher value, at the cost of more memory use. + pub falloff_resolution: u32, + /// The resolution at which to sample the phase function of each scattering + /// term. Custom or more detailed phase functions may benefit from a higher + /// value, at the cost of more memory use. + pub phase_resolution: u32, + /// The list of [`ScatteringTerm`]s that compose this [`ScatteringMedium`] + pub terms: SmallVec<[ScatteringTerm; 1]>, +} + +impl Default for ScatteringMedium { + fn default() -> Self { + ScatteringMedium::earthlike(256, 256) + } +} + +impl ScatteringMedium { + // Returns a scattering medium with a default label and the + // specified scattering terms. + pub fn new( + falloff_resolution: u32, + phase_resolution: u32, + terms: impl IntoIterator, + ) -> Self { + Self { + label: None, + falloff_resolution, + phase_resolution, + terms: terms.into_iter().collect(), + } + } + + // Consumes and returns this scattering medium with a new label. + pub fn with_label(self, label: impl Into>) -> Self { + Self { + label: Some(label.into()), + ..self + } + } + + // Consumes and returns this scattering medium with each scattering terms' + // densities multiplied by `multiplier`. + pub fn with_density_multiplier(mut self, multiplier: f32) -> Self { + self.terms.iter_mut().for_each(|term| { + term.absorption *= multiplier; + term.scattering *= multiplier; + }); + + self + } + + /// Returns a scattering medium representing an earthlike atmosphere. + /// + /// Uses physically-based scale heights from Earth's atmosphere, assuming + /// a 60 km atmosphere height: + /// - Rayleigh (molecular) scattering: 8 km scale height + /// - Mie (aerosol) scattering: 1.2 km scale height + pub fn earthlike(falloff_resolution: u32, phase_resolution: u32) -> Self { + Self::new( + falloff_resolution, + phase_resolution, + [ + // Rayleigh scattering Term + ScatteringTerm { + absorption: Vec3::ZERO, + scattering: Vec3::new(5.802e-6, 13.558e-6, 33.100e-6), + falloff: Falloff::Exponential { scale: 8.0 / 60.0 }, + phase: PhaseFunction::Rayleigh, + }, + // Mie scattering Term + ScatteringTerm { + absorption: Vec3::splat(3.996e-6), + scattering: Vec3::splat(0.444e-6), + falloff: Falloff::Exponential { scale: 1.2 / 60.0 }, + phase: PhaseFunction::Mie { asymmetry: 0.8 }, + }, + // Ozone scattering Term + ScatteringTerm { + absorption: Vec3::new(0.650e-6, 1.881e-6, 0.085e-6), + scattering: Vec3::ZERO, + falloff: Falloff::Tent { + center: 0.75, + width: 0.3, + }, + phase: PhaseFunction::Isotropic, + }, + ], + ) + .with_label("earthlike_atmosphere") + } +} + +/// An individual element of a [`ScatteringMedium`]. +/// +/// A [`ScatteringMedium`] can be built out of a number of simpler [`ScatteringTerm`]s, +/// which correspond to an individual element of the medium. For example, Earth's +/// atmosphere would be (roughly) composed of two [`ScatteringTerm`]s: the atmospheric +/// gases themselves, which extend to the edge of space, and suspended dust particles, +/// which are denser but lie closer to the ground. +#[derive(Default, Clone)] +pub struct ScatteringTerm { + /// This term's optical absorption density, or how much light of each wavelength + /// it absorbs per meter. + /// + /// units: m^-1 + pub absorption: Vec3, + /// This term's optical scattering density, or how much light of each wavelength + /// it scatters per meter. + /// + /// units: m^-1 + pub scattering: Vec3, + /// This term's falloff distribution. See the docs on [`Falloff`] for more info. + pub falloff: Falloff, + /// This term's [phase function], which determines the character of how it + /// scatters light. See the docs on [`PhaseFunction`] for more info. + /// + /// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions + pub phase: PhaseFunction, +} + +/// Describes how the media in a [`ScatteringTerm`] is distributed. +/// +/// This is closely related to the optical density values [`ScatteringTerm::absorption`] and +/// [`ScatteringTerm::scattering`]. Most media aren't the same density everywhere; +/// near the edge of space Earth's atmosphere is much less dense, and it absorbs +/// and scatters less light. +/// +/// [`Falloff`] determines how the density of a medium changes as a function of +/// an abstract "falloff parameter" `p`. `p = 1` denotes where the medium is the +/// densest, i.e. at the surface of the Earth, `p = 0` denotes where the medium +/// fades away completely, i.e. at the edge of space, and values between scale +/// linearly with distance, so `p = 0.5` would be halfway between the surface +/// and the edge of space. +/// +/// When processing a [`ScatteringMedium`], the `absorption` and `scattering` values +/// for each [`ScatteringTerm`] are multiplied by the value of the falloff function, `f(p)`. +#[derive(Default, Clone)] +pub enum Falloff { + /// A simple linear falloff function, which essentially + /// passes the falloff parameter through unchanged. + /// + /// f(1) = 1 + /// f(0) = 0 + /// f(p) = p + #[default] + Linear, + /// An exponential falloff function parametrized by a proportional scale. + /// When paired with an absolute "falloff distance" like the distance from + /// Earth's surface to the edge of space, this is analogous to the "height + /// scale" value common in atmospheric scattering literature, though it will + /// diverge from this for large or negative `scale` values. + /// + /// f(1) = 1 + /// f(0) = 0 + /// f(p) = (e^((1-p)/s) - e^(1/s))/(e - e^(1/s)) + Exponential { + /// The "scale" of the exponential falloff. Values closer to zero will + /// produce steeper falloff, and values farther from zero will produce + /// gentler falloff, approaching linear falloff as scale goes to `+-∞`. + /// + /// Negative values change the *concavity* of the falloff function: + /// rather than an initial narrow region of steep falloff followed by a + /// wide region of gentle falloff, there will be an initial wide region + /// of gentle falloff followed by a narrow region of steep falloff. + /// + /// domain: (-∞, ∞) + /// + /// NOTE, this function is not defined when `scale == 0`. + /// In that case, it will fall back to linear falloff. + scale: f32, + }, + /// A tent-shaped falloff function, which produces a triangular + /// peak at the center and linearly falls off to either side. + /// + /// f(`center`) = 1 + /// f(`center` +- `width` / 2) = 0 + Tent { + /// The center of the tent function peak + /// + /// domain: [0, 1] + center: f32, + /// The total width of the tent function peak + /// + /// domain: [0, 1] + width: f32, + }, + /// A falloff function defined by a custom curve. + /// + /// domain: [0, 1], + /// range: [0, 1], + Curve(Arc + Send + Sync>), +} + +impl Falloff { + /// Returns a falloff function corresponding to a custom curve. + pub fn from_curve(curve: impl Curve + Send + Sync + 'static) -> Self { + Self::Curve(Arc::new(curve)) + } + + fn sample(&self, p: f32) -> f32 { + match self { + Falloff::Linear => p, + Falloff::Exponential { scale } => { + // fill discontinuity at scale == 0, + // arbitrarily choose linear falloff + if *scale == 0.0 { + p + } else { + let s = -1.0 / scale; + let exp_p_s = ops::exp((1.0 - p) * s); + let exp_s = ops::exp(s); + (exp_p_s - exp_s) / (1.0 - exp_s) + } + } + Falloff::Tent { center, width } => (1.0 - (p - center).abs() / (0.5 * width)).max(0.0), + Falloff::Curve(curve) => curve.sample(p).unwrap_or(0.0), + } + } +} + +/// Describes how a [`ScatteringTerm`] scatters light in different directions. +/// +/// A [phase function] is a function `f: [-1, 1] -> [0, ∞)`, symmetric about `x=0` +/// whose input is the cosine of the angle between an incoming light direction and +/// and outgoing light direction, and whose output is the proportion of the incoming +/// light that is actually scattered in that direction. +/// +/// The phase function has an important effect on the "look" of a medium in a scene. +/// Media consisting of particles of a different size or shape scatter light differently, +/// and our brains are very good at telling the difference. A dust cloud, which might +/// correspond roughly to `PhaseFunction::Mie { asymmetry: 0.8 }`, looks quite different +/// from the rest of the sky (atmospheric gases), which correspond to `PhaseFunction::Rayleigh` +/// +/// [phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions +#[derive(Clone)] +pub enum PhaseFunction { + /// A phase function that scatters light evenly in all directions. + Isotropic, + + /// A phase function representing [Rayleigh scattering]. + /// + /// Rayleigh scattering occurs naturally for particles much smaller than + /// the wavelengths of visible light, such as gas molecules in the atmosphere. + /// It's generally wavelength-dependent, where shorter wavelengths are scattered + /// more strongly, so [scattering](ScatteringTerm::scattering) should have + /// higher values for blue than green and green than red. Particles that + /// participate in Rayleigh scattering don't absorb any light, either. + /// + /// [Rayleigh scattering]: https://en.wikipedia.org/wiki/Rayleigh_scattering + Rayleigh, + + /// The [Henyey-Greenstein phase function], which approximates [Mie scattering]. + /// + /// Mie scattering occurs naturally for spherical particles of dust + /// and aerosols roughly the same size as the wavelengths of visible light, + /// so it's useful for representing dust or sea spray. It's generally + /// wavelength-independent, so [absorption](ScatteringTerm::absorption) + /// and [scattering](ScatteringTerm::scattering) should be set to a greyscale value. + /// + /// [Mie scattering]: https://en.wikipedia.org/wiki/Mie_scattering + /// [Henyey-Greenstein phase function]: https://www.oceanopticsbook.info/view/scattering/level-2/the-henyey-greenstein-phase-function + Mie { + /// Whether the Mie scattering function is biased towards scattering + /// light forwards (asymmetry > 0) or backwards (asymmetry < 0). + /// + /// domain: [-1, 1] + asymmetry: f32, + }, + + /// A phase function defined by a custom curve, where the input + /// is the cosine of the angle between the incoming light ray + /// and the scattered light ray, and the output is the fraction + /// of the incoming light scattered in that direction. + /// + /// Note: it's important for photorealism that the phase function + /// be *energy conserving*, meaning that in total no more light can + /// be scattered than actually entered the medium. For this to be + /// the case, the integral of the phase function over its domain must + /// be equal to 1/2π. + /// + /// 1 + /// ∫ p(x) dx = 1/2π + /// -1 + /// + /// domain: [-1, 1] + /// range: [0, 1] + Curve(Arc + Send + Sync>), +} + +impl PhaseFunction { + /// A phase function defined by a custom curve. + pub fn from_curve(curve: impl Curve + Send + Sync + 'static) -> Self { + Self::Curve(Arc::new(curve)) + } + + fn sample(&self, neg_l_dot_v: f32) -> f32 { + const FRAC_4_PI: f32 = 0.25 / PI; + const FRAC_3_16_PI: f32 = 0.1875 / PI; + match self { + PhaseFunction::Isotropic => FRAC_4_PI, + PhaseFunction::Rayleigh => FRAC_3_16_PI * (1.0 + neg_l_dot_v * neg_l_dot_v), + PhaseFunction::Mie { asymmetry } => { + let denom = 1.0 + asymmetry.squared() - 2.0 * asymmetry * neg_l_dot_v; + FRAC_4_PI * (1.0 - asymmetry.squared()) / (denom * denom.sqrt()) + } + PhaseFunction::Curve(curve) => curve.sample(neg_l_dot_v).unwrap_or(0.0), + } + } +} + +impl Default for PhaseFunction { + fn default() -> Self { + Self::Mie { asymmetry: 0.8 } + } +} + +/// The GPU representation of a [`ScatteringMedium`]. +pub struct GpuScatteringMedium { + /// The terms of the scattering medium. + pub terms: SmallVec<[ScatteringTerm; 1]>, + /// The resolution at which to sample the falloff distribution of each + /// scattering term. + pub falloff_resolution: u32, + /// The resolution at which to sample the phase function of each + /// scattering term. + pub phase_resolution: u32, + /// The `density_lut`, a 2D `falloff_resolution x 2` LUT which contains the + /// medium's optical density with respect to the atmosphere's "falloff parameter", + /// a linear value which is 1.0 at the planet's surface and 0.0 at the edge of + /// space. The first and second rows correspond to absorption density and + /// scattering density respectively. + pub density_lut: Texture, + /// The default [`TextureView`] of the `density_lut` + pub density_lut_view: TextureView, + /// The `scattering_lut`, a 2D `falloff_resolution x phase_resolution` LUT which + /// contains the medium's scattering density multiplied by the phase function, with + /// the U axis corresponding to the falloff parameter and the V axis corresponding + /// to `neg_LdotV * 0.5 + 0.5`, where `neg_LdotV` is the dot product of the light + /// direction and the incoming view vector. + pub scattering_lut: Texture, + /// The default [`TextureView`] of the `scattering_lut` + pub scattering_lut_view: TextureView, +} + +impl RenderAsset for GpuScatteringMedium { + type SourceAsset = ScatteringMedium; + + type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + (render_device, render_queue): &mut SystemParamItem, + _previous_asset: Option<&Self>, + ) -> Result> { + let mut density: Vec = + Vec::with_capacity(2 * source_asset.falloff_resolution as usize); + + density.extend((0..source_asset.falloff_resolution).map(|i| { + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + + source_asset + .terms + .iter() + .map(|term| term.absorption.extend(0.0) * term.falloff.sample(falloff)) + .sum::() + })); + + density.extend((0..source_asset.falloff_resolution).map(|i| { + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + + source_asset + .terms + .iter() + .map(|term| term.scattering.extend(0.0) * term.falloff.sample(falloff)) + .sum::() + })); + + let mut scattering: Vec = Vec::with_capacity( + source_asset.falloff_resolution as usize * source_asset.phase_resolution as usize, + ); + + scattering.extend( + (0..source_asset.falloff_resolution * source_asset.phase_resolution).map(|raw_i| { + let i = raw_i % source_asset.phase_resolution; + let j = raw_i / source_asset.phase_resolution; + let falloff = (i as f32 + 0.5) / source_asset.falloff_resolution as f32; + let phase = (j as f32 + 0.5) / source_asset.phase_resolution as f32; + let neg_l_dot_v = phase * 2.0 - 1.0; + + source_asset + .terms + .iter() + .map(|term| { + term.scattering.extend(0.0) + * term.falloff.sample(falloff) + * term.phase.sample(neg_l_dot_v) + }) + .sum::() + }), + ); + + let density_lut = render_device.create_texture_with_data( + render_queue, + &TextureDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_density_lut", label)) + .as_deref() + .or(Some("scattering_medium_density_lut")), + size: Extent3d { + width: source_asset.falloff_resolution, + height: 2, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(density.as_slice()), + ); + + let density_lut_view = density_lut.create_view(&TextureViewDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_density_lut_view", label)) + .as_deref() + .or(Some("scattering_medium_density_lut_view")), + ..Default::default() + }); + + let scattering_lut = render_device.create_texture_with_data( + render_queue, + &TextureDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_scattering_lut", label)) + .as_deref() + .or(Some("scattering_medium_scattering_lut")), + size: Extent3d { + width: source_asset.falloff_resolution, + height: source_asset.phase_resolution, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(scattering.as_slice()), + ); + + let scattering_lut_view = scattering_lut.create_view(&TextureViewDescriptor { + label: source_asset + .label + .as_deref() + .map(|label| format!("{}_scattering_lut", label)) + .as_deref() + .or(Some("scattering_medium_scattering_lut_view")), + ..Default::default() + }); + + Ok(Self { + terms: source_asset.terms, + falloff_resolution: source_asset.falloff_resolution, + phase_resolution: source_asset.phase_resolution, + density_lut, + density_lut_view, + scattering_lut, + scattering_lut_view, + }) + } +} + +/// The default sampler for all scattering media LUTs. +/// +/// Just a bilinear clamp-to-edge sampler, nothing fancy. +#[derive(Resource)] +pub struct ScatteringMediumSampler(Sampler); + +impl ScatteringMediumSampler { + pub fn sampler(&self) -> &Sampler { + &self.0 + } +} + +fn init_scattering_medium_sampler(mut commands: Commands, render_device: Res) { + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("scattering_medium_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + + commands.insert_resource(ScatteringMediumSampler(sampler)); +} diff --git a/vendor/bevy_pbr/src/mesh_material.rs b/vendor/bevy_pbr/src/mesh_material.rs new file mode 100644 index 00000000..46520ccd --- /dev/null +++ b/vendor/bevy_pbr/src/mesh_material.rs @@ -0,0 +1,75 @@ +use crate::Material; +use bevy_asset::{AsAssetId, AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use derive_more::derive::From; + +/// A [material](Material) used for rendering a [`Mesh3d`]. +/// +/// See [`Material`] for general information about 3D materials and how to implement your own materials. +/// +/// [`Mesh3d`]: bevy_mesh::Mesh3d +/// +/// # Example +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_mesh::{Mesh, Mesh3d}; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// // Spawn an entity with a mesh using `StandardMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(StandardMaterial { +/// base_color: RED.into(), +/// ..Default::default() +/// })), +/// )); +/// } +/// ``` +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct MeshMaterial3d(pub Handle); + +impl Default for MeshMaterial3d { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl PartialEq for MeshMaterial3d { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for MeshMaterial3d {} + +impl From> for AssetId { + fn from(material: MeshMaterial3d) -> Self { + material.id() + } +} + +impl From<&MeshMaterial3d> for AssetId { + fn from(material: &MeshMaterial3d) -> Self { + material.id() + } +} + +impl AsAssetId for MeshMaterial3d { + type Asset = M; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} diff --git a/vendor/bevy_pbr/src/meshlet/asset.rs b/vendor/bevy_pbr/src/meshlet/asset.rs new file mode 100644 index 00000000..38b02703 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/asset.rs @@ -0,0 +1,321 @@ +use alloc::sync::Arc; +use bevy_asset::{ + io::{Reader, Writer}, + saver::{AssetSaver, SavedAsset}, + Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, +}; +use bevy_math::{Vec2, Vec3}; +use bevy_reflect::TypePath; +use bevy_render::render_resource::ShaderType; +use bevy_tasks::block_on; +use bytemuck::{Pod, Zeroable}; +use lz4_flex::frame::{FrameDecoder, FrameEncoder}; +use std::io::{Read, Write}; +use thiserror::Error; + +/// Unique identifier for the [`MeshletMesh`] asset format. +const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668; + +/// The current version of the [`MeshletMesh`] asset format. +pub const MESHLET_MESH_ASSET_VERSION: u64 = 3; + +/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. +/// +/// A [`bevy_mesh::Mesh`] can be converted to a [`MeshletMesh`] using `MeshletMesh::from_mesh` when the `meshlet_processor` cargo feature is enabled. +/// The conversion step is very slow, and is meant to be ran once ahead of time, and not during runtime. This type of mesh is not suitable for +/// dynamically generated geometry. +/// +/// There are restrictions on the [`Material`](`crate::Material`) functionality that can be used with this type of mesh. +/// * Materials have no control over the vertex shader or vertex attributes. +/// * Materials must be opaque. Transparent, alpha masked, and transmissive materials are not supported. +/// * Do not use normal maps baked from higher-poly geometry. Use the high-poly geometry directly and skip the normal map. +/// * If additional detail is needed, a smaller tiling normal map not baked from a mesh is ok. +/// * Material shaders must not use builtin functions that automatically calculate derivatives . +/// * Performing manual arithmetic on texture coordinates (UVs) is forbidden. Use the chain-rule version of arithmetic functions instead (TODO: not yet implemented). +/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes. +/// * Materials must use the [`Material::meshlet_mesh_fragment_shader`](`crate::Material::meshlet_mesh_fragment_shader`) method (and similar variants for prepass/deferred shaders) +/// which requires certain shader patterns that differ from the regular material shaders. +/// +/// See also [`MeshletMesh3d`](`super::MeshletMesh3d`) and [`MeshletPlugin`](`super::MeshletPlugin`). +#[derive(Asset, TypePath, Clone)] +pub struct MeshletMesh { + /// Quantized and bitstream-packed vertex positions for meshlet vertices. + pub(crate) vertex_positions: Arc<[u32]>, + /// Octahedral-encoded and 2x16snorm packed normals for meshlet vertices. + pub(crate) vertex_normals: Arc<[u32]>, + /// Uncompressed vertex texture coordinates for meshlet vertices. + pub(crate) vertex_uvs: Arc<[Vec2]>, + /// Triangle indices for meshlets. + pub(crate) indices: Arc<[u8]>, + /// The BVH8 used for culling and LOD selection of the meshlets. The root is at index 0. + pub(crate) bvh: Arc<[BvhNode]>, + /// The list of meshlets making up this mesh. + pub(crate) meshlets: Arc<[Meshlet]>, + /// Spherical bounding volumes. + pub(crate) meshlet_cull_data: Arc<[MeshletCullData]>, + /// The tight AABB of the meshlet mesh, used for frustum and occlusion culling at the instance + /// level. + pub(crate) aabb: MeshletAabb, + /// The depth of the culling BVH, used to determine the number of dispatches at runtime. + pub(crate) bvh_depth: u32, +} + +/// A single BVH8 node in the BVH used for culling and LOD selection of a [`MeshletMesh`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct BvhNode { + /// The tight AABBs of this node's children, used for frustum and occlusion during BVH + /// traversal. + pub aabbs: [MeshletAabbErrorOffset; 8], + /// The LOD bounding spheres of this node's children, used for LOD selection during BVH + /// traversal. + pub lod_bounds: [MeshletBoundingSphere; 8], + /// If `u8::MAX`, it indicates that the child of each children is a BVH node, otherwise it is the number of meshlets in the group. + pub child_counts: [u8; 8], + pub _padding: [u32; 2], +} + +/// A single meshlet within a [`MeshletMesh`]. +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct Meshlet { + /// The bit offset within the parent mesh's [`MeshletMesh::vertex_positions`] buffer where the vertex positions for this meshlet begin. + pub start_vertex_position_bit: u32, + /// The offset within the parent mesh's [`MeshletMesh::vertex_normals`] and [`MeshletMesh::vertex_uvs`] buffers + /// where non-position vertex attributes for this meshlet begin. + pub start_vertex_attribute_id: u32, + /// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin. + pub start_index_id: u32, + /// The amount of vertices in this meshlet (minus one to fit 256 in a u8). + pub vertex_count_minus_one: u8, + /// The amount of triangles in this meshlet. + pub triangle_count: u8, + /// Unused. + pub padding: u16, + /// Number of bits used to store the X channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_x: u8, + /// Number of bits used to store the Y channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_y: u8, + /// Number of bits used to store the Z channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_z: u8, + /// Power of 2 factor used to quantize vertex positions within this meshlet. + pub vertex_position_quantization_factor: u8, + /// Minimum quantized X channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_x: f32, + /// Minimum quantized Y channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_y: f32, + /// Minimum quantized Z channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_z: f32, +} + +/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MeshletCullData { + /// Tight bounding box, used for frustum and occlusion culling for this meshlet. + pub aabb: MeshletAabbErrorOffset, + /// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view. + pub lod_group_sphere: MeshletBoundingSphere, +} + +/// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabb { + pub center: Vec3, + pub half_extent: Vec3, +} + +// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabbErrorOffset { + pub center: Vec3, + pub error: f32, + pub half_extent: Vec3, + pub child_offset: u32, +} + +/// A spherical bounding volume used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct MeshletBoundingSphere { + pub center: Vec3, + pub radius: f32, +} + +/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] +pub struct MeshletMeshSaver; + +impl AssetSaver for MeshletMeshSaver { + type Asset = MeshletMesh; + type Settings = (); + type OutputLoader = MeshletMeshLoader; + type Error = MeshletMeshSaveOrLoadError; + + async fn save( + &self, + writer: &mut Writer, + asset: SavedAsset<'_, MeshletMesh>, + _settings: &(), + ) -> Result<(), MeshletMeshSaveOrLoadError> { + // Write asset magic number + writer + .write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes()) + .await?; + + // Write asset version + writer + .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) + .await?; + + writer.write_all(bytemuck::bytes_of(&asset.aabb)).await?; + writer + .write_all(bytemuck::bytes_of(&asset.bvh_depth)) + .await?; + + // Compress and write asset data + let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); + write_slice(&asset.vertex_positions, &mut writer)?; + write_slice(&asset.vertex_normals, &mut writer)?; + write_slice(&asset.vertex_uvs, &mut writer)?; + write_slice(&asset.indices, &mut writer)?; + write_slice(&asset.bvh, &mut writer)?; + write_slice(&asset.meshlets, &mut writer)?; + write_slice(&asset.meshlet_cull_data, &mut writer)?; + // BUG: Flushing helps with an async_fs bug, but it still fails sometimes. https://github.com/smol-rs/async-fs/issues/45 + // ERROR bevy_asset::server: Failed to load asset with asset loader MeshletMeshLoader: failed to fill whole buffer + writer.flush()?; + writer.finish()?; + + Ok(()) + } +} + +/// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] +pub struct MeshletMeshLoader; + +impl AssetLoader for MeshletMeshLoader { + type Asset = MeshletMesh; + type Settings = (); + type Error = MeshletMeshSaveOrLoadError; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &(), + _load_context: &mut LoadContext<'_>, + ) -> Result { + // Load and check magic number + let magic = async_read_u64(reader).await?; + if magic != MESHLET_MESH_ASSET_MAGIC { + return Err(MeshletMeshSaveOrLoadError::WrongFileType); + } + + // Load and check asset version + let version = async_read_u64(reader).await?; + if version != MESHLET_MESH_ASSET_VERSION { + return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); + } + + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let aabb = bytemuck::cast(bytes); + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let bvh_depth = u32::from_le_bytes(bytes); + + // Load and decompress asset data + let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); + let vertex_positions = read_slice(reader)?; + let vertex_normals = read_slice(reader)?; + let vertex_uvs = read_slice(reader)?; + let indices = read_slice(reader)?; + let bvh = read_slice(reader)?; + let meshlets = read_slice(reader)?; + let meshlet_cull_data = read_slice(reader)?; + + Ok(MeshletMesh { + vertex_positions, + vertex_normals, + vertex_uvs, + indices, + bvh, + meshlets, + meshlet_cull_data, + aabb, + bvh_depth, + }) + } + + fn extensions(&self) -> &[&str] { + &["meshlet_mesh"] + } +} + +#[derive(Error, Debug)] +pub enum MeshletMeshSaveOrLoadError { + #[error("file was not a MeshletMesh asset")] + WrongFileType, + #[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")] + WrongVersion { found: u64 }, + #[error("failed to compress or decompress asset data")] + CompressionOrDecompression(#[from] lz4_flex::frame::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +async fn async_read_u64(reader: &mut dyn Reader) -> Result { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes).await?; + Ok(u64::from_le_bytes(bytes)) +} + +fn read_u64(reader: &mut dyn Read) -> Result { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes)?; + Ok(u64::from_le_bytes(bytes)) +} + +fn write_slice( + field: &[T], + writer: &mut dyn Write, +) -> Result<(), MeshletMeshSaveOrLoadError> { + writer.write_all(&(field.len() as u64).to_le_bytes())?; + writer.write_all(bytemuck::cast_slice(field))?; + Ok(()) +} + +fn read_slice(reader: &mut dyn Read) -> Result, std::io::Error> { + let len = read_u64(reader)? as usize; + + let mut data: Arc<[T]> = core::iter::repeat_with(T::zeroed).take(len).collect(); + let slice = Arc::get_mut(&mut data).unwrap(); + reader.read_exact(bytemuck::cast_slice_mut(slice))?; + + Ok(data) +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncWriteSyncAdapter<'a>(&'a mut Writer); + +impl Write for AsyncWriteSyncAdapter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + block_on(self.0.write(buf)) + } + + fn flush(&mut self) -> std::io::Result<()> { + block_on(self.0.flush()) + } +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader); + +impl Read for AsyncReadSyncAdapter<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + block_on(self.0.read(buf)) + } +} diff --git a/vendor/bevy_pbr/src/meshlet/clear_visibility_buffer.wgsl b/vendor/bevy_pbr/src/meshlet/clear_visibility_buffer.wgsl new file mode 100644 index 00000000..5956921c --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/clear_visibility_buffer.wgsl @@ -0,0 +1,18 @@ +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#endif +var view_size: vec2; + +@compute +@workgroup_size(16, 16, 1) +fn clear_visibility_buffer(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= view_size) { return; } + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0lu)); +#else + textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0u)); +#endif +} diff --git a/vendor/bevy_pbr/src/meshlet/cull_bvh.wgsl b/vendor/bevy_pbr/src/meshlet/cull_bvh.wgsl new file mode 100644 index 00000000..b0bbb5f8 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/cull_bvh.wgsl @@ -0,0 +1,110 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + get_aabb_child_offset, + constants, + meshlet_bvh_nodes, + meshlet_bvh_cull_count_read, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_meshlet_cull_count_early, + meshlet_meshlet_cull_count_late, + meshlet_meshlet_cull_dispatch_early, + meshlet_meshlet_cull_dispatch_late, + meshlet_meshlet_cull_queue, + meshlet_second_pass_bvh_count, + meshlet_second_pass_bvh_dispatch, + meshlet_second_pass_bvh_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +@compute +@workgroup_size(128, 1, 1) // 8 threads per node, 16 nodes per workgroup +fn cull_bvh(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the queue ID for this thread + let dispatch_id = global_invocation_id.x; + var node = dispatch_id >> 3u; + let subnode = dispatch_id & 7u; + if node >= meshlet_bvh_cull_count_read { return; } + + node = select(node, constants.rightmost_slot - node, constants.read_from_front == 0u); + let instanced_offset = meshlet_bvh_cull_queue[node]; + let instance_id = instanced_offset.instance_id; + let bvh_node = &meshlet_bvh_nodes[instanced_offset.offset]; + + var aabb_error_offset = (*bvh_node).aabbs[subnode]; + let aabb = get_aabb(&aabb_error_offset); + let parent_error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*bvh_node).lod_bounds[subnode]; + + let parent_is_imperceptible = lod_error_is_imperceptible(lod_sphere, parent_error, instance_id); + // Error and frustum cull, in both passes + if parent_is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + let child_offset = get_aabb_child_offset(&aabb_error_offset); + let index = subnode >> 2u; + let bit_offset = subnode & 3u; + let packed_child_count = (*bvh_node).child_counts[index]; + let child_count = extractBits(packed_child_count, bit_offset * 8u, 8u); + var value = InstancedOffset(instance_id, child_offset); + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + if child_count == 255u { + let id = atomicAdd(&meshlet_second_pass_bvh_count, 1u); + meshlet_second_pass_bvh_queue[id] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_second_pass_bvh_dispatch.x, 1u); + } + } else { + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); + } +#endif + return; + } + + // If we pass, push the children to the next BVH cull + if child_count == 255u { + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + let index = select(constants.rightmost_slot - id, id, constants.read_from_front == 0u); + meshlet_bvh_cull_queue[index] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } + } else { +#ifdef MESHLET_FIRST_CULLING_PASS + let base = atomicAdd(&meshlet_meshlet_cull_count_early, child_count); + let end = base + child_count; + for (var i = base; i < end; i++) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (end + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_early.x, req); +#else + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); +#endif + } +} diff --git a/vendor/bevy_pbr/src/meshlet/cull_clusters.wgsl b/vendor/bevy_pbr/src/meshlet/cull_clusters.wgsl new file mode 100644 index 00000000..85cbc065 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/cull_clusters.wgsl @@ -0,0 +1,93 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + constants, + view, + meshlet_instance_uniforms, + meshlet_cull_data, + meshlet_software_raster_indirect_args, + meshlet_hardware_raster_indirect_args, + meshlet_previous_raster_counts, + meshlet_raster_clusters, + meshlet_meshlet_cull_count_read, + meshlet_meshlet_cull_count_write, + meshlet_meshlet_cull_dispatch, + meshlet_meshlet_cull_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + ScreenAabb, + project_aabb, + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} +#import bevy_render::maths::affine3_to_square + +@compute +@workgroup_size(128, 1, 1) // 1 cluster per thread +fn cull_clusters(@builtin(global_invocation_id) global_invocation_id: vec3) { + if global_invocation_id.x >= meshlet_meshlet_cull_count_read { return; } + +#ifdef MESHLET_FIRST_CULLING_PASS + let meshlet_id = global_invocation_id.x; +#else + let meshlet_id = constants.rightmost_slot - global_invocation_id.x; +#endif + let instanced_offset = meshlet_meshlet_cull_queue[meshlet_id]; + let instance_id = instanced_offset.instance_id; + let cull_data = &meshlet_cull_data[instanced_offset.offset]; + var aabb_error_offset = (*cull_data).aabb; + let aabb = get_aabb(&aabb_error_offset); + let error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*cull_data).lod_group_sphere; + + let is_imperceptible = lod_error_is_imperceptible(lod_sphere, error, instance_id); + // Error and frustum cull, in both passes + if !is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_meshlet_cull_count_write, 1u); + let value = InstancedOffset(instance_id, instanced_offset.offset); + meshlet_meshlet_cull_queue[constants.rightmost_slot - id] = value; + if ((id & 127u) == 0) { + atomicAdd(&meshlet_meshlet_cull_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, rasterize the meshlet + // Check how big the cluster is in screen space + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let projection = view.clip_from_world; + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + var sw_raster = project_aabb(clip_from_local, near, aabb, &screen_aabb); + if sw_raster { + let aabb_size = (screen_aabb.max.xy - screen_aabb.min.xy) * view.viewport.zw; + sw_raster = all(aabb_size <= vec2(64.0)); + } + + var buffer_slot: u32; + if sw_raster { + // Append this cluster to the list for software rasterization + buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u); + buffer_slot += meshlet_previous_raster_counts[0]; + } else { + // Append this cluster to the list for hardware rasterization + buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u); + buffer_slot += meshlet_previous_raster_counts[1]; + buffer_slot = constants.rightmost_slot - buffer_slot; + } + meshlet_raster_clusters[buffer_slot] = InstancedOffset(instance_id, instanced_offset.offset); +} diff --git a/vendor/bevy_pbr/src/meshlet/cull_instances.wgsl b/vendor/bevy_pbr/src/meshlet/cull_instances.wgsl new file mode 100644 index 00000000..5d14d10b --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/cull_instances.wgsl @@ -0,0 +1,76 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + constants, + meshlet_view_instance_visibility, + meshlet_instance_aabbs, + meshlet_instance_bvh_root_nodes, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_second_pass_instance_count, + meshlet_second_pass_instance_dispatch, + meshlet_second_pass_instance_candidates, +} +#import bevy_pbr::meshlet_cull_shared::{ + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +fn instance_count() -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return constants.scene_instance_count; +#else + return meshlet_second_pass_instance_count; +#endif +} + +fn map_instance_id(id: u32) -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return id; +#else + return meshlet_second_pass_instance_candidates[id]; +#endif +} + +fn should_cull_instance(instance_id: u32) -> bool { + let bit_offset = instance_id >> 5u; + let packed_visibility = meshlet_view_instance_visibility[instance_id & 31u]; + return bool(extractBits(packed_visibility, bit_offset, 1u)); +} + +@compute +@workgroup_size(128, 1, 1) // 1 instance per thread +fn cull_instances(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the instance ID for this thread + let dispatch_id = global_invocation_id.x; + if dispatch_id >= instance_count() { return; } + + let instance_id = map_instance_id(dispatch_id); + let aabb = meshlet_instance_aabbs[instance_id]; + + // Visibility and frustum cull, but only in the first pass +#ifdef MESHLET_FIRST_CULLING_PASS + if should_cull_instance(instance_id) || !aabb_in_frustum(aabb, instance_id) { return; } +#endif + + // If we pass, try occlusion culling + // If this instance was occluded, push it to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_second_pass_instance_count, 1u); + meshlet_second_pass_instance_candidates[id] = instance_id; + if ((id & 127u) == 0u) { + atomicAdd(&meshlet_second_pass_instance_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, push the instance's root node to BVH cull + let root_node = meshlet_instance_bvh_root_nodes[instance_id]; + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + meshlet_bvh_cull_queue[id] = InstancedOffset(instance_id, root_node); + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } +} diff --git a/vendor/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl b/vendor/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl new file mode 100644 index 00000000..243a4009 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/dummy_visibility_buffer_resolve.wgsl @@ -0,0 +1,4 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +/// Dummy shader to prevent naga_oil from complaining about missing imports when the MeshletPlugin is not loaded, +/// as naga_oil tries to resolve imports even if they're behind an #ifdef. diff --git a/vendor/bevy_pbr/src/meshlet/fill_counts.wgsl b/vendor/bevy_pbr/src/meshlet/fill_counts.wgsl new file mode 100644 index 00000000..f319e395 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/fill_counts.wgsl @@ -0,0 +1,35 @@ +/// Copies the counts of meshlets in the hardware and software buckets, resetting the counters in the process. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: u32, + first_vertex: u32, + first_instance: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(2) var meshlet_previous_raster_counts: array; +#ifdef MESHLET_2D_DISPATCH +@group(0) @binding(3) var meshlet_software_raster_cluster_count: u32; +#endif + +@compute +@workgroup_size(1, 1, 1) +fn fill_counts() { +#ifdef MESHLET_2D_DISPATCH + meshlet_previous_raster_counts[0] += meshlet_software_raster_cluster_count; +#else + meshlet_previous_raster_counts[0] += meshlet_software_raster_indirect_args.x; +#endif + meshlet_software_raster_indirect_args.x = 0; + + meshlet_previous_raster_counts[1] += meshlet_hardware_raster_indirect_args.instance_count; + meshlet_hardware_raster_indirect_args.instance_count = 0; +} diff --git a/vendor/bevy_pbr/src/meshlet/from_mesh.rs b/vendor/bevy_pbr/src/meshlet/from_mesh.rs new file mode 100644 index 00000000..23de9790 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/from_mesh.rs @@ -0,0 +1,1091 @@ +use crate::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData}; + +use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh}; +use alloc::borrow::Cow; +use bevy_math::{ + bounding::{Aabb3d, BoundingSphere, BoundingVolume}, + ops::log2, + IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use bevy_render::render_resource::PrimitiveTopology; +use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice}; +use bitvec::{order::Lsb0, vec::BitVec, view::BitView}; +use core::{f32, ops::Range}; +use itertools::Itertools; +use meshopt::{ + build_meshlets, ffi::meshopt_Meshlet, generate_position_remap, + simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, +}; +use metis::{option::Opt, Graph}; +use smallvec::SmallVec; +use thiserror::Error; +use tracing::debug_span; + +// Aim to have 8 meshlets per group +const TARGET_MESHLETS_PER_GROUP: usize = 8; +// Reject groups that keep over 60% of their original triangles. We'd much rather render a few +// extra triangles than create too many meshlets, increasing cull overhead. +const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.60; + +/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`]. +/// +/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4). +pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4; + +const CENTIMETERS_PER_METER: f32 = 100.0; + +impl MeshletMesh { + /// Process a [`Mesh`] to generate a [`MeshletMesh`]. + /// + /// This process is very slow, and should be done ahead of time, and not at runtime. + /// + /// # Requirements + /// + /// This function requires the `meshlet_processor` cargo feature. + /// + /// The input mesh must: + /// 1. Use [`PrimitiveTopology::TriangleList`] + /// 2. Use indices + /// 3. Have the exact following set of vertex attributes: `{POSITION, NORMAL, UV_0}` (tangents can be used in material shaders, but are calculated at runtime and are not stored in the mesh) + /// + /// # Vertex precision + /// + /// `vertex_position_quantization_factor` is the amount of precision to use when quantizing vertex positions. + /// + /// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`. + /// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter. + /// + /// Use [`MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed. + /// + /// To ensure that two different meshes do not have cracks between them when placed directly next to each other: + /// * Use the same quantization factor when converting each mesh to a meshlet mesh + /// * Ensure that their [`bevy_transform::components::Transform::translation`]s are a multiple of 1/2^x centimeters (note that translations are in meters) + /// * Ensure that their [`bevy_transform::components::Transform::scale`]s are the same + /// * Ensure that their [`bevy_transform::components::Transform::rotation`]s are a multiple of 90 degrees + pub fn from_mesh( + mesh: &Mesh, + vertex_position_quantization_factor: u8, + ) -> Result { + let s = debug_span!("build meshlet mesh"); + let _e = s.enter(); + + // Validate mesh format + let indices = validate_input_mesh(mesh)?; + + // Get meshlet vertices + let vertex_buffer = mesh.create_packed_vertex_buffer_data(); + let vertex_stride = mesh.get_vertex_size() as usize; + let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap(); + let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]); + + // Generate a position-only vertex buffer for determining triangle/meshlet connectivity + let position_only_vertex_remap = generate_position_remap(&vertices); + + // Split the mesh into an initial list of meshlets (LOD 0) + let (mut meshlets, mut cull_data) = + compute_meshlets(&indices, &vertices, &position_only_vertex_remap, None); + + let mut vertex_locks = vec![false; vertices.vertex_count]; + + // Build further LODs + let mut bvh = BvhBuilder::default(); + let mut all_groups = Vec::new(); + let mut simplification_queue: Vec<_> = (0..meshlets.len() as u32).collect(); + let mut stuck = Vec::new(); + while !simplification_queue.is_empty() { + let s = debug_span!("simplify lod", meshlets = simplification_queue.len()); + let _e = s.enter(); + + // For each meshlet build a list of connected meshlets (meshlets that share a vertex) + let connected_meshlets_per_meshlet = find_connected_meshlets( + &simplification_queue, + &meshlets, + &position_only_vertex_remap, + ); + + // Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP, + // grouping meshlets with a high number of shared vertices + let groups = group_meshlets( + &simplification_queue, + &cull_data, + &connected_meshlets_per_meshlet, + ); + simplification_queue.clear(); + + // Lock borders between groups to prevent cracks when simplifying + lock_group_borders( + &mut vertex_locks, + &groups, + &meshlets, + &position_only_vertex_remap, + ); + + let simplified = groups.par_chunk_map(AsyncComputeTaskPool::get(), 1, |_, groups| { + let mut group = groups[0].clone(); + + // If the group only has a single meshlet we can't simplify it + if group.meshlets.len() == 1 { + return Err(group); + } + + let s = debug_span!("simplify group", meshlets = group.meshlets.len()); + let _e = s.enter(); + + // Simplify the group to ~50% triangle count + let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group( + &group, + &meshlets, + &vertices, + vertex_normals, + vertex_stride, + &vertex_locks, + ) else { + // Couldn't simplify the group enough + return Err(group); + }; + + // Force the group error to be atleast as large as all of its constituent meshlet's + // individual errors. + for &id in group.meshlets.iter() { + group_error = group_error.max(cull_data[id as usize].error); + } + group.parent_error = group_error; + + // Build new meshlets using the simplified group + let new_meshlets = compute_meshlets( + &simplified_group_indices, + &vertices, + &position_only_vertex_remap, + Some((group.lod_bounds, group.parent_error)), + ); + + Ok((group, new_meshlets)) + }); + + let first_group = all_groups.len() as u32; + let mut passed_tris = 0; + let mut stuck_tris = 0; + for group in simplified { + match group { + Ok((group, (new_meshlets, new_cull_data))) => { + let start = meshlets.len(); + merge_meshlets(&mut meshlets, new_meshlets); + cull_data.extend(new_cull_data); + let end = meshlets.len(); + let new_meshlet_ids = start as u32..end as u32; + + passed_tris += triangles_in_meshlets(&meshlets, new_meshlet_ids.clone()); + simplification_queue.extend(new_meshlet_ids); + all_groups.push(group); + } + Err(group) => { + stuck_tris += + triangles_in_meshlets(&meshlets, group.meshlets.iter().copied()); + stuck.push(group); + } + } + } + + // If we have enough triangles that passed, we can retry simplifying the stuck + // meshlets. + if passed_tris > stuck_tris / 3 { + simplification_queue.extend(stuck.drain(..).flat_map(|group| group.meshlets)); + } + + bvh.add_lod(first_group, &all_groups); + } + + // If there's any stuck meshlets left, add another LOD level with only them + if !stuck.is_empty() { + let first_group = all_groups.len() as u32; + all_groups.extend(stuck); + bvh.add_lod(first_group, &all_groups); + } + + let (bvh, aabb, depth) = bvh.build(&mut meshlets, all_groups, &mut cull_data); + + // Copy vertex attributes per meshlet and compress + let mut vertex_positions = BitVec::::new(); + let mut vertex_normals = Vec::new(); + let mut vertex_uvs = Vec::new(); + let mut bevy_meshlets = Vec::with_capacity(meshlets.len()); + for (i, meshlet) in meshlets.meshlets.iter().enumerate() { + build_and_compress_per_meshlet_vertex_data( + meshlet, + meshlets.get(i).vertices, + &vertex_buffer, + vertex_stride, + &mut vertex_positions, + &mut vertex_normals, + &mut vertex_uvs, + &mut bevy_meshlets, + vertex_position_quantization_factor, + ); + } + vertex_positions.set_uninitialized(false); + + Ok(Self { + vertex_positions: vertex_positions.into_vec().into(), + vertex_normals: vertex_normals.into(), + vertex_uvs: vertex_uvs.into(), + indices: meshlets.triangles.into(), + bvh: bvh.into(), + meshlets: bevy_meshlets.into(), + meshlet_cull_data: cull_data + .into_iter() + .map(|cull_data| MeshletCullData { + aabb: aabb_to_meshlet(cull_data.aabb, cull_data.error, 0), + lod_group_sphere: sphere_to_meshlet(cull_data.lod_group_sphere), + }) + .collect(), + aabb, + bvh_depth: depth, + }) + } +} + +fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshConversionError> { + if mesh.primitive_topology() != PrimitiveTopology::TriangleList { + return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology); + } + + if mesh.attributes().map(|(attribute, _)| attribute.id).ne([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + ]) { + return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes( + mesh.attributes() + .map(|(attribute, _)| format!("{attribute:?}")) + .collect(), + )); + } + + match mesh.indices() { + Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())), + Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()), + _ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices), + } +} + +fn triangles_in_meshlets(meshlets: &Meshlets, ids: impl IntoIterator) -> u32 { + ids.into_iter() + .map(|id| meshlets.get(id as _).triangles.len() as u32 / 3) + .sum() +} + +fn compute_meshlets( + indices: &[u32], + vertices: &VertexDataAdapter, + position_only_vertex_remap: &[u32], + prev_lod_data: Option<(BoundingSphere, f32)>, +) -> (Meshlets, Vec) { + // For each vertex, build a list of all triangles that use it + let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_remap.len()]; + for (i, index) in indices.iter().enumerate() { + let vertex_id = position_only_vertex_remap[*index as usize]; + let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize]; + vertex_to_triangles.push(i / 3); + } + + // For each triangle pair, count how many vertices they share + let mut triangle_pair_to_shared_vertex_count = >::default(); + for vertex_triangle_ids in vertices_to_triangles { + for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() { + let count = triangle_pair_to_shared_vertex_count + .entry(( + triangle_id1.min(triangle_id2), + triangle_id1.max(triangle_id2), + )) + .or_insert(0); + *count += 1; + } + } + + // For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count + let triangle_count = indices.len() / 3; + let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count]; + for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count + { + // We record both id1->id2 and id2->id1 as adjacency is symmetrical + connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count)); + connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count)); + } + + // The order of triangles depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? + for list in connected_triangles_per_triangle.iter_mut() { + list.sort_unstable(); + } + + let mut xadj = Vec::with_capacity(triangle_count + 1); + let mut adjncy = Vec::new(); + let mut adjwgt = Vec::new(); + for connected_triangles in connected_triangles_per_triangle { + xadj.push(adjncy.len() as i32); + for (connected_triangle_id, shared_vertex_count) in connected_triangles { + adjncy.push(connected_triangle_id as i32); + adjwgt.push(shared_vertex_count); + // TODO: Additional weight based on triangle center spatial proximity? + } + } + xadj.push(adjncy.len() as i32); + + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions + + let mut meshlet_per_triangle = vec![0; triangle_count]; + let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet + Graph::new(1, partition_count as i32, &xadj, &adjncy) + .unwrap() + .set_options(&options) + .set_adjwgt(&adjwgt) + .part_recursive(&mut meshlet_per_triangle) + .unwrap(); + + let mut indices_per_meshlet = vec![Vec::new(); partition_count]; + for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() { + let meshlet_indices = &mut indices_per_meshlet[meshlet as usize]; + let base_index = triangle_id * 3; + meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]); + } + + // Use meshopt to build meshlets from the sets of triangles + let mut meshlets = Meshlets { + meshlets: Vec::new(), + vertices: Vec::new(), + triangles: Vec::new(), + }; + let mut cull_data = Vec::new(); + let get_vertex = |&v: &u32| { + *bytemuck::from_bytes::( + &vertices.reader.get_ref() + [vertices.position_offset + v as usize * vertices.vertex_stride..][..12], + ) + }; + for meshlet_indices in &indices_per_meshlet { + let meshlet = build_meshlets(meshlet_indices, vertices, 256, 128, 0.0); + for meshlet in meshlet.iter() { + let (lod_group_sphere, error) = prev_lod_data.unwrap_or_else(|| { + let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); + (BoundingSphere::new(bounds.center, bounds.radius), 0.0) + }); + + cull_data.push(TempMeshletCullData { + aabb: Aabb3d::from_point_cloud( + Isometry3d::IDENTITY, + meshlet.vertices.iter().map(get_vertex), + ), + lod_group_sphere, + error, + }); + } + merge_meshlets(&mut meshlets, meshlet); + } + (meshlets, cull_data) +} + +fn find_connected_meshlets( + simplification_queue: &[u32], + meshlets: &Meshlets, + position_only_vertex_remap: &[u32], +) -> Vec> { + // For each vertex, build a list of all meshlets that use it + let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_remap.len()]; + for (id_index, &meshlet_id) in simplification_queue.iter().enumerate() { + let meshlet = meshlets.get(meshlet_id as _); + for index in meshlet.triangles { + let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize]; + let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize]; + // Meshlets are added in order, so we can just check the last element to deduplicate, + // in the case of two triangles sharing the same vertex within a single meshlet + if vertex_to_meshlets.last() != Some(&id_index) { + vertex_to_meshlets.push(id_index); + } + } + } + + // For each meshlet pair, count how many vertices they share + let mut meshlet_pair_to_shared_vertex_count = >::default(); + for vertex_meshlet_ids in vertices_to_meshlets { + for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() { + let count = meshlet_pair_to_shared_vertex_count + .entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2))) + .or_insert(0); + *count += 1; + } + } + + // For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count + let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()]; + for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count { + // We record both id1->id2 and id2->id1 as adjacency is symmetrical + connected_meshlets_per_meshlet[meshlet_id1].push((meshlet_id2, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id2].push((meshlet_id1, shared_vertex_count)); + } + + // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? + for list in connected_meshlets_per_meshlet.iter_mut() { + list.sort_unstable(); + } + + connected_meshlets_per_meshlet +} + +// METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf +fn group_meshlets( + simplification_queue: &[u32], + meshlet_cull_data: &[TempMeshletCullData], + connected_meshlets_per_meshlet: &[Vec<(usize, usize)>], +) -> Vec { + let mut xadj = Vec::with_capacity(simplification_queue.len() + 1); + let mut adjncy = Vec::new(); + let mut adjwgt = Vec::new(); + for connected_meshlets in connected_meshlets_per_meshlet { + xadj.push(adjncy.len() as i32); + for (connected_meshlet_id, shared_vertex_count) in connected_meshlets { + adjncy.push(*connected_meshlet_id as i32); + adjwgt.push(*shared_vertex_count as i32); + // TODO: Additional weight based on meshlet spatial proximity + } + } + xadj.push(adjncy.len() as i32); + + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 200; + + let mut group_per_meshlet = vec![0; simplification_queue.len()]; + let partition_count = simplification_queue + .len() + .div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic + Graph::new(1, partition_count as i32, &xadj, &adjncy) + .unwrap() + .set_options(&options) + .set_adjwgt(&adjwgt) + .part_recursive(&mut group_per_meshlet) + .unwrap(); + + let mut groups = vec![TempMeshletGroup::default(); partition_count]; + for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() { + let group = &mut groups[meshlet_group as usize]; + let meshlet_id = simplification_queue[i]; + + group.meshlets.push(meshlet_id); + let data = &meshlet_cull_data[meshlet_id as usize]; + group.aabb = group.aabb.merge(&data.aabb); + group.lod_bounds = merge_spheres(group.lod_bounds, data.lod_group_sphere); + } + groups +} + +fn lock_group_borders( + vertex_locks: &mut [bool], + groups: &[TempMeshletGroup], + meshlets: &Meshlets, + position_only_vertex_remap: &[u32], +) { + let mut position_only_locks = vec![-1; position_only_vertex_remap.len()]; + + // Iterate over position-only based vertices of all meshlets in all groups + for (group_id, group) in groups.iter().enumerate() { + for &meshlet_id in group.meshlets.iter() { + let meshlet = meshlets.get(meshlet_id as usize); + for index in meshlet.triangles { + let vertex_id = + position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize; + + // If the vertex is not yet claimed by any group, or was already claimed by this group + if position_only_locks[vertex_id] == -1 + || position_only_locks[vertex_id] == group_id as i32 + { + position_only_locks[vertex_id] = group_id as i32; // Then claim the vertex for this group + } else { + position_only_locks[vertex_id] = -2; // Else vertex was already claimed by another group or was already locked, lock it + } + } + } + } + + // Lock vertices used by more than 1 group + for i in 0..vertex_locks.len() { + let vertex_id = position_only_vertex_remap[i] as usize; + vertex_locks[i] = position_only_locks[vertex_id] == -2; + } +} + +fn simplify_meshlet_group( + group: &TempMeshletGroup, + meshlets: &Meshlets, + vertices: &VertexDataAdapter<'_>, + vertex_normals: &[f32], + vertex_stride: usize, + vertex_locks: &[bool], +) -> Option<(Vec, f32)> { + // Build a new index buffer into the mesh vertex data by combining all meshlet data in the group + let group_indices = group + .meshlets + .iter() + .flat_map(|&meshlet_id| { + let meshlet = meshlets.get(meshlet_id as _); + meshlet + .triangles + .iter() + .map(|&meshlet_index| meshlet.vertices[meshlet_index as usize]) + }) + .collect::>(); + + // Simplify the group to ~50% triangle count + let mut error = 0.0; + let simplified_group_indices = simplify_with_attributes_and_locks( + &group_indices, + vertices, + vertex_normals, + &[0.5; 3], + vertex_stride, + vertex_locks, + group_indices.len() / 2, + f32::MAX, + SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute, + Some(&mut error), + ); + + // Check if we were able to simplify + if simplified_group_indices.len() as f32 / group_indices.len() as f32 + > SIMPLIFICATION_FAILURE_PERCENTAGE + { + return None; + } + + Some((simplified_group_indices, error)) +} + +fn merge_meshlets(meshlets: &mut Meshlets, merge: Meshlets) { + let vertex_offset = meshlets.vertices.len() as u32; + let triangle_offset = meshlets.triangles.len() as u32; + meshlets.vertices.extend_from_slice(&merge.vertices); + meshlets.triangles.extend_from_slice(&merge.triangles); + meshlets + .meshlets + .extend(merge.meshlets.into_iter().map(|mut meshlet| { + meshlet.vertex_offset += vertex_offset; + meshlet.triangle_offset += triangle_offset; + meshlet + })); +} + +fn build_and_compress_per_meshlet_vertex_data( + meshlet: &meshopt_Meshlet, + meshlet_vertex_ids: &[u32], + vertex_buffer: &[u8], + vertex_stride: usize, + vertex_positions: &mut BitVec, + vertex_normals: &mut Vec, + vertex_uvs: &mut Vec, + meshlets: &mut Vec, + vertex_position_quantization_factor: u8, +) { + let start_vertex_position_bit = vertex_positions.len() as u32; + let start_vertex_attribute_id = vertex_normals.len() as u32; + + let quantization_factor = + (1 << vertex_position_quantization_factor) as f32 * CENTIMETERS_PER_METER; + + let mut min_quantized_position_channels = IVec3::MAX; + let mut max_quantized_position_channels = IVec3::MIN; + + // Lossy vertex compression + let mut quantized_positions = [IVec3::ZERO; 256]; + for (i, vertex_id) in meshlet_vertex_ids.iter().enumerate() { + // Load source vertex attributes + let vertex_id_byte = *vertex_id as usize * vertex_stride; + let vertex_data = &vertex_buffer[vertex_id_byte..(vertex_id_byte + vertex_stride)]; + let position = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[0..12])); + let normal = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[12..24])); + let uv = Vec2::from_slice(bytemuck::cast_slice(&vertex_data[24..32])); + + // Copy uncompressed UV + vertex_uvs.push(uv); + + // Compress normal + vertex_normals.push(pack2x16snorm(octahedral_encode(normal))); + + // Quantize position to a fixed-point IVec3 + let quantized_position = (position * quantization_factor + 0.5).as_ivec3(); + quantized_positions[i] = quantized_position; + + // Compute per X/Y/Z-channel quantized position min/max for this meshlet + min_quantized_position_channels = min_quantized_position_channels.min(quantized_position); + max_quantized_position_channels = max_quantized_position_channels.max(quantized_position); + } + + // Calculate bits needed to encode each quantized vertex position channel based on the range of each channel + let range = max_quantized_position_channels - min_quantized_position_channels + 1; + let bits_per_vertex_position_channel_x = log2(range.x as f32).ceil() as u8; + let bits_per_vertex_position_channel_y = log2(range.y as f32).ceil() as u8; + let bits_per_vertex_position_channel_z = log2(range.z as f32).ceil() as u8; + + // Lossless encoding of vertex positions in the minimum number of bits per channel + for quantized_position in quantized_positions.iter().take(meshlet_vertex_ids.len()) { + // Remap [range_min, range_max] IVec3 to [0, range_max - range_min] UVec3 + let position = (quantized_position - min_quantized_position_channels).as_uvec3(); + + // Store as a packed bitstream + vertex_positions.extend_from_bitslice( + &position.x.view_bits::()[..bits_per_vertex_position_channel_x as usize], + ); + vertex_positions.extend_from_bitslice( + &position.y.view_bits::()[..bits_per_vertex_position_channel_y as usize], + ); + vertex_positions.extend_from_bitslice( + &position.z.view_bits::()[..bits_per_vertex_position_channel_z as usize], + ); + } + + meshlets.push(Meshlet { + start_vertex_position_bit, + start_vertex_attribute_id, + start_index_id: meshlet.triangle_offset, + vertex_count_minus_one: (meshlet.vertex_count - 1) as u8, + triangle_count: meshlet.triangle_count as u8, + padding: 0, + bits_per_vertex_position_channel_x, + bits_per_vertex_position_channel_y, + bits_per_vertex_position_channel_z, + vertex_position_quantization_factor, + min_vertex_position_channel_x: min_quantized_position_channels.x as f32, + min_vertex_position_channel_y: min_quantized_position_channels.y as f32, + min_vertex_position_channel_z: min_quantized_position_channels.z as f32, + }); +} + +fn merge_spheres(a: BoundingSphere, b: BoundingSphere) -> BoundingSphere { + let sr = a.radius().min(b.radius()); + let br = a.radius().max(b.radius()); + let len = a.center.distance(b.center); + if len + sr <= br || sr == 0.0 || len == 0.0 { + if a.radius() > b.radius() { + a + } else { + b + } + } else { + let radius = (sr + br + len) / 2.0; + let center = + (a.center + b.center + (a.radius() - b.radius()) * (a.center - b.center) / len) / 2.0; + BoundingSphere::new(center, radius) + } +} + +#[derive(Copy, Clone)] +struct TempMeshletCullData { + aabb: Aabb3d, + lod_group_sphere: BoundingSphere, + error: f32, +} + +#[derive(Clone)] +struct TempMeshletGroup { + aabb: Aabb3d, + lod_bounds: BoundingSphere, + parent_error: f32, + meshlets: SmallVec<[u32; TARGET_MESHLETS_PER_GROUP]>, +} + +impl Default for TempMeshletGroup { + fn default() -> Self { + Self { + aabb: aabb_default(), // Default AABB to merge into + lod_bounds: BoundingSphere::new(Vec3A::ZERO, 0.0), + parent_error: f32::MAX, + meshlets: SmallVec::new(), + } + } +} + +// All the BVH build code was stolen from https://github.com/SparkyPotato/radiance/blob/4aa17a3a5be7a0466dc69713e249bbcee9f46057/crates/rad-renderer/src/assets/mesh/virtual_mesh.rs because it works and I'm lazy and don't want to reimplement it +struct TempBvhNode { + group: u32, + aabb: Aabb3d, + children: SmallVec<[u32; 8]>, +} + +#[derive(Default)] +struct BvhBuilder { + nodes: Vec, + lods: Vec>, +} + +impl BvhBuilder { + fn add_lod(&mut self, offset: u32, all_groups: &[TempMeshletGroup]) { + let first = self.nodes.len() as u32; + self.nodes.extend( + all_groups + .iter() + .enumerate() + .skip(offset as _) + .map(|(i, group)| TempBvhNode { + group: i as u32, + aabb: group.aabb, + children: SmallVec::new(), + }), + ); + let end = self.nodes.len() as u32; + if first != end { + self.lods.push(first..end); + } + } + + fn surface_area(&self, nodes: &[u32]) -> f32 { + nodes + .iter() + .map(|&x| self.nodes[x as usize].aabb) + .reduce(|a, b| a.merge(&b)) + .expect("cannot find surface area of zero nodes") + .visible_area() + } + + fn sort_nodes_by_sah(&self, nodes: &mut [u32], splits: [usize; 8]) { + // We use a BVH8, so just recursively binary split 3 times for near-optimal SAH + for i in 0..3 { + let parts = 1 << i; // 2^i + let nodes_per_split = 8 >> i; // 8 / 2^i + let half_count = nodes_per_split / 2; + let mut offset = 0; + for p in 0..parts { + let first = p * nodes_per_split; + let mut s0 = 0; + let mut s1 = 0; + for i in 0..half_count { + s0 += splits[first + i]; + s1 += splits[first + half_count + i]; + } + let c = s0 + s1; + let nodes = &mut nodes[offset..(offset + c)]; + offset += c; + + let mut cost = f32::MAX; + let mut axis = 0; + let key = |x, ax| self.nodes[x as usize].aabb.center()[ax]; + for ax in 0..3 { + nodes.sort_unstable_by(|&x, &y| key(x, ax).partial_cmp(&key(y, ax)).unwrap()); + let (left, right) = nodes.split_at(s0); + let c = self.surface_area(left) + self.surface_area(right); + if c < cost { + axis = ax; + cost = c; + } + } + if axis != 2 { + nodes.sort_unstable_by(|&x, &y| { + key(x, axis).partial_cmp(&key(y, axis)).unwrap() + }); + } + } + } + } + + fn build_temp_inner(&mut self, nodes: &mut [u32], optimize: bool) -> u32 { + let count = nodes.len(); + if count == 1 { + nodes[0] + } else if count <= 8 { + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children: nodes.iter().copied().collect(), + }); + i as _ + } else { + // We need to split the nodes into 8 groups, with the smallest possible tree depth. + // Additionally, no child should be more than one level deeper than the others. + // At `l` levels, we can fit upto 8^l nodes. + // The `max_child_size` is the largest power of 8 <= `count` (any larger and we'd have + // unfilled nodes). + // The `min_child_size` is thus 1 level (8 times) smaller. + // After distributing `min_child_size` to all children, we have distributed + // `min_child_size * 8` nodes (== `max_child_size`). + // The remaining nodes are then distributed left to right. + let max_child_size = 1 << ((count.ilog2() / 3) * 3); + let min_child_size = max_child_size >> 3; + let max_extra_per_node = max_child_size - min_child_size; + let mut extra = count - max_child_size; // 8 * min_child_size + let splits = core::array::from_fn(|_| { + let size = extra.min(max_extra_per_node); + extra -= size; + min_child_size + size + }); + + if optimize { + self.sort_nodes_by_sah(nodes, splits); + } + + let mut offset = 0; + let children = splits + .into_iter() + .map(|size| { + let i = self.build_temp_inner(&mut nodes[offset..(offset + size)], optimize); + offset += size; + i + }) + .collect(); + + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children, + }); + i as _ + } + } + + fn build_temp(&mut self) -> u32 { + let mut lods = Vec::with_capacity(self.lods.len()); + for lod in core::mem::take(&mut self.lods) { + let mut lod: Vec<_> = lod.collect(); + let root = self.build_temp_inner(&mut lod, true); + let node = &self.nodes[root as usize]; + if node.group != u32::MAX || node.children.len() == 8 { + lods.push(root); + } else { + lods.extend(node.children.iter().copied()); + } + } + self.build_temp_inner(&mut lods, false) + } + + fn build_inner( + &self, + groups: &[TempMeshletGroup], + out: &mut Vec, + max_depth: &mut u32, + node: u32, + depth: u32, + ) -> u32 { + *max_depth = depth.max(*max_depth); + let node = &self.nodes[node as usize]; + let onode = out.len(); + out.push(BvhNode::default()); + + for (i, &child_id) in node.children.iter().enumerate() { + let child = &self.nodes[child_id as usize]; + if child.group != u32::MAX { + let group = &groups[child.group as usize]; + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + out.lod_bounds[i] = sphere_to_meshlet(group.lod_bounds); + out.child_counts[i] = group.meshlets[1] as _; + } else { + let child_id = self.build_inner(groups, out, max_depth, child_id, depth + 1); + let child = &out[child_id as usize]; + let mut aabb = aabb_default(); + let mut parent_error = 0.0f32; + let mut lod_bounds = BoundingSphere::new(Vec3A::ZERO, 0.0); + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + child.aabbs[i].center, + child.aabbs[i].half_extent, + )); + lod_bounds = merge_spheres( + lod_bounds, + BoundingSphere::new(child.lod_bounds[i].center, child.lod_bounds[i].radius), + ); + parent_error = parent_error.max(child.aabbs[i].error); + } + + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(aabb, parent_error, child_id); + out.lod_bounds[i] = sphere_to_meshlet(lod_bounds); + out.child_counts[i] = u8::MAX; + } + } + + onode as _ + } + + fn build( + mut self, + meshlets: &mut Meshlets, + mut groups: Vec, + cull_data: &mut Vec, + ) -> (Vec, MeshletAabb, u32) { + // The BVH requires group meshlets to be contiguous, so remap them first. + let mut remap = Vec::with_capacity(meshlets.meshlets.len()); + let mut remapped_cull_data = Vec::with_capacity(cull_data.len()); + for group in groups.iter_mut() { + let first = remap.len() as u32; + let count = group.meshlets.len() as u32; + remap.extend( + group + .meshlets + .iter() + .map(|&m| meshlets.meshlets[m as usize]), + ); + remapped_cull_data.extend(group.meshlets.iter().map(|&m| cull_data[m as usize])); + group.meshlets.resize(2, 0); + group.meshlets[0] = first; + group.meshlets[1] = count; + } + meshlets.meshlets = remap; + *cull_data = remapped_cull_data; + + let mut out = vec![]; + let mut aabb = aabb_default(); + let mut max_depth = 0; + + if self.nodes.len() == 1 { + let mut o = BvhNode::default(); + let group = &groups[0]; + o.aabbs[0] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + o.lod_bounds[0] = sphere_to_meshlet(group.lod_bounds); + o.child_counts[0] = group.meshlets[1] as _; + out.push(o); + aabb = group.aabb; + max_depth = 1; + } else { + let root = self.build_temp(); + let root = self.build_inner(&groups, &mut out, &mut max_depth, root, 1); + assert_eq!(root, 0, "root must be 0"); + + let root = &out[0]; + for i in 0..8 { + if root.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + root.aabbs[i].center, + root.aabbs[i].half_extent, + )); + } + } + + let mut reachable = vec![false; meshlets.meshlets.len()]; + verify_bvh(&out, cull_data, &mut reachable, 0); + assert!( + reachable.iter().all(|&x| x), + "all meshlets must be reachable" + ); + + ( + out, + MeshletAabb { + center: aabb.center().into(), + half_extent: aabb.half_size().into(), + }, + max_depth, + ) + } +} + +fn verify_bvh( + out: &[BvhNode], + cull_data: &[TempMeshletCullData], + reachable: &mut [bool], + node: u32, +) { + let node = &out[node as usize]; + for i in 0..8 { + let sphere = node.lod_bounds[i]; + let error = node.aabbs[i].error; + if node.child_counts[i] == u8::MAX { + let child = &out[node.aabbs[i].child_offset as usize]; + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + assert!( + child.aabbs[i].error <= error, + "BVH errors are not monotonic" + ); + let sphere_error = (sphere.center - child.lod_bounds[i].center).length() + - (sphere.radius - child.lod_bounds[i].radius); + assert!( + sphere_error <= 0.0001, + "BVH lod spheres are not monotonic ({sphere_error})" + ); + } + verify_bvh(out, cull_data, reachable, node.aabbs[i].child_offset); + } else { + for m in 0..node.child_counts[i] as u32 { + let mid = (m + node.aabbs[i].child_offset) as usize; + let meshlet = &cull_data[mid]; + assert!(meshlet.error <= error, "meshlet errors are not monotonic"); + let sphere_error = (Vec3A::from(sphere.center) - meshlet.lod_group_sphere.center) + .length() + - (sphere.radius - meshlet.lod_group_sphere.radius()); + assert!( + sphere_error <= 0.0001, + "meshlet lod spheres are not monotonic: ({sphere_error})" + ); + reachable[mid] = true; + } + } + } +} + +fn aabb_default() -> Aabb3d { + Aabb3d { + min: Vec3A::INFINITY, + max: Vec3A::NEG_INFINITY, + } +} + +fn aabb_to_meshlet(aabb: Aabb3d, error: f32, child_offset: u32) -> MeshletAabbErrorOffset { + MeshletAabbErrorOffset { + center: aabb.center().into(), + error, + half_extent: aabb.half_size().into(), + child_offset, + } +} + +fn sphere_to_meshlet(sphere: BoundingSphere) -> MeshletBoundingSphere { + MeshletBoundingSphere { + center: sphere.center.into(), + radius: sphere.radius(), + } +} + +// TODO: Precise encode variant +fn octahedral_encode(v: Vec3) -> Vec2 { + let n = v / (v.x.abs() + v.y.abs() + v.z.abs()); + let octahedral_wrap = (1.0 - n.yx().abs()) + * Vec2::new( + if n.x >= 0.0 { 1.0 } else { -1.0 }, + if n.y >= 0.0 { 1.0 } else { -1.0 }, + ); + if n.z >= 0.0 { + n.xy() + } else { + octahedral_wrap + } +} + +// https://www.w3.org/TR/WGSL/#pack2x16snorm-builtin +fn pack2x16snorm(v: Vec2) -> u32 { + let v = v.clamp(Vec2::NEG_ONE, Vec2::ONE); + let v = (v * 32767.0 + 0.5).floor().as_i16vec2(); + bytemuck::cast(v) +} + +/// An error produced by [`MeshletMesh::from_mesh`]. +#[derive(Error, Debug)] +pub enum MeshToMeshletMeshConversionError { + #[error("Mesh primitive topology is not TriangleList")] + WrongMeshPrimitiveTopology, + #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}: {0:?}")] + WrongMeshVertexAttributes(Vec), + #[error("Mesh has no indices")] + MeshMissingIndices, +} diff --git a/vendor/bevy_pbr/src/meshlet/instance_manager.rs b/vendor/bevy_pbr/src/meshlet/instance_manager.rs new file mode 100644 index 00000000..6747732f --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/instance_manager.rs @@ -0,0 +1,295 @@ +use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; +use crate::DUMMY_MESH_MATERIAL; +use crate::{ + meshlet::asset::MeshletAabb, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, + PreviousGlobalTransform, RenderMaterialBindings, RenderMaterialInstances, +}; +use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; +use bevy_camera::visibility::RenderLayers; +use bevy_ecs::{ + entity::{Entities, Entity, EntityHashMap}, + message::MessageReader, + query::Has, + resource::Resource, + system::{Local, Query, Res, ResMut, SystemState}, +}; +use bevy_light::{NotShadowCaster, NotShadowReceiver}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::{render_resource::StorageBuffer, sync_world::MainEntity, MainWorld}; +use bevy_transform::components::GlobalTransform; +use core::ops::DerefMut; + +/// Manages data for each entity with a [`MeshletMesh`]. +#[derive(Resource)] +pub struct InstanceManager { + /// Amount of instances in the scene. + pub scene_instance_count: u32, + /// The max BVH depth of any instance in the scene. This is used to control the number of + /// dependent dispatches emitted for BVH traversal. + pub max_bvh_depth: u32, + + /// Per-instance [`MainEntity`], [`RenderLayers`], and [`NotShadowCaster`]. + pub instances: Vec<(MainEntity, RenderLayers, bool)>, + /// Per-instance [`MeshUniform`]. + pub instance_uniforms: StorageBuffer>, + /// Per-instance model-space AABB. + pub instance_aabbs: StorageBuffer>, + /// Per-instance material ID. + pub instance_material_ids: StorageBuffer>, + /// Per-instance index to the root node of the instance's BVH. + pub instance_bvh_root_nodes: StorageBuffer>, + /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. + pub view_instance_visibility: EntityHashMap>>, + + /// Next material ID available. + next_material_id: u32, + /// Map of material asset to material ID. + material_id_lookup: HashMap, + /// Set of material IDs used in the scene. + material_ids_present_in_scene: HashSet, +} + +impl InstanceManager { + pub fn new() -> Self { + Self { + scene_instance_count: 0, + max_bvh_depth: 0, + + instances: Vec::new(), + instance_uniforms: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_uniforms")); + buffer + }, + instance_aabbs: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_aabbs")); + buffer + }, + instance_material_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_material_ids")); + buffer + }, + instance_bvh_root_nodes: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_bvh_root_nodes")); + buffer + }, + view_instance_visibility: EntityHashMap::default(), + + next_material_id: 0, + material_id_lookup: HashMap::default(), + material_ids_present_in_scene: HashSet::default(), + } + } + + pub fn add_instance( + &mut self, + instance: MainEntity, + root_bvh_node: u32, + aabb: MeshletAabb, + bvh_depth: u32, + transform: &GlobalTransform, + previous_transform: Option<&PreviousGlobalTransform>, + render_layers: Option<&RenderLayers>, + mesh_material_ids: &RenderMaterialInstances, + render_material_bindings: &RenderMaterialBindings, + not_shadow_receiver: bool, + not_shadow_caster: bool, + ) { + // Build a MeshUniform for the instance + let transform = transform.affine(); + let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); + let mut flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if transform.matrix3.determinant().is_sign_positive() { + flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + let transforms = MeshTransforms { + world_from_local: (&transform).into(), + previous_world_from_local: (&previous_transform).into(), + flags: flags.bits(), + }; + + let mesh_material = mesh_material_ids.mesh_material(instance); + let mesh_material_binding_id = if mesh_material != DUMMY_MESH_MATERIAL.untyped() { + render_material_bindings + .get(&mesh_material) + .cloned() + .unwrap_or_default() + } else { + // Use a dummy binding ID if the mesh has no material + MaterialBindingId::default() + }; + + let mesh_uniform = MeshUniform::new( + &transforms, + 0, + mesh_material_binding_id.slot, + None, + None, + None, + ); + + // Append instance data + self.instances.push(( + instance, + render_layers.cloned().unwrap_or(RenderLayers::default()), + not_shadow_caster, + )); + self.instance_uniforms.get_mut().push(mesh_uniform); + self.instance_aabbs.get_mut().push(aabb); + self.instance_material_ids.get_mut().push(0); + self.instance_bvh_root_nodes.get_mut().push(root_bvh_node); + + self.scene_instance_count += 1; + self.max_bvh_depth = self.max_bvh_depth.max(bvh_depth); + } + + /// Get the material ID for a [`crate::Material`]. + pub fn get_material_id(&mut self, material_asset_id: UntypedAssetId) -> u32 { + *self + .material_id_lookup + .entry(material_asset_id) + .or_insert_with(|| { + self.next_material_id += 1; + self.next_material_id + }) + } + + pub fn material_present_in_scene(&self, material_id: &u32) -> bool { + self.material_ids_present_in_scene.contains(material_id) + } + + pub fn reset(&mut self, entities: &Entities) { + self.scene_instance_count = 0; + self.max_bvh_depth = 0; + + self.instances.clear(); + self.instance_uniforms.get_mut().clear(); + self.instance_aabbs.get_mut().clear(); + self.instance_material_ids.get_mut().clear(); + self.instance_bvh_root_nodes.get_mut().clear(); + self.view_instance_visibility + .retain(|view_entity, _| entities.contains(*view_entity)); + self.view_instance_visibility + .values_mut() + .for_each(|b| b.get_mut().clear()); + + self.next_material_id = 0; + self.material_id_lookup.clear(); + self.material_ids_present_in_scene.clear(); + } +} + +pub fn extract_meshlet_mesh_entities( + mut meshlet_mesh_manager: ResMut, + mut instance_manager: ResMut, + // TODO: Replace main_world and system_state when Extract>> is possible + mut main_world: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, + mut system_state: Local< + Option< + SystemState<( + Query<( + Entity, + &MeshletMesh3d, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + Option<&RenderLayers>, + Has, + Has, + )>, + Res, + ResMut>, + MessageReader>, + )>, + >, + >, + render_entities: &Entities, +) { + // Get instances query + if system_state.is_none() { + *system_state = Some(SystemState::new(&mut main_world)); + } + let system_state = system_state.as_mut().unwrap(); + let (instances_query, asset_server, mut assets, mut asset_events) = + system_state.get_mut(&mut main_world); + + // Reset per-frame data + instance_manager.reset(render_entities); + + // Free GPU buffer space for any modified or dropped MeshletMesh assets + for asset_event in asset_events.read() { + if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event { + meshlet_mesh_manager.remove(id); + } + } + + // Iterate over every instance + // TODO: Switch to change events to not upload every instance every frame. + for ( + instance, + meshlet_mesh, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ) in &instances_query + { + // Skip instances with an unloaded MeshletMesh asset + // TODO: This is a semi-expensive check + if asset_server.is_managed(meshlet_mesh.id()) + && !asset_server.is_loaded_with_dependencies(meshlet_mesh.id()) + { + continue; + } + + // Upload the instance's MeshletMesh asset data if not done already done + let (root_bvh_node, aabb, bvh_depth) = + meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets); + + // Add the instance's data to the instance manager + instance_manager.add_instance( + instance.into(), + root_bvh_node, + aabb, + bvh_depth, + transform, + previous_transform, + render_layers, + &mesh_material_ids, + &render_material_bindings, + not_shadow_receiver, + not_shadow_caster, + ); + } +} + +/// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems, +/// and note that the material is used by at least one entity in the scene. +pub fn queue_material_meshlet_meshes( + mut instance_manager: ResMut, + render_material_instances: Res, +) { + let instance_manager = instance_manager.deref_mut(); + + for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { + if let Some(material_instance) = render_material_instances.instances.get(instance) + && let Some(material_id) = instance_manager + .material_id_lookup + .get(&material_instance.asset_id) + { + instance_manager + .material_ids_present_in_scene + .insert(*material_id); + instance_manager.instance_material_ids.get_mut()[i] = *material_id; + } + } +} diff --git a/vendor/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/vendor/bevy_pbr/src/meshlet/material_pipeline_prepare.rs new file mode 100644 index 00000000..e00c4190 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -0,0 +1,475 @@ +use super::{ + instance_manager::InstanceManager, pipelines::MeshletPipelines, + resource_manager::ResourceManager, +}; +use crate::*; +use bevy_camera::{Camera3d, Projection}; +use bevy_core_pipeline::{ + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, ShadowFilteringMethod}; +use bevy_mesh::VertexBufferLayout; +use bevy_mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::erased_render_asset::ErasedRenderAssets; +use bevy_render::{camera::TemporalJitter, render_resource::*, view::ExtractedView}; +use bevy_utils::default; +use core::any::{Any, TypeId}; + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`MeshletMainOpaquePass3dNode`](`super::MeshletMainOpaquePass3dNode`). +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// Prepare [`Material`] pipelines for [`MeshletMesh`](`super::MeshletMesh`) entities for use in [`MeshletMainOpaquePass3dNode`](`super::MeshletMainOpaquePass3dNode`), +/// and register the material with [`InstanceManager`]. +pub fn prepare_material_meshlet_meshes_main_opaque_pass( + resource_manager: ResMut, + mut instance_manager: ResMut, + mut cache: Local>, + pipeline_cache: Res, + material_pipeline: Res, + mesh_pipeline: Res, + render_materials: Res>, + meshlet_pipelines: Res, + render_material_instances: Res, + material_bind_group_allocators: Res, + mut mesh_vertex_buffer_layouts: ResMut, + mut views: Query< + ( + &mut MeshletViewMaterialsMainOpaquePass, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + (Has, Has), + ( + Has, + Has, + Has, + Has, + ), + Has, + Option<&Projection>, + Has>, + Has>, + ), + With, + >, +) { + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + view, + tonemapping, + dither, + shadow_filter_method, + (ssao, distance_fog), + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + temporal_jitter, + projection, + has_environment_maps, + has_irradiance_volumes, + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances + .instances + .values() + .map(|instance| instance.asset_id) + .collect::>() + { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + + if material.properties.render_method != OpaqueRendererMethod::Forward + || material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: material_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let layout = mesh_pipeline.get_view_layout(view_key.into()); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + resource_manager.material_shade_bind_group_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ]; + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout, + push_constant_ranges: vec![], + vertex: VertexState { + shader: meshlet_pipelines.meshlet_mesh_material.clone(), + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + buffers: Vec::new(), + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: match material.properties.get_shader(MeshletFragmentShader) { + Some(shader) => shader.clone(), + None => meshlet_pipelines.meshlet_mesh_material.clone(), + }, + shader_defs, + entry_point: material_fragment.entry_point, + targets: material_fragment.targets, + }), + zero_initialize_workgroup_memory: false, + }; + let type_id = material_id.type_id(); + let Some(material_bind_group_allocator) = material_bind_group_allocators.get(&type_id) + else { + continue; + }; + let material_id = instance_manager.get_material_id(material_id); + + let pipeline_id = *cache.entry((view_key, type_id)).or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + continue; + }; + + materials.push((material_id, pipeline_id, (*bind_group).clone())); + } + } +} + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`MeshletPrepassNode`](`super::MeshletPrepassNode`). +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsPrepass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`MeshletDeferredGBufferPrepassNode`](`super::MeshletDeferredGBufferPrepassNode`). +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsDeferredGBufferPrepass( + pub Vec<(u32, CachedRenderPipelineId, BindGroup)>, +); + +/// Prepare [`Material`] pipelines for [`MeshletMesh`](`super::MeshletMesh`) entities for use in [`MeshletPrepassNode`](`super::MeshletPrepassNode`), +/// and [`MeshletDeferredGBufferPrepassNode`](`super::MeshletDeferredGBufferPrepassNode`) and register the material with [`InstanceManager`]. +pub fn prepare_material_meshlet_meshes_prepass( + resource_manager: ResMut, + mut instance_manager: ResMut, + mut cache: Local>, + pipeline_cache: Res, + prepass_pipeline: Res, + material_bind_group_allocators: Res, + render_materials: Res>, + meshlet_pipelines: Res, + render_material_instances: Res, + mut mesh_vertex_buffer_layouts: ResMut, + mut views: Query< + ( + &mut MeshletViewMaterialsPrepass, + &mut MeshletViewMaterialsDeferredGBufferPrepass, + &ExtractedView, + AnyOf<(&NormalPrepass, &MotionVectorPrepass, &DeferredPrepass)>, + ), + With, + >, +) { + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + mut deferred_materials, + view, + (normal_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances + .instances + .values() + .map(|instance| instance.asset_id) + .collect::>() + { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) + else { + continue; + }; + + if material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let material_wants_deferred = matches!( + material.properties.render_method, + OpaqueRendererMethod::Deferred + ); + if deferred_prepass.is_some() && material_wants_deferred { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } else if normal_prepass.is_none() && motion_vector_prepass.is_none() { + continue; + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + prepass_pipeline.view_layout_motion_vectors.clone() + } else { + prepass_pipeline.view_layout_no_motion_vectors.clone() + }; + + let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + material + .properties + .get_shader(MeshletDeferredFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) + } else { + material + .properties + .get_shader(MeshletPrepassFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) + }; + + let entry_point = if fragment_shader == meshlet_pipelines.meshlet_mesh_material { + material_fragment.entry_point.clone() + } else { + None + }; + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout: vec![ + view_layout, + prepass_pipeline.empty_layout.clone(), + resource_manager.material_shade_bind_group_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ], + vertex: VertexState { + shader: meshlet_pipelines.meshlet_mesh_material.clone(), + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + ..default() + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: fragment_shader, + shader_defs, + entry_point, + targets: material_fragment.targets, + }), + ..default() + }; + + let material_id = instance_manager.get_material_id(material_id); + + let pipeline_id = *cache + .entry((view_key, material_id.type_id())) + .or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + continue; + }; + + let item = (material_id, pipeline_id, (*bind_group).clone()); + if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + deferred_materials.push(item); + } else { + materials.push(item); + } + } + } +} + +// Meshlet materials don't use a traditional vertex buffer, but the material specialization requires one. +fn fake_vertex_buffer_layout(layouts: &mut MeshVertexBufferLayouts) -> MeshVertexBufferLayoutRef { + layouts.insert(MeshVertexBufferLayout::new( + vec![ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ], + VertexBufferLayout { + array_stride: 48, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: Mesh::ATTRIBUTE_POSITION.format, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_NORMAL.format, + offset: 12, + shader_location: 1, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_UV_0.format, + offset: 24, + shader_location: 2, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_TANGENT.format, + offset: 32, + shader_location: 3, + }, + ], + }, + )) +} diff --git a/vendor/bevy_pbr/src/meshlet/material_shade_nodes.rs b/vendor/bevy_pbr/src/meshlet/material_shade_nodes.rs new file mode 100644 index 00000000..890df912 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/material_shade_nodes.rs @@ -0,0 +1,421 @@ +use super::{ + material_pipeline_prepare::{ + MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, + MeshletViewMaterialsPrepass, + }, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, + InstanceManager, +}; +use crate::{ + MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, + ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, +}; +use bevy_camera::MainPassResolutionOverride; +use bevy_camera::Viewport; +use bevy_core_pipeline::prepass::{ + MotionVectorPrepass, PreviousViewUniformOffset, ViewPrepassTextures, +}; +use bevy_ecs::{ + query::{Has, QueryItem}, + world::World, +}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, + StoreOp, + }, + renderer::RenderContext, + view::{ViewTarget, ViewUniformOffset}, +}; + +/// Fullscreen shading pass based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletMainOpaquePass3dNode; +impl ViewNode for MeshletMainOpaquePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static MeshViewBindGroup, + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, + Option<&'static MainPassResolutionOverride>, + &'static MeshletViewMaterialsMainOpaquePass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + target, + mesh_view_bind_group, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + resolution_override, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_opaque_3d_pass"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "meshlet_material_opaque_3d_pass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(3, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Fullscreen pass to generate prepass textures based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletPrepassNode; +impl ViewNode for MeshletPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, + Has, + &'static MeshletViewMaterialsPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + resolution_override, + view_has_motion_vector_prepass, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + // Use None in place of Deferred attachments + None, + None, + ]; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "meshlet_material_prepass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + if view_has_motion_vector_prepass { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Fullscreen pass to generate a gbuffer based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletDeferredGBufferPrepassNode; +impl ViewNode for MeshletDeferredGBufferPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, + Has, + &'static MeshletViewMaterialsDeferredGBufferPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + resolution_override, + view_has_motion_vector_prepass, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + view_prepass_textures + .deferred + .as_ref() + .map(|deferred_texture| deferred_texture.get_attachment()), + view_prepass_textures + .deferred_lighting_pass_id + .as_ref() + .map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()), + ]; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_deferred_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = + diagnostics.pass_span(&mut render_pass, "meshlet_material_deferred_prepass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + if view_has_motion_vector_prepass { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} diff --git a/vendor/bevy_pbr/src/meshlet/meshlet_bindings.wgsl b/vendor/bevy_pbr/src/meshlet/meshlet_bindings.wgsl new file mode 100644 index 00000000..9d624f37 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/meshlet_bindings.wgsl @@ -0,0 +1,306 @@ +#define_import_path bevy_pbr::meshlet_bindings + +#import bevy_pbr::mesh_types::Mesh +#import bevy_render::view::View +#import bevy_pbr::prepass_bindings::PreviousViewUniforms +#import bevy_pbr::utils::octahedral_decode_signed + +struct BvhNode { + aabbs: array, + lod_bounds: array, 8>, + child_counts: array, + _padding: vec2, +} + +struct Meshlet { + start_vertex_position_bit: u32, + start_vertex_attribute_id: u32, + start_index_id: u32, + packed_a: u32, + packed_b: u32, + min_vertex_position_channel_x: f32, + min_vertex_position_channel_y: f32, + min_vertex_position_channel_z: f32, +} + +fn get_meshlet_vertex_count(meshlet: ptr) -> u32 { + return extractBits((*meshlet).packed_a, 0u, 8u) + 1u; +} + +fn get_meshlet_triangle_count(meshlet: ptr) -> u32 { + return extractBits((*meshlet).packed_a, 8u, 8u); +} + +struct MeshletCullData { + aabb: MeshletAabbErrorOffset, + lod_group_sphere: vec4, +} + +struct MeshletAabb { + center: vec3, + half_extent: vec3, +} + +struct MeshletAabbErrorOffset { + center_and_error: vec4, + half_extent_and_child_offset: vec4, +} + +fn get_aabb(aabb: ptr) -> MeshletAabb { + return MeshletAabb( + (*aabb).center_and_error.xyz, + (*aabb).half_extent_and_child_offset.xyz, + ); +} + +fn get_aabb_error(aabb: ptr) -> f32 { + return (*aabb).center_and_error.w; +} + +fn get_aabb_child_offset(aabb: ptr) -> u32 { + return bitcast((*aabb).half_extent_and_child_offset.w); +} + +struct DispatchIndirectArgs { + x: atomic, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: atomic, + first_vertex: u32, + first_instance: u32, +} + +// Either a BVH node or a meshlet, along with the instance it is associated with. +// Refers to BVH nodes in `meshlet_bvh_cull_queue` and `meshlet_second_pass_bvh_queue`, where `offset` is the index into `meshlet_bvh_nodes`. +// Refers to meshlets in `meshlet_meshlet_cull_queue` and `meshlet_raster_clusters`. +// In `meshlet_meshlet_cull_queue`, `offset` is the index into `meshlet_cull_data`. +// In `meshlet_raster_clusters`, `offset` is the index into `meshlets`. +struct InstancedOffset { + instance_id: u32, + offset: u32, +} + +const CENTIMETERS_PER_METER = 100.0; + +#ifdef MESHLET_INSTANCE_CULLING_PASS +struct Constants { scene_instance_count: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Per entity instance data +@group(0) @binding(3) var meshlet_instance_uniforms: array; +@group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask +@group(0) @binding(5) var meshlet_instance_aabbs: array; +@group(0) @binding(6) var meshlet_instance_bvh_root_nodes: array; + +// BVH cull queue data +@group(0) @binding(7) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(8) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(9) var meshlet_bvh_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_second_pass_instance_count: atomic; +@group(0) @binding(11) var meshlet_second_pass_instance_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_second_pass_instance_candidates: array; +#else +@group(0) @binding(10) var meshlet_second_pass_instance_count: u32; +@group(0) @binding(11) var meshlet_second_pass_instance_candidates: array; +#endif +#endif + +#ifdef MESHLET_BVH_CULLING_PASS +struct Constants { read_from_front: u32, rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_bvh_nodes: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// BVH cull queue data +@group(0) @binding(5) var meshlet_bvh_cull_count_read: u32; +@group(0) @binding(6) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(7) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(8) var meshlet_bvh_cull_queue: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_early: atomic; +@group(0) @binding(10) var meshlet_meshlet_cull_count_late: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch_early: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_dispatch_late: DispatchIndirectArgs; +@group(0) @binding(13) var meshlet_meshlet_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(14) var meshlet_second_pass_bvh_count: atomic; +@group(0) @binding(15) var meshlet_second_pass_bvh_dispatch: DispatchIndirectArgs; +@group(0) @binding(16) var meshlet_second_pass_bvh_queue: array; +#endif +#endif + +#ifdef MESHLET_CLUSTER_CULLING_PASS +struct Constants { rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_cull_data: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// Raster queue data +@group(0) @binding(5) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(6) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(7) var meshlet_previous_raster_counts: array; +@group(0) @binding(8) var meshlet_raster_clusters: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_read: u32; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_meshlet_cull_count_write: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_queue: array; +#else +@group(0) @binding(10) var meshlet_meshlet_cull_queue: array; +#endif +#endif + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS +@group(0) @binding(0) var meshlet_raster_clusters: array; // Per cluster +@group(0) @binding(1) var meshlets: array; // Per meshlet +@group(0) @binding(2) var meshlet_indices: array; // Many per meshlet +@group(0) @binding(3) var meshlet_vertex_positions: array; // Many per meshlet +@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(5) var meshlet_previous_raster_counts: array; +@group(0) @binding(6) var meshlet_software_raster_cluster_count: u32; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; +#endif +@group(0) @binding(8) var view: View; + +// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? +fn get_meshlet_vertex_id(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} + +fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) -> vec3 { + // Get bitstream start for the vertex + let unpacked = unpack4xU8((*meshlet).packed_b); + let bits_per_channel = unpacked.xyz; + let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z; + var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex); + + // Read each vertex channel from the bitstream + var vertex_position_packed = vec3(0u); + for (var i = 0u; i < 3u; i++) { + let lower_word_index = start_bit / 32u; + let lower_word_bit_offset = start_bit & 31u; + var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset; + if lower_word_bit_offset + bits_per_channel[i] > 32u { + next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset); + } + vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]); + start_bit += bits_per_channel[i]; + } + + // Remap [0, range_max - range_min] vec3 to [range_min, range_max] vec3 + var vertex_position = vec3(vertex_position_packed) + vec3( + (*meshlet).min_vertex_position_channel_x, + (*meshlet).min_vertex_position_channel_y, + (*meshlet).min_vertex_position_channel_z, + ); + + // Reverse vertex quantization + let vertex_position_quantization_factor = unpacked.w; + vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER; + + return vertex_position; +} +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +@group(2) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +@group(2) @binding(1) var meshlet_raster_clusters: array; // Per cluster +@group(2) @binding(2) var meshlets: array; // Per meshlet +@group(2) @binding(3) var meshlet_indices: array; // Many per meshlet +@group(2) @binding(4) var meshlet_vertex_positions: array; // Many per meshlet +@group(2) @binding(5) var meshlet_vertex_normals: array; // Many per meshlet +@group(2) @binding(6) var meshlet_vertex_uvs: array>; // Many per meshlet +@group(2) @binding(7) var meshlet_instance_uniforms: array; // Per entity instance + +// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? +fn get_meshlet_vertex_id(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} + +fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) -> vec3 { + // Get bitstream start for the vertex + let unpacked = unpack4xU8((*meshlet).packed_b); + let bits_per_channel = unpacked.xyz; + let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z; + var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex); + + // Read each vertex channel from the bitstream + var vertex_position_packed = vec3(0u); + for (var i = 0u; i < 3u; i++) { + let lower_word_index = start_bit / 32u; + let lower_word_bit_offset = start_bit & 31u; + var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset; + if lower_word_bit_offset + bits_per_channel[i] > 32u { + next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset); + } + vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]); + start_bit += bits_per_channel[i]; + } + + // Remap [0, range_max - range_min] vec3 to [range_min, range_max] vec3 + var vertex_position = vec3(vertex_position_packed) + vec3( + (*meshlet).min_vertex_position_channel_x, + (*meshlet).min_vertex_position_channel_y, + (*meshlet).min_vertex_position_channel_z, + ); + + // Reverse vertex quantization + let vertex_position_quantization_factor = unpacked.w; + vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER; + + return vertex_position; +} + +fn get_meshlet_vertex_normal(meshlet: ptr, vertex_id: u32) -> vec3 { + let packed_normal = meshlet_vertex_normals[(*meshlet).start_vertex_attribute_id + vertex_id]; + return octahedral_decode_signed(unpack2x16snorm(packed_normal)); +} + +fn get_meshlet_vertex_uv(meshlet: ptr, vertex_id: u32) -> vec2 { + return meshlet_vertex_uvs[(*meshlet).start_vertex_attribute_id + vertex_id]; +} +#endif diff --git a/vendor/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl b/vendor/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl new file mode 100644 index 00000000..975dd74f --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl @@ -0,0 +1,207 @@ +#define_import_path bevy_pbr::meshlet_cull_shared + +#import bevy_pbr::meshlet_bindings::{ + MeshletAabb, + DispatchIndirectArgs, + InstancedOffset, + depth_pyramid, + view, + previous_view, + meshlet_instance_uniforms, +} +#import bevy_render::maths::affine3_to_square + +// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 +fn lod_error_is_imperceptible(lod_sphere: vec4, simplification_error: f32, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); + let camera_pos = view.world_position; + + let projection = view.clip_from_view; + if projection[3][3] == 1.0 { + // Orthographic + let world_error = simplification_error * world_scale; + let proj = projection[1][1]; + let height = 2.0 / proj; + let norm_error = world_error / height; + return norm_error * view.viewport.w < 1.0; + } else { + // Perspective + var near = projection[3][2]; + let world_sphere_center = (world_from_local * vec4(lod_sphere.xyz, 1.0)).xyz; + let world_sphere_radius = lod_sphere.w * world_scale; + let d_pos = world_sphere_center - camera_pos; + let d = sqrt(dot(d_pos, d_pos)) - world_sphere_radius; + let norm_error = simplification_error / max(d, near) * projection[1][1] * 0.5; + return norm_error * view.viewport.w < 1.0; + } +} + +fn normalize_plane(p: vec4) -> vec4 { + return p / length(p.xyz); +} + +// https://fgiesen.wordpress.com/2012/08/31/frustum-planes-from-the-projection-matrix/ +// https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/ +fn aabb_in_frustum(aabb: MeshletAabb, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let row_major = transpose(clip_from_local); + let planes = array( + row_major[3] + row_major[0], + row_major[3] - row_major[0], + row_major[3] + row_major[1], + row_major[3] - row_major[1], + row_major[2], + ); + + for (var i = 0; i < 5; i++) { + let plane = normalize_plane(planes[i]); + let flipped = aabb.half_extent * sign(plane.xyz); + if dot(aabb.center + flipped, plane.xyz) <= -plane.w { + return false; + } + } + return true; +} + +struct ScreenAabb { + min: vec3, + max: vec3, +} + +fn min8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +fn max8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return max(max(max(a, b), max(c, d)), max(max(e, f), max(g, h))); +} + +fn min8_4(a: vec4, b: vec4, c: vec4, d: vec4, e: vec4, f: vec4, g: vec4, h: vec4) -> vec4 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +// https://zeux.io/2023/01/12/approximate-projected-bounds/ +fn project_aabb(clip_from_local: mat4x4, near: f32, aabb: MeshletAabb, out: ptr) -> bool { + let extent = aabb.half_extent * 2.0; + let sx = clip_from_local * vec4(extent.x, 0.0, 0.0, 0.0); + let sy = clip_from_local * vec4(0.0, extent.y, 0.0, 0.0); + let sz = clip_from_local * vec4(0.0, 0.0, extent.z, 0.0); + + let p0 = clip_from_local * vec4(aabb.center - aabb.half_extent, 1.0); + let p1 = p0 + sz; + let p2 = p0 + sy; + let p3 = p2 + sz; + let p4 = p0 + sx; + let p5 = p4 + sz; + let p6 = p4 + sy; + let p7 = p6 + sz; + + let depth = min8_4(p0, p1, p2, p3, p4, p5, p6, p7).w; + // do not occlusion cull if we are inside the aabb + if depth < near { + return false; + } + + let dp0 = p0.xyz / p0.w; + let dp1 = p1.xyz / p1.w; + let dp2 = p2.xyz / p2.w; + let dp3 = p3.xyz / p3.w; + let dp4 = p4.xyz / p4.w; + let dp5 = p5.xyz / p5.w; + let dp6 = p6.xyz / p6.w; + let dp7 = p7.xyz / p7.w; + let min = min8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + let max = max8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + var vaabb = vec4(min.xy, max.xy); + // convert ndc to texture coordinates by rescaling and flipping Y + vaabb = vaabb.xwzy * vec4(0.5, -0.5, 0.5, -0.5) + 0.5; + (*out).min = vec3(vaabb.xy, min.z); + (*out).max = vec3(vaabb.zw, max.z); + return true; +} + +fn sample_hzb(smin: vec2, smax: vec2, mip: i32) -> f32 { + let texel = vec4(0, 1, 2, 3); + let sx = min(smin.x + texel, smax.xxxx); + let sy = min(smin.y + texel, smax.yyyy); + // TODO: switch to min samplers when wgpu has them + // sampling 16 times a finer mip is worth the extra cost for better culling + let a = sample_hzb_row(sx, sy.x, mip); + let b = sample_hzb_row(sx, sy.y, mip); + let c = sample_hzb_row(sx, sy.z, mip); + let d = sample_hzb_row(sx, sy.w, mip); + return min(min(a, b), min(c, d)); +} + +fn sample_hzb_row(sx: vec4, sy: u32, mip: i32) -> f32 { + let a = textureLoad(depth_pyramid, vec2(sx.x, sy), mip).x; + let b = textureLoad(depth_pyramid, vec2(sx.y, sy), mip).x; + let c = textureLoad(depth_pyramid, vec2(sx.z, sy), mip).x; + let d = textureLoad(depth_pyramid, vec2(sx.w, sy), mip).x; + return min(min(a, b), min(c, d)); +} + +// TODO: We should probably be using a POT HZB texture? +fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2) -> bool { + let hzb_size = ceil(screen * 0.5); + let aabb_min = aabb.min.xy * hzb_size; + let aabb_max = aabb.max.xy * hzb_size; + + let min_texel = vec2(max(aabb_min, vec2(0.0))); + let max_texel = vec2(min(aabb_max, hzb_size - 1.0)); + let size = max_texel - min_texel; + let max_size = max(size.x, size.y); + + // note: add 1 before max because the unsigned overflow behavior is intentional + // it wraps around firstLeadingBit(0) = ~0 to 0 + // TODO: we actually sample a 4x4 block, so ideally this would be `max(..., 3u) - 3u`. + // However, since our HZB is not a power of two, we need to be extra-conservative to not over-cull, so we go up a mip. + var mip = max(firstLeadingBit(max_size) + 1u, 2u) - 2u; + + if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) { + mip += 1u; + } + + let smin = min_texel >> vec2(mip); + let smax = max_texel >> vec2(mip); + + let curr_depth = sample_hzb(smin, smax, i32(mip)); + return aabb.max.z <= curr_depth; +} + +fn occlusion_cull_projection() -> mat4x4 { +#ifdef FIRST_CULLING_PASS + return view.clip_from_world; +#else + return previous_view.clip_from_world; +#endif +} + +fn occlusion_cull_clip_from_local(instance_id: u32) -> mat4x4 { +#ifdef FIRST_CULLING_PASS + let prev_world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].previous_world_from_local); + return previous_view.clip_from_world * prev_world_from_local; +#else + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + return view.clip_from_world * world_from_local; +#endif +} + +fn should_occlusion_cull_aabb(aabb: MeshletAabb, instance_id: u32) -> bool { + let projection = occlusion_cull_projection(); + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + + let clip_from_local = occlusion_cull_clip_from_local(instance_id); + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + if project_aabb(clip_from_local, near, aabb, &screen_aabb) { + return occlusion_cull_screen_aabb(screen_aabb, view.viewport.zw); + } + return false; +} diff --git a/vendor/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/vendor/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs new file mode 100644 index 00000000..29d7a75d --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -0,0 +1,161 @@ +use crate::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; +use alloc::sync::Arc; +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res, ResMut}, +}; +use bevy_math::Vec2; +use bevy_platform::collections::HashMap; +use bevy_render::{ + render_resource::BufferAddress, + renderer::{RenderDevice, RenderQueue}, +}; +use core::ops::Range; + +/// Manages uploading [`MeshletMesh`] asset data to the GPU. +#[derive(Resource)] +pub struct MeshletMeshManager { + pub vertex_positions: PersistentGpuBuffer>, + pub vertex_normals: PersistentGpuBuffer>, + pub vertex_uvs: PersistentGpuBuffer>, + pub indices: PersistentGpuBuffer>, + pub bvh_nodes: PersistentGpuBuffer>, + pub meshlets: PersistentGpuBuffer>, + pub meshlet_cull_data: PersistentGpuBuffer>, + meshlet_mesh_slices: + HashMap, ([Range; 7], MeshletAabb, u32)>, +} + +pub fn init_meshlet_mesh_manager(mut commands: Commands, render_device: Res) { + commands.insert_resource(MeshletMeshManager { + vertex_positions: PersistentGpuBuffer::new("meshlet_vertex_positions", &render_device), + vertex_normals: PersistentGpuBuffer::new("meshlet_vertex_normals", &render_device), + vertex_uvs: PersistentGpuBuffer::new("meshlet_vertex_uvs", &render_device), + indices: PersistentGpuBuffer::new("meshlet_indices", &render_device), + bvh_nodes: PersistentGpuBuffer::new("meshlet_bvh_nodes", &render_device), + meshlets: PersistentGpuBuffer::new("meshlets", &render_device), + meshlet_cull_data: PersistentGpuBuffer::new("meshlet_cull_data", &render_device), + meshlet_mesh_slices: HashMap::default(), + }); +} + +impl MeshletMeshManager { + // Returns the index of the root BVH node, as well as the depth of the BVH. + pub fn queue_upload_if_needed( + &mut self, + asset_id: AssetId, + assets: &mut Assets, + ) -> (u32, MeshletAabb, u32) { + let queue_meshlet_mesh = |asset_id: &AssetId| { + let meshlet_mesh = assets.remove_untracked(*asset_id).expect( + "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", + ); + + let vertex_positions_slice = self + .vertex_positions + .queue_write(Arc::clone(&meshlet_mesh.vertex_positions), ()); + let vertex_normals_slice = self + .vertex_normals + .queue_write(Arc::clone(&meshlet_mesh.vertex_normals), ()); + let vertex_uvs_slice = self + .vertex_uvs + .queue_write(Arc::clone(&meshlet_mesh.vertex_uvs), ()); + let indices_slice = self + .indices + .queue_write(Arc::clone(&meshlet_mesh.indices), ()); + let meshlets_slice = self.meshlets.queue_write( + Arc::clone(&meshlet_mesh.meshlets), + ( + vertex_positions_slice.start, + vertex_normals_slice.start, + indices_slice.start, + ), + ); + let base_meshlet_index = (meshlets_slice.start / size_of::() as u64) as u32; + let bvh_node_slice = self + .bvh_nodes + .queue_write(Arc::clone(&meshlet_mesh.bvh), base_meshlet_index); + let meshlet_cull_data_slice = self + .meshlet_cull_data + .queue_write(Arc::clone(&meshlet_mesh.meshlet_cull_data), ()); + + ( + [ + vertex_positions_slice, + vertex_normals_slice, + vertex_uvs_slice, + indices_slice, + bvh_node_slice, + meshlets_slice, + meshlet_cull_data_slice, + ], + meshlet_mesh.aabb, + meshlet_mesh.bvh_depth, + ) + }; + + // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading + let ([_, _, _, _, bvh_node_slice, _, _], aabb, bvh_depth) = self + .meshlet_mesh_slices + .entry(asset_id) + .or_insert_with_key(queue_meshlet_mesh) + .clone(); + + ( + (bvh_node_slice.start / size_of::() as u64) as u32, + aabb, + bvh_depth, + ) + } + + pub fn remove(&mut self, asset_id: &AssetId) { + if let Some(( + [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, bvh_node_slice, meshlets_slice, meshlet_cull_data_slice], + _, + _, + )) = self.meshlet_mesh_slices.remove(asset_id) + { + self.vertex_positions + .mark_slice_unused(vertex_positions_slice); + self.vertex_normals.mark_slice_unused(vertex_normals_slice); + self.vertex_uvs.mark_slice_unused(vertex_uvs_slice); + self.indices.mark_slice_unused(indices_slice); + self.bvh_nodes.mark_slice_unused(bvh_node_slice); + self.meshlets.mark_slice_unused(meshlets_slice); + self.meshlet_cull_data + .mark_slice_unused(meshlet_cull_data_slice); + } + } +} + +/// Upload all newly queued [`MeshletMesh`] asset data to the GPU. +pub fn perform_pending_meshlet_mesh_writes( + mut meshlet_mesh_manager: ResMut, + render_queue: Res, + render_device: Res, +) { + meshlet_mesh_manager + .vertex_positions + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .vertex_normals + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .vertex_uvs + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .indices + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .bvh_nodes + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .meshlets + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .meshlet_cull_data + .perform_writes(&render_queue, &render_device); +} diff --git a/vendor/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl b/vendor/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl new file mode 100644 index 00000000..1309c788 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/meshlet_mesh_material.wgsl @@ -0,0 +1,52 @@ +#import bevy_pbr::{ + meshlet_visibility_buffer_resolve::resolve_vertex_output, + view_transformations::uv_to_ndc, + prepass_io, + pbr_prepass_functions, + utils::rand_f, +} + +@vertex +fn vertex(@builtin(vertex_index) vertex_input: u32) -> @builtin(position) vec4 { + let vertex_index = vertex_input % 3u; + let material_id = vertex_input / 3u; + let material_depth = f32(material_id) / 65535.0; + let uv = vec2(vec2(vertex_index >> 1u, vertex_index & 1u)) * 2.0; + return vec4(uv_to_ndc(uv), material_depth, 1.0); +} + +@fragment +fn fragment(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { + let vertex_output = resolve_vertex_output(frag_coord); + var rng = vertex_output.cluster_id; + let color = vec3(rand_f(&rng), rand_f(&rng), rand_f(&rng)); + return vec4(color, 1.0); +} + +#ifdef PREPASS_FRAGMENT +@fragment +fn prepass_fragment(@builtin(position) frag_coord: vec4) -> prepass_io::FragmentOutput { + let vertex_output = resolve_vertex_output(frag_coord); + + var out: prepass_io::FragmentOutput; + +#ifdef NORMAL_PREPASS + out.normal = vec4(vertex_output.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + +#ifdef MOTION_VECTOR_PREPASS + out.motion_vector = vertex_output.motion_vector; +#endif + +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + + return out; +} +#endif diff --git a/vendor/bevy_pbr/src/meshlet/meshlet_preview.png b/vendor/bevy_pbr/src/meshlet/meshlet_preview.png new file mode 100644 index 00000000..2c319a89 Binary files /dev/null and b/vendor/bevy_pbr/src/meshlet/meshlet_preview.png differ diff --git a/vendor/bevy_pbr/src/meshlet/mod.rs b/vendor/bevy_pbr/src/meshlet/mod.rs new file mode 100644 index 00000000..13ec1444 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/mod.rs @@ -0,0 +1,307 @@ +//! Render high-poly 3d meshes using an efficient GPU-driven method. See [`MeshletPlugin`] and [`MeshletMesh`] for details. + +mod asset; +#[cfg(feature = "meshlet_processor")] +mod from_mesh; +mod instance_manager; +mod material_pipeline_prepare; +mod material_shade_nodes; +mod meshlet_mesh_manager; +mod persistent_buffer; +mod persistent_buffer_impls; +mod pipelines; +mod resource_manager; +mod visibility_buffer_raster_node; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum NodeMeshlet { + VisibilityBufferRasterPass, + Prepass, + DeferredPrepass, + MainOpaquePass, + } +} + +pub(crate) use self::{ + instance_manager::{queue_material_meshlet_meshes, InstanceManager}, + material_pipeline_prepare::{ + prepare_material_meshlet_meshes_main_opaque_pass, prepare_material_meshlet_meshes_prepass, + }, +}; + +pub use self::asset::{ + MeshletMesh, MeshletMeshLoader, MeshletMeshSaver, MESHLET_MESH_ASSET_VERSION, +}; +#[cfg(feature = "meshlet_processor")] +pub use self::from_mesh::{ + MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR, +}; +use self::{ + graph::NodeMeshlet, + instance_manager::extract_meshlet_mesh_entities, + material_pipeline_prepare::{ + MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, + MeshletViewMaterialsPrepass, + }, + material_shade_nodes::{ + MeshletDeferredGBufferPrepassNode, MeshletMainOpaquePass3dNode, MeshletPrepassNode, + }, + meshlet_mesh_manager::perform_pending_meshlet_mesh_writes, + pipelines::*, + resource_manager::{ + prepare_meshlet_per_frame_resources, prepare_meshlet_view_bind_groups, ResourceManager, + }, + visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode, +}; +use crate::{ + graph::NodePbr, meshlet::meshlet_mesh_manager::init_meshlet_mesh_manager, + PreviousGlobalTransform, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, AssetApp, AssetId, Handle}; +use bevy_camera::visibility::{self, Visibility, VisibilityClass}; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DeferredPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::Has, + reflect::ReflectComponent, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res}, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_graph::{RenderGraphExt, ViewNodeRunner}, + renderer::RenderDevice, + settings::WgpuFeatures, + view::{prepare_view_targets, Msaa}, + ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_transform::components::Transform; +use derive_more::From; +use tracing::error; + +/// Provides a plugin for rendering large amounts of high-poly 3d meshes using an efficient GPU-driven method. See also [`MeshletMesh`]. +/// +/// Rendering dense scenes made of high-poly meshes with thousands or millions of triangles is extremely expensive in Bevy's standard renderer. +/// Once meshes are pre-processed into a [`MeshletMesh`], this plugin can render these kinds of scenes very efficiently. +/// +/// In comparison to Bevy's standard renderer: +/// * Much more efficient culling. Meshlets can be culled individually, instead of all or nothing culling for entire meshes at a time. +/// Additionally, occlusion culling can eliminate meshlets that would cause overdraw. +/// * Much more efficient batching. All geometry can be rasterized in a single draw. +/// * Scales better with large amounts of dense geometry and overdraw. Bevy's standard renderer will bottleneck sooner. +/// * Near-seamless level of detail (LOD). +/// * Much greater base overhead. Rendering will be slower and use more memory than Bevy's standard renderer +/// with small amounts of geometry and overdraw. +/// * Requires preprocessing meshes. See [`MeshletMesh`] for details. +/// * Limitations on the kinds of materials you can use. See [`MeshletMesh`] for details. +/// +/// This plugin requires a fairly recent GPU that supports [`WgpuFeatures::TEXTURE_INT64_ATOMIC`]. +/// +/// This plugin currently works only on the Vulkan and Metal backends. +/// +/// This plugin is not compatible with [`Msaa`]. Any camera rendering a [`MeshletMesh`] must have +/// [`Msaa`] set to [`Msaa::Off`]. +/// +/// Mixing forward+prepass and deferred rendering for opaque materials is not currently supported when using this plugin. +/// You must use one or the other by setting [`crate::DefaultOpaqueRendererMethod`]. +/// Do not override [`crate::Material::opaque_render_method`] for any material when using this plugin. +/// +/// ![A render of the Stanford dragon as a `MeshletMesh`](https://raw.githubusercontent.com/bevyengine/bevy/main/crates/bevy_pbr/src/meshlet/meshlet_preview.png) +pub struct MeshletPlugin { + /// The maximum amount of clusters that can be processed at once, + /// used to control the size of a pre-allocated GPU buffer. + /// + /// If this number is too low, you'll see rendering artifacts like missing or blinking meshes. + /// + /// Each cluster slot costs 4 bytes of VRAM. + /// + /// Must not be greater than 2^25. + pub cluster_buffer_slots: u32, +} + +impl MeshletPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::TEXTURE_INT64_ATOMIC + | WgpuFeatures::TEXTURE_ATOMIC + | WgpuFeatures::SHADER_INT64 + | WgpuFeatures::SUBGROUP + | WgpuFeatures::DEPTH_CLIP_CONTROL + | WgpuFeatures::PUSH_CONSTANTS + } +} + +impl Plugin for MeshletPlugin { + fn build(&self, app: &mut App) { + #[cfg(target_endian = "big")] + compile_error!("MeshletPlugin is only supported on little-endian processors."); + + if self.cluster_buffer_slots > 2_u32.pow(25) { + error!("MeshletPlugin::cluster_buffer_slots must not be greater than 2^25."); + std::process::exit(1); + } + + load_shader_library!(app, "meshlet_bindings.wgsl"); + load_shader_library!(app, "visibility_buffer_resolve.wgsl"); + load_shader_library!(app, "meshlet_cull_shared.wgsl"); + embedded_asset!(app, "clear_visibility_buffer.wgsl"); + embedded_asset!(app, "cull_instances.wgsl"); + embedded_asset!(app, "cull_bvh.wgsl"); + embedded_asset!(app, "cull_clusters.wgsl"); + embedded_asset!(app, "visibility_buffer_software_raster.wgsl"); + embedded_asset!(app, "visibility_buffer_hardware_raster.wgsl"); + embedded_asset!(app, "meshlet_mesh_material.wgsl"); + embedded_asset!(app, "resolve_render_targets.wgsl"); + embedded_asset!(app, "remap_1d_to_2d_dispatch.wgsl"); + embedded_asset!(app, "fill_counts.wgsl"); + + app.init_asset::() + .register_asset_loader(MeshletMeshLoader); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Create a variable here so we can move-capture it. + let cluster_buffer_slots = self.cluster_buffer_slots; + let init_resource_manager_system = + move |mut commands: Commands, render_device: Res| { + commands + .insert_resource(ResourceManager::new(cluster_buffer_slots, &render_device)); + }; + + render_app + .add_render_graph_node::( + Core3d, + NodeMeshlet::VisibilityBufferRasterPass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::Prepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::DeferredPrepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::MainOpaquePass, + ) + .add_render_graph_edges( + Core3d, + ( + NodeMeshlet::VisibilityBufferRasterPass, + NodePbr::EarlyShadowPass, + // + NodeMeshlet::Prepass, + // + NodeMeshlet::DeferredPrepass, + Node3d::EndPrepasses, + // + Node3d::StartMainPass, + NodeMeshlet::MainOpaquePass, + Node3d::MainOpaquePass, + Node3d::EndMainPass, + ), + ) + .insert_resource(InstanceManager::new()) + .add_systems( + RenderStartup, + ( + check_meshlet_features, + ( + (init_resource_manager_system, init_meshlet_pipelines).chain(), + init_meshlet_mesh_manager, + ), + ) + .chain(), + ) + .add_systems(ExtractSchedule, extract_meshlet_mesh_entities) + .add_systems( + Render, + ( + perform_pending_meshlet_mesh_writes.in_set(RenderSystems::PrepareAssets), + configure_meshlet_views + .after(prepare_view_targets) + .in_set(RenderSystems::ManageViews), + prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), + prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + queue_material_meshlet_meshes.in_set(RenderSystems::QueueMeshes), + prepare_material_meshlet_meshes_main_opaque_pass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes), + ), + ); + } +} + +fn check_meshlet_features(render_device: Res) { + let features = render_device.features(); + if !features.contains(MeshletPlugin::required_wgpu_features()) { + error!( + "MeshletPlugin can't be used. GPU lacks support for required features: {:?}.", + MeshletPlugin::required_wgpu_features().difference(features) + ); + std::process::exit(1); + } +} + +/// The meshlet mesh equivalent of [`bevy_mesh::Mesh3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(Transform, PreviousGlobalTransform, Visibility, VisibilityClass)] +#[component(on_add = visibility::add_visibility_class::)] +pub struct MeshletMesh3d(pub Handle); + +impl From for AssetId { + fn from(mesh: MeshletMesh3d) -> Self { + mesh.id() + } +} + +impl From<&MeshletMesh3d> for AssetId { + fn from(mesh: &MeshletMesh3d) -> Self { + mesh.id() + } +} + +fn configure_meshlet_views( + mut views_3d: Query<( + Entity, + &Msaa, + Has, + Has, + Has, + )>, + mut commands: Commands, +) { + for (entity, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in &mut views_3d { + if *msaa != Msaa::Off { + error!("MeshletPlugin can't be used with MSAA. Add Msaa::Off to your camera to use this plugin."); + std::process::exit(1); + } + + if !(normal_prepass || motion_vector_prepass || deferred_prepass) { + commands + .entity(entity) + .insert(MeshletViewMaterialsMainOpaquePass::default()); + } else { + // TODO: Should we add both Prepass and DeferredGBufferPrepass materials here, and in other systems/nodes? + commands.entity(entity).insert(( + MeshletViewMaterialsMainOpaquePass::default(), + MeshletViewMaterialsPrepass::default(), + MeshletViewMaterialsDeferredGBufferPrepass::default(), + )); + } + } +} diff --git a/vendor/bevy_pbr/src/meshlet/persistent_buffer.rs b/vendor/bevy_pbr/src/meshlet/persistent_buffer.rs new file mode 100644 index 00000000..bae7b670 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -0,0 +1,132 @@ +use bevy_render::{ + render_resource::{ + BindingResource, Buffer, BufferAddress, BufferDescriptor, BufferUsages, + CommandEncoderDescriptor, COPY_BUFFER_ALIGNMENT, + }, + renderer::{RenderDevice, RenderQueue}, +}; +use core::{num::NonZero, ops::Range}; +use range_alloc::RangeAllocator; + +/// Wrapper for a GPU buffer holding a large amount of data that persists across frames. +pub struct PersistentGpuBuffer { + /// Debug label for the buffer. + label: &'static str, + /// Handle to the GPU buffer. + buffer: Buffer, + /// Tracks free slices of the buffer. + allocation_planner: RangeAllocator, + /// Queue of pending writes, and associated metadata. + write_queue: Vec<(T, T::Metadata, Range)>, +} + +impl PersistentGpuBuffer { + /// Create a new persistent buffer. + pub fn new(label: &'static str, render_device: &RenderDevice) -> Self { + Self { + label, + buffer: render_device.create_buffer(&BufferDescriptor { + label: Some(label), + size: 0, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }), + allocation_planner: RangeAllocator::new(0..0), + write_queue: Vec::new(), + } + } + + /// Queue an item of type T to be added to the buffer, returning the byte range within the buffer that it will be located at. + pub fn queue_write(&mut self, data: T, metadata: T::Metadata) -> Range { + let data_size = data.size_in_bytes() as u64; + debug_assert!(data_size.is_multiple_of(COPY_BUFFER_ALIGNMENT)); + if let Ok(buffer_slice) = self.allocation_planner.allocate_range(data_size) { + self.write_queue + .push((data, metadata, buffer_slice.clone())); + return buffer_slice; + } + + let buffer_size = self.allocation_planner.initial_range(); + let double_buffer_size = (buffer_size.end - buffer_size.start) * 2; + let new_size = double_buffer_size.max(data_size); + self.allocation_planner.grow_to(buffer_size.end + new_size); + + let buffer_slice = self.allocation_planner.allocate_range(data_size).unwrap(); + self.write_queue + .push((data, metadata, buffer_slice.clone())); + buffer_slice + } + + /// Upload all pending data to the GPU buffer. + pub fn perform_writes(&mut self, render_queue: &RenderQueue, render_device: &RenderDevice) { + if self.allocation_planner.initial_range().end > self.buffer.size() { + self.expand_buffer(render_device, render_queue); + } + + let queue_count = self.write_queue.len(); + + for (data, metadata, buffer_slice) in self.write_queue.drain(..) { + let buffer_slice_size = + NonZero::::new(buffer_slice.end - buffer_slice.start).unwrap(); + let mut buffer_view = render_queue + .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) + .unwrap(); + data.write_bytes_le(metadata, &mut buffer_view, buffer_slice.start); + } + + let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32; + if queue_saturation < 0.3 { + self.write_queue = Vec::new(); + } + } + + /// Mark a section of the GPU buffer as no longer needed. + pub fn mark_slice_unused(&mut self, buffer_slice: Range) { + self.allocation_planner.free_range(buffer_slice); + } + + pub fn binding(&self) -> BindingResource<'_> { + self.buffer.as_entire_binding() + } + + /// Expand the buffer by creating a new buffer and copying old data over. + fn expand_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + let size = self.allocation_planner.initial_range(); + let new_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some(self.label), + size: size.end - size.start, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("persistent_gpu_buffer_expand"), + }); + command_encoder.copy_buffer_to_buffer(&self.buffer, 0, &new_buffer, 0, self.buffer.size()); + render_queue.submit([command_encoder.finish()]); + + self.buffer = new_buffer; + } +} + +/// A trait representing data that can be written to a [`PersistentGpuBuffer`]. +pub trait PersistentGpuBufferable { + /// Additional metadata associated with each item, made available during `write_bytes_le`. + type Metadata; + + /// The size in bytes of `self`. This will determine the size of the buffer passed into + /// `write_bytes_le`. + /// + /// All data written must be in a multiple of `wgpu::COPY_BUFFER_ALIGNMENT` bytes. Failure to do so will + /// result in a panic when using [`PersistentGpuBuffer`]. + fn size_in_bytes(&self) -> usize; + + /// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice. + /// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU. + fn write_bytes_le( + &self, + metadata: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ); +} diff --git a/vendor/bevy_pbr/src/meshlet/persistent_buffer_impls.rs b/vendor/bevy_pbr/src/meshlet/persistent_buffer_impls.rs new file mode 100644 index 00000000..210a52be --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/persistent_buffer_impls.rs @@ -0,0 +1,128 @@ +use crate::meshlet::asset::{BvhNode, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBufferable}; +use alloc::sync::Arc; +use bevy_math::Vec2; +use bevy_render::render_resource::BufferAddress; + +impl PersistentGpuBufferable for Arc<[BvhNode]> { + type Metadata = u32; + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + base_meshlet_index: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ) { + const SIZE: usize = size_of::(); + for (i, &node) in self.iter().enumerate() { + let bytes: [u8; SIZE] = + bytemuck::cast(node.offset_aabbs(base_meshlet_index, buffer_offset)); + buffer_slice[i * SIZE..(i + 1) * SIZE].copy_from_slice(&bytes); + } + } +} + +impl BvhNode { + fn offset_aabbs(mut self, base_meshlet_index: u32, buffer_offset: BufferAddress) -> Self { + let size = size_of::(); + let base_bvh_node_index = (buffer_offset / size as u64) as u32; + for i in 0..self.aabbs.len() { + self.aabbs[i].child_offset += if self.child_is_bvh_node(i) { + base_bvh_node_index + } else { + base_meshlet_index + }; + } + self + } + + fn child_is_bvh_node(&self, i: usize) -> bool { + self.child_counts[i] == u8::MAX + } +} + +impl PersistentGpuBufferable for Arc<[Meshlet]> { + type Metadata = (u64, u64, u64); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + (vertex_position_offset, vertex_attribute_offset, index_offset): Self::Metadata, + buffer_slice: &mut [u8], + _: BufferAddress, + ) { + let vertex_position_offset = (vertex_position_offset * 8) as u32; + let vertex_attribute_offset = (vertex_attribute_offset as usize / size_of::()) as u32; + let index_offset = index_offset as u32; + + for (i, meshlet) in self.iter().enumerate() { + let size = size_of::(); + let i = i * size; + let bytes = bytemuck::cast::<_, [u8; size_of::()]>(Meshlet { + start_vertex_position_bit: meshlet.start_vertex_position_bit + + vertex_position_offset, + start_vertex_attribute_id: meshlet.start_vertex_attribute_id + + vertex_attribute_offset, + start_index_id: meshlet.start_index_id + index_offset, + ..*meshlet + }); + buffer_slice[i..(i + size)].clone_from_slice(&bytes); + } + } +} + +impl PersistentGpuBufferable for Arc<[MeshletCullData]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + +impl PersistentGpuBufferable for Arc<[u8]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(self); + } +} + +impl PersistentGpuBufferable for Arc<[u32]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + +impl PersistentGpuBufferable for Arc<[Vec2]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} diff --git a/vendor/bevy_pbr/src/meshlet/pipelines.rs b/vendor/bevy_pbr/src/meshlet/pipelines.rs new file mode 100644 index 00000000..04d833a3 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/pipelines.rs @@ -0,0 +1,580 @@ +use super::resource_manager::ResourceManager; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::{ + core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DownsampleDepthShader, + FullscreenShader, +}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res}, + world::World, +}; +use bevy_render::render_resource::*; +use bevy_shader::Shader; +use bevy_utils::default; + +#[derive(Resource)] +pub struct MeshletPipelines { + clear_visibility_buffer: CachedComputePipelineId, + clear_visibility_buffer_shadow_view: CachedComputePipelineId, + first_instance_cull: CachedComputePipelineId, + second_instance_cull: CachedComputePipelineId, + first_bvh_cull: CachedComputePipelineId, + second_bvh_cull: CachedComputePipelineId, + first_meshlet_cull: CachedComputePipelineId, + second_meshlet_cull: CachedComputePipelineId, + downsample_depth_first: CachedComputePipelineId, + downsample_depth_second: CachedComputePipelineId, + downsample_depth_first_shadow_view: CachedComputePipelineId, + downsample_depth_second_shadow_view: CachedComputePipelineId, + visibility_buffer_software_raster: CachedComputePipelineId, + visibility_buffer_software_raster_shadow_view: CachedComputePipelineId, + visibility_buffer_hardware_raster: CachedRenderPipelineId, + visibility_buffer_hardware_raster_shadow_view: CachedRenderPipelineId, + visibility_buffer_hardware_raster_shadow_view_unclipped: CachedRenderPipelineId, + resolve_depth: CachedRenderPipelineId, + resolve_depth_shadow_view: CachedRenderPipelineId, + resolve_material_depth: CachedRenderPipelineId, + remap_1d_to_2d_dispatch: Option, + fill_counts: CachedComputePipelineId, + pub(crate) meshlet_mesh_material: Handle, +} + +pub fn init_meshlet_pipelines( + mut commands: Commands, + resource_manager: Res, + fullscreen_shader: Res, + downsample_depth_shader: Res, + pipeline_cache: Res, + asset_server: Res, +) { + let clear_visibility_buffer_bind_group_layout = resource_manager + .clear_visibility_buffer_bind_group_layout + .clone(); + let clear_visibility_buffer_shadow_view_bind_group_layout = resource_manager + .clear_visibility_buffer_shadow_view_bind_group_layout + .clone(); + let first_instance_cull_bind_group_layout = resource_manager + .first_instance_cull_bind_group_layout + .clone(); + let second_instance_cull_bind_group_layout = resource_manager + .second_instance_cull_bind_group_layout + .clone(); + let first_bvh_cull_bind_group_layout = + resource_manager.first_bvh_cull_bind_group_layout.clone(); + let second_bvh_cull_bind_group_layout = + resource_manager.second_bvh_cull_bind_group_layout.clone(); + let first_meshlet_cull_bind_group_layout = resource_manager + .first_meshlet_cull_bind_group_layout + .clone(); + let second_meshlet_cull_bind_group_layout = resource_manager + .second_meshlet_cull_bind_group_layout + .clone(); + let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone(); + let downsample_depth_shadow_view_layout = resource_manager + .downsample_depth_shadow_view_bind_group_layout + .clone(); + let visibility_buffer_raster_layout = resource_manager + .visibility_buffer_raster_bind_group_layout + .clone(); + let visibility_buffer_raster_shadow_view_layout = resource_manager + .visibility_buffer_raster_shadow_view_bind_group_layout + .clone(); + let resolve_depth_layout = resource_manager.resolve_depth_bind_group_layout.clone(); + let resolve_depth_shadow_view_layout = resource_manager + .resolve_depth_shadow_view_bind_group_layout + .clone(); + let resolve_material_depth_layout = resource_manager + .resolve_material_depth_bind_group_layout + .clone(); + let remap_1d_to_2d_dispatch_layout = resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .clone(); + + let downsample_depth_shader = (*downsample_depth_shader).clone(); + let vertex_state = fullscreen_shader.to_vertex_state(); + let fill_counts_layout = resource_manager.fill_counts_bind_group_layout.clone(); + + let clear_visibility_buffer = + load_embedded_asset!(asset_server.as_ref(), "clear_visibility_buffer.wgsl"); + let cull_instances = load_embedded_asset!(asset_server.as_ref(), "cull_instances.wgsl"); + let cull_bvh = load_embedded_asset!(asset_server.as_ref(), "cull_bvh.wgsl"); + let cull_clusters = load_embedded_asset!(asset_server.as_ref(), "cull_clusters.wgsl"); + let visibility_buffer_software_raster = load_embedded_asset!( + asset_server.as_ref(), + "visibility_buffer_software_raster.wgsl" + ); + let visibility_buffer_hardware_raster = load_embedded_asset!( + asset_server.as_ref(), + "visibility_buffer_hardware_raster.wgsl" + ); + let resolve_render_targets = + load_embedded_asset!(asset_server.as_ref(), "resolve_render_targets.wgsl"); + let remap_1d_to_2d_dispatch = + load_embedded_asset!(asset_server.as_ref(), "remap_1d_to_2d_dispatch.wgsl"); + let fill_counts = load_embedded_asset!(asset_server.as_ref(), "fill_counts.wgsl"); + let meshlet_mesh_material = + load_embedded_asset!(asset_server.as_ref(), "meshlet_mesh_material.wgsl"); + + commands.insert_resource(MeshletPipelines { + clear_visibility_buffer: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_clear_visibility_buffer_pipeline".into()), + layout: vec![clear_visibility_buffer_bind_group_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: clear_visibility_buffer.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + ..default() + }), + + clear_visibility_buffer_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_clear_visibility_buffer_shadow_view_pipeline".into()), + layout: vec![clear_visibility_buffer_shadow_view_bind_group_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: clear_visibility_buffer, + ..default() + }, + ), + + first_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_instance_cull_pipeline".into()), + layout: vec![first_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances.clone(), + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_instance_cull_pipeline".into()), + layout: vec![second_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances, + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + first_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_bvh_cull_pipeline".into()), + layout: vec![first_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh.clone(), + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_bvh_cull_pipeline".into()), + layout: vec![second_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh, + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + first_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_meshlet_cull_pipeline".into()), + layout: vec![first_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters.clone(), + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_meshlet_cull_pipeline".into()), + layout: vec![second_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters, + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + downsample_depth_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_first_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + "MESHLET".into(), + ], + entry_point: Some("downsample_depth_first".into()), + ..default() + }), + + downsample_depth_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_second_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + "MESHLET".into(), + ], + entry_point: Some("downsample_depth_second".into()), + ..default() + }), + + downsample_depth_first_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_first_pipeline".into()), + layout: vec![downsample_depth_shadow_view_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec!["MESHLET".into()], + entry_point: Some("downsample_depth_first".into()), + ..default() + }, + ), + + downsample_depth_second_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_second_pipeline".into()), + layout: vec![downsample_depth_shadow_view_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader, + shader_defs: vec!["MESHLET".into()], + entry_point: Some("downsample_depth_second".into()), + zero_initialize_workgroup_memory: false, + }, + ), + + visibility_buffer_software_raster: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_visibility_buffer_software_raster_pipeline".into()), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![], + shader: visibility_buffer_software_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + ..default() + }, + ), + + visibility_buffer_software_raster_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_software_raster_shadow_view_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], + push_constant_ranges: vec![], + shader: visibility_buffer_software_raster, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + ..default() + }, + ), + + visibility_buffer_hardware_raster: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_pipeline".into()), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }, + ), + + visibility_buffer_hardware_raster_shadow_view: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_hardware_raster_shadow_view_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }, + ), + + visibility_buffer_hardware_raster_shadow_view_unclipped: pipeline_cache + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline" + .into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }), + + resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_layout], + vertex: vertex_state.clone(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: Some("resolve_depth".into()), + ..default() + }), + ..default() + }), + + resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_shadow_view_layout], + vertex: vertex_state.clone(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets.clone(), + entry_point: Some("resolve_depth".into()), + ..default() + }), + ..default() + }), + + resolve_material_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_material_depth_pipeline".into()), + layout: vec![resolve_material_depth_layout], + vertex: vertex_state, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: Some("resolve_material_depth".into()), + targets: vec![], + }), + ..default() + }), + + fill_counts: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_fill_counts_pipeline".into()), + layout: vec![fill_counts_layout], + shader: fill_counts, + shader_defs: vec![if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into()], + ..default() + }), + + remap_1d_to_2d_dispatch: remap_1d_to_2d_dispatch_layout.map(|layout| { + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_remap_1d_to_2d_dispatch_pipeline".into()), + layout: vec![layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: remap_1d_to_2d_dispatch, + ..default() + }) + }), + + meshlet_mesh_material, + }); +} + +impl MeshletPipelines { + pub fn get( + world: &World, + ) -> Option<( + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + Option<&ComputePipeline>, + &ComputePipeline, + )> { + let pipeline_cache = world.get_resource::()?; + let pipeline = world.get_resource::()?; + Some(( + pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer)?, + pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.first_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.visibility_buffer_software_raster)?, + pipeline_cache + .get_compute_pipeline(pipeline.visibility_buffer_software_raster_shadow_view)?, + pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_hardware_raster)?, + pipeline_cache + .get_render_pipeline(pipeline.visibility_buffer_hardware_raster_shadow_view)?, + pipeline_cache.get_render_pipeline( + pipeline.visibility_buffer_hardware_raster_shadow_view_unclipped, + )?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth_shadow_view)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_material_depth)?, + match pipeline.remap_1d_to_2d_dispatch { + Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?), + None => None, + }, + pipeline_cache.get_compute_pipeline(pipeline.fill_counts)?, + )) + } +} diff --git a/vendor/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl b/vendor/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl new file mode 100644 index 00000000..b9970c42 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/remap_1d_to_2d_dispatch.wgsl @@ -0,0 +1,24 @@ +/// Remaps an indirect 1d to 2d dispatch for devices with low dispatch size limit. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_software_raster_cluster_count: u32; +var max_compute_workgroups_per_dimension: u32; + +@compute +@workgroup_size(1, 1, 1) +fn remap_dispatch() { + let cluster_count = meshlet_software_raster_indirect_args.x; + + if cluster_count > max_compute_workgroups_per_dimension { + let n = u32(ceil(sqrt(f32(cluster_count)))); + meshlet_software_raster_indirect_args.x = n; + meshlet_software_raster_indirect_args.y = n; + meshlet_software_raster_cluster_count = cluster_count; + } +} diff --git a/vendor/bevy_pbr/src/meshlet/resolve_render_targets.wgsl b/vendor/bevy_pbr/src/meshlet/resolve_render_targets.wgsl new file mode 100644 index 00000000..6fef0cc2 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/resolve_render_targets.wgsl @@ -0,0 +1,41 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::meshlet_bindings::InstancedOffset + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#endif +@group(0) @binding(1) var meshlet_raster_clusters: array; // Per cluster +@group(0) @binding(2) var meshlet_instance_material_ids: array; // Per entity instance + +/// This pass writes out the depth texture. +@fragment +fn resolve_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let visibility = textureLoad(meshlet_visibility_buffer, vec2(in.position.xy)).r; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let depth = u32(visibility >> 32u); +#else + let depth = visibility; +#endif + + if depth == 0u { discard; } + + return bitcast(depth); +} + +/// This pass writes out the material depth texture. +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@fragment +fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let visibility = textureLoad(meshlet_visibility_buffer, vec2(in.position.xy)).r; + + let depth = visibility >> 32u; + if depth == 0lu { discard; } + + let cluster_id = u32(visibility) >> 7u; + let instance_id = meshlet_raster_clusters[cluster_id].instance_id; + let material_id = meshlet_instance_material_ids[instance_id]; + return f32(material_id) / 65535.0; +} +#endif diff --git a/vendor/bevy_pbr/src/meshlet/resource_manager.rs b/vendor/bevy_pbr/src/meshlet/resource_manager.rs new file mode 100644 index 00000000..e7335885 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/resource_manager.rs @@ -0,0 +1,1233 @@ +use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager}; +use crate::ShadowView; +use bevy_camera::{visibility::RenderLayers, Camera3d}; +use bevy_core_pipeline::{ + experimental::mip_generation::{self, ViewDepthPyramid}, + prepass::{PreviousViewData, PreviousViewUniforms}, +}; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityHashMap}, + query::AnyOf, + resource::Resource, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::ToExtents; +use bevy_math::{UVec2, Vec4Swizzles}; +use bevy_render::{ + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::{ExtractedView, ViewUniform, ViewUniforms}, +}; +use binding_types::*; +use core::iter; + +/// Manages per-view and per-cluster GPU resources for [`MeshletPlugin`](`super::MeshletPlugin`). +#[derive(Resource)] +pub struct ResourceManager { + /// Intermediate buffer of cluster IDs for use with rasterizing the visibility buffer + visibility_buffer_raster_clusters: Buffer, + /// Intermediate buffer of previous counts of clusters in rasterizer buckets + pub visibility_buffer_raster_cluster_prev_counts: Buffer, + /// Intermediate buffer of count of clusters to software rasterize + software_raster_cluster_count: Buffer, + /// BVH traversal queues + bvh_traversal_queues: [Buffer; 2], + /// Cluster cull candidate queue + cluster_cull_candidate_queue: Buffer, + /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`], [`Self::bvh_traversal_queues`], and [`Self::cluster_cull_candidate_queue`] + cull_queue_rightmost_slot: u32, + + /// Second pass instance candidates + second_pass_candidates: Option, + /// Sampler for a depth pyramid + depth_pyramid_sampler: Sampler, + /// Dummy texture view for binding depth pyramids with less than the maximum amount of mips + depth_pyramid_dummy_texture: TextureView, + + // TODO + previous_depth_pyramids: EntityHashMap, + + // Bind group layouts + pub clear_visibility_buffer_bind_group_layout: BindGroupLayoutDescriptor, + pub clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayoutDescriptor, + pub first_instance_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub second_instance_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub first_bvh_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub second_bvh_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub first_meshlet_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub second_meshlet_cull_bind_group_layout: BindGroupLayoutDescriptor, + pub visibility_buffer_raster_bind_group_layout: BindGroupLayoutDescriptor, + pub visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayoutDescriptor, + pub downsample_depth_bind_group_layout: BindGroupLayoutDescriptor, + pub downsample_depth_shadow_view_bind_group_layout: BindGroupLayoutDescriptor, + pub resolve_depth_bind_group_layout: BindGroupLayoutDescriptor, + pub resolve_depth_shadow_view_bind_group_layout: BindGroupLayoutDescriptor, + pub resolve_material_depth_bind_group_layout: BindGroupLayoutDescriptor, + pub material_shade_bind_group_layout: BindGroupLayoutDescriptor, + pub fill_counts_bind_group_layout: BindGroupLayoutDescriptor, + pub remap_1d_to_2d_dispatch_bind_group_layout: Option, +} + +impl ResourceManager { + pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self { + let needs_dispatch_remap = + cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension; + // The IDs are a (u32, u32) of instance and index. + let cull_queue_size = 2 * cluster_buffer_slots as u64 * size_of::() as u64; + + Self { + visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_clusters"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + visibility_buffer_raster_cluster_prev_counts: render_device.create_buffer( + &BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_cluster_prev_counts"), + size: size_of::() as u64 * 2, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }, + ), + software_raster_cluster_count: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_software_raster_cluster_count"), + size: size_of::() as u64, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + bvh_traversal_queues: [ + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_0"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_1"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ], + cluster_cull_candidate_queue: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_cull_candidate_queue"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + cull_queue_rightmost_slot: cluster_buffer_slots - 1, + + second_pass_candidates: None, + depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("meshlet_depth_pyramid_sampler"), + ..SamplerDescriptor::default() + }), + depth_pyramid_dummy_texture: mip_generation::create_depth_pyramid_dummy_texture( + render_device, + "meshlet_depth_pyramid_dummy_texture", + "meshlet_depth_pyramid_dummy_texture_view", + ), + + previous_depth_pyramids: EntityHashMap::default(), + + // TODO: Buffer min sizes + clear_visibility_buffer_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_clear_visibility_buffer_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::COMPUTE, + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::WriteOnly), + ), + ), + clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_clear_visibility_buffer_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::COMPUTE, + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), + ), + ), + first_instance_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_first_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + 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_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_instance_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_second_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + 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_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + first_bvh_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_first_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_bvh_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_second_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + first_meshlet_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_first_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_meshlet_cull_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_second_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + downsample_depth_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_downsample_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { + let write_only_r32float = || { + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) + }; + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + texture_storage_2d( + TextureFormat::R32Float, + StorageTextureAccess::ReadWrite, + ), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + sampler(SamplerBindingType::NonFiltering), + ) + }), + ), + downsample_depth_shadow_view_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_downsample_depth_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { + let write_only_r32float = || { + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) + }; + ( + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + texture_storage_2d( + TextureFormat::R32Float, + StorageTextureAccess::ReadWrite, + ), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + sampler(SamplerBindingType::NonFiltering), + ) + }), + ), + visibility_buffer_raster_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_visibility_buffer_raster_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ( + 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), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::Atomic), + uniform_buffer::(true), + ), + ), + ), + visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_visibility_buffer_raster_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ( + 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), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::Atomic), + uniform_buffer::(true), + ), + ), + ), + resolve_depth_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_resolve_depth_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + ), + ), + resolve_depth_shadow_view_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_resolve_depth_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly), + ), + ), + resolve_material_depth_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_resolve_material_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + material_shade_bind_group_layout: BindGroupLayoutDescriptor::new( + "meshlet_mesh_material_shade_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + 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), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + fill_counts_bind_group_layout: if needs_dispatch_remap { + BindGroupLayoutDescriptor::new( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + } else { + BindGroupLayoutDescriptor::new( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }, + remap_1d_to_2d_dispatch_bind_group_layout: needs_dispatch_remap.then(|| { + BindGroupLayoutDescriptor::new( + "meshlet_remap_1d_to_2d_dispatch_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }), + } + } +} + +// ------------ TODO: Everything under here needs to be rewritten and cached ------------ + +#[derive(Component)] +pub struct MeshletViewResources { + pub scene_instance_count: u32, + pub rightmost_slot: u32, + pub max_bvh_depth: u32, + instance_visibility: Buffer, + pub dummy_render_target: CachedTexture, + pub visibility_buffer: CachedTexture, + pub second_pass_count: Buffer, + pub second_pass_dispatch: Buffer, + pub second_pass_candidates: Buffer, + pub first_bvh_cull_count_front: Buffer, + pub first_bvh_cull_dispatch_front: Buffer, + pub first_bvh_cull_count_back: Buffer, + pub first_bvh_cull_dispatch_back: Buffer, + pub first_bvh_cull_queue: Buffer, + pub second_bvh_cull_count_front: Buffer, + pub second_bvh_cull_dispatch_front: Buffer, + pub second_bvh_cull_count_back: Buffer, + pub second_bvh_cull_dispatch_back: Buffer, + pub second_bvh_cull_queue: Buffer, + pub front_meshlet_cull_count: Buffer, + pub front_meshlet_cull_dispatch: Buffer, + pub back_meshlet_cull_count: Buffer, + pub back_meshlet_cull_dispatch: Buffer, + pub meshlet_cull_queue: Buffer, + pub visibility_buffer_software_raster_indirect_args: Buffer, + pub visibility_buffer_hardware_raster_indirect_args: Buffer, + pub depth_pyramid: ViewDepthPyramid, + previous_depth_pyramid: TextureView, + pub material_depth: Option, + pub view_size: UVec2, + not_shadow_view: bool, +} + +#[derive(Component)] +pub struct MeshletViewBindGroups { + pub clear_visibility_buffer: BindGroup, + pub first_instance_cull: BindGroup, + pub second_instance_cull: BindGroup, + pub first_bvh_cull_ping: BindGroup, + pub first_bvh_cull_pong: BindGroup, + pub second_bvh_cull_ping: BindGroup, + pub second_bvh_cull_pong: BindGroup, + pub first_meshlet_cull: BindGroup, + pub second_meshlet_cull: BindGroup, + pub downsample_depth: BindGroup, + pub visibility_buffer_raster: BindGroup, + pub resolve_depth: BindGroup, + pub resolve_material_depth: Option, + pub material_shade: Option, + pub remap_1d_to_2d_dispatch: Option, + pub fill_counts: BindGroup, +} + +// TODO: Cache things per-view and skip running this system / optimize this system +pub fn prepare_meshlet_per_frame_resources( + mut resource_manager: ResMut, + mut instance_manager: ResMut, + views: Query<( + Entity, + &ExtractedView, + Option<&RenderLayers>, + AnyOf<(&Camera3d, &ShadowView)>, + )>, + mut texture_cache: ResMut, + render_queue: Res, + render_device: Res, + mut commands: Commands, +) { + if instance_manager.scene_instance_count == 0 { + return; + } + + let instance_manager = instance_manager.as_mut(); + + // TODO: Move this and the submit to a separate system and remove pub from the fields + instance_manager + .instance_uniforms + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_aabbs + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_material_ids + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_bvh_root_nodes + .write_buffer(&render_device, &render_queue); + + let needed_buffer_size = 4 * instance_manager.scene_instance_count as u64; + let second_pass_candidates = match &mut resource_manager.second_pass_candidates { + Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), + slot => { + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_second_pass_candidates"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + + for (view_entity, view, render_layers, (_, shadow_view)) in &views { + let not_shadow_view = shadow_view.is_none(); + + let instance_visibility = instance_manager + .view_instance_visibility + .entry(view_entity) + .or_insert_with(|| { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_view_instance_visibility")); + buffer + }); + for (instance_index, (_, layers, not_shadow_caster)) in + instance_manager.instances.iter().enumerate() + { + // If either the layers don't match the view's layers or this is a shadow view + // and the instance is not a shadow caster, hide the instance for this view + if !render_layers + .unwrap_or(&RenderLayers::default()) + .intersects(layers) + || (shadow_view.is_some() && *not_shadow_caster) + { + let vec = instance_visibility.get_mut(); + let index = instance_index / 32; + let bit = instance_index - index * 32; + if vec.len() <= index { + vec.extend(iter::repeat_n(0, index - vec.len() + 1)); + } + vec[index] |= 1 << bit; + } + } + instance_visibility.write_buffer(&render_device, &render_queue); + let instance_visibility = instance_visibility.buffer().unwrap().clone(); + + // TODO: Remove this once wgpu allows render passes with no attachments + let dummy_render_target = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_dummy_render_target"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R8Uint, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + let visibility_buffer = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_visibility_buffer"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: if not_shadow_view { + TextureFormat::R64Uint + } else { + TextureFormat::R32Uint + }, + usage: TextureUsages::STORAGE_ATOMIC | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }, + ); + + let second_pass_count = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let second_pass_dispatch = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let first_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let first_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let second_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let second_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let front_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let front_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + let back_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let back_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let visibility_buffer_software_raster_indirect_args = render_device + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_software_raster_indirect_args"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let visibility_buffer_hardware_raster_indirect_args = render_device + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args"), + contents: DrawIndirectArgs { + vertex_count: 128 * 3, + instance_count: 0, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let depth_pyramid = ViewDepthPyramid::new( + &render_device, + &mut texture_cache, + &resource_manager.depth_pyramid_dummy_texture, + view.viewport.zw(), + "meshlet_depth_pyramid", + "meshlet_depth_pyramid_texture_view", + ); + + let previous_depth_pyramid = + match resource_manager.previous_depth_pyramids.get(&view_entity) { + Some(texture_view) => texture_view.clone(), + None => depth_pyramid.all_mips.clone(), + }; + resource_manager + .previous_depth_pyramids + .insert(view_entity, depth_pyramid.all_mips.clone()); + + let material_depth = TextureDescriptor { + label: Some("meshlet_material_depth"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth16Unorm, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + commands.entity(view_entity).insert(MeshletViewResources { + scene_instance_count: instance_manager.scene_instance_count, + rightmost_slot: resource_manager.cull_queue_rightmost_slot, + max_bvh_depth: instance_manager.max_bvh_depth, + instance_visibility, + dummy_render_target, + visibility_buffer, + second_pass_count, + second_pass_dispatch, + second_pass_candidates: second_pass_candidates.clone(), + first_bvh_cull_count_front, + first_bvh_cull_dispatch_front, + first_bvh_cull_count_back, + first_bvh_cull_dispatch_back, + first_bvh_cull_queue: resource_manager.bvh_traversal_queues[0].clone(), + second_bvh_cull_count_front, + second_bvh_cull_dispatch_front, + second_bvh_cull_count_back, + second_bvh_cull_dispatch_back, + second_bvh_cull_queue: resource_manager.bvh_traversal_queues[1].clone(), + front_meshlet_cull_count, + front_meshlet_cull_dispatch, + back_meshlet_cull_count, + back_meshlet_cull_dispatch, + meshlet_cull_queue: resource_manager.cluster_cull_candidate_queue.clone(), + visibility_buffer_software_raster_indirect_args, + visibility_buffer_hardware_raster_indirect_args, + depth_pyramid, + previous_depth_pyramid, + material_depth: not_shadow_view + .then(|| texture_cache.get(&render_device, material_depth)), + view_size: view.viewport.zw(), + not_shadow_view, + }); + } +} + +pub fn prepare_meshlet_view_bind_groups( + meshlet_mesh_manager: Res, + resource_manager: Res, + instance_manager: Res, + views: Query<(Entity, &MeshletViewResources)>, + view_uniforms: Res, + previous_view_uniforms: Res, + render_device: Res, + pipeline_cache: Res, + mut commands: Commands, +) { + let (Some(view_uniforms), Some(previous_view_uniforms)) = ( + view_uniforms.uniforms.binding(), + previous_view_uniforms.uniforms.binding(), + ) else { + return; + }; + + // TODO: Some of these bind groups can be reused across multiple views + for (view_entity, view_resources) in &views { + let clear_visibility_buffer = render_device.create_bind_group( + "meshlet_clear_visibility_buffer_bind_group", + &pipeline_cache.get_bind_group_layout(if view_resources.not_shadow_view { + &resource_manager.clear_visibility_buffer_bind_group_layout + } else { + &resource_manager.clear_visibility_buffer_shadow_view_bind_group_layout + }), + &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), + ); + + let first_instance_cull = render_device.create_bind_group( + "meshlet_first_instance_cull_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.first_instance_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_dispatch.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let second_instance_cull = render_device.create_bind_group( + "meshlet_second_instance_cull_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.second_instance_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let first_bvh_cull_ping = render_device.create_bind_group( + "meshlet_first_bvh_cull_ping_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.first_bvh_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let first_bvh_cull_pong = render_device.create_bind_group( + "meshlet_first_bvh_cull_pong_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.first_bvh_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_ping = render_device.create_bind_group( + "meshlet_second_bvh_cull_ping_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.second_bvh_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_pong = render_device.create_bind_group( + "meshlet_second_bvh_cull_pong_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.second_bvh_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let first_meshlet_cull = render_device.create_bind_group( + "meshlet_first_meshlet_cull_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.first_meshlet_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_meshlet_cull = render_device.create_bind_group( + "meshlet_second_meshlet_cull_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.second_meshlet_cull_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let downsample_depth = view_resources.depth_pyramid.create_bind_group( + &render_device, + "meshlet_downsample_depth_bind_group", + &pipeline_cache.get_bind_group_layout(if view_resources.not_shadow_view { + &resource_manager.downsample_depth_bind_group_layout + } else { + &resource_manager.downsample_depth_shadow_view_bind_group_layout + }), + &view_resources.visibility_buffer.default_view, + &resource_manager.depth_pyramid_sampler, + ); + + let visibility_buffer_raster = render_device.create_bind_group( + "meshlet_visibility_raster_buffer_bind_group", + &pipeline_cache.get_bind_group_layout(if view_resources.not_shadow_view { + &resource_manager.visibility_buffer_raster_bind_group_layout + } else { + &resource_manager.visibility_buffer_raster_shadow_view_bind_group_layout + }), + &BindGroupEntries::sequential(( + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + &view_resources.visibility_buffer.default_view, + view_uniforms.clone(), + )), + ); + + let resolve_depth = render_device.create_bind_group( + "meshlet_resolve_depth_bind_group", + &pipeline_cache.get_bind_group_layout(if view_resources.not_shadow_view { + &resource_manager.resolve_depth_bind_group_layout + } else { + &resource_manager.resolve_depth_shadow_view_bind_group_layout + }), + &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), + ); + + let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| { + render_device.create_bind_group( + "meshlet_resolve_material_depth_bind_group", + &pipeline_cache.get_bind_group_layout( + &resource_manager.resolve_material_depth_bind_group_layout, + ), + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + instance_manager.instance_material_ids.binding().unwrap(), + )), + ) + }); + + let material_shade = view_resources.material_depth.as_ref().map(|_| { + render_device.create_bind_group( + "meshlet_mesh_material_shade_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.material_shade_bind_group_layout), + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + meshlet_mesh_manager.vertex_normals.binding(), + meshlet_mesh_manager.vertex_uvs.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + )), + ) + }); + + let remap_1d_to_2d_dispatch = resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .as_ref() + .map(|layout| { + render_device.create_bind_group( + "meshlet_remap_1d_to_2d_dispatch_bind_group", + &pipeline_cache.get_bind_group_layout(layout), + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + }); + + let fill_counts = if resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .is_some() + { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.fill_counts_bind_group_layout), + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + } else { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &pipeline_cache + .get_bind_group_layout(&resource_manager.fill_counts_bind_group_layout), + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + )), + ) + }; + + commands.entity(view_entity).insert(MeshletViewBindGroups { + clear_visibility_buffer, + first_instance_cull, + second_instance_cull, + first_bvh_cull_ping, + first_bvh_cull_pong, + second_bvh_cull_ping, + second_bvh_cull_pong, + first_meshlet_cull, + second_meshlet_cull, + downsample_depth, + visibility_buffer_raster, + resolve_depth, + resolve_material_depth, + material_shade, + remap_1d_to_2d_dispatch, + fill_counts, + }); + } +} diff --git a/vendor/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl b/vendor/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl new file mode 100644 index 00000000..2a251443 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl @@ -0,0 +1,79 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_cluster_meshlet_ids, + meshlets, + meshlet_cluster_instance_ids, + meshlet_instance_uniforms, + meshlet_raster_clusters, + meshlet_previous_raster_counts, + meshlet_visibility_buffer, + view, + get_meshlet_triangle_count, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + }, + mesh_functions::mesh_position_local_to_world, +} +#import bevy_render::maths::affine3_to_square +var meshlet_raster_cluster_rightmost_slot: u32; + +/// Vertex/fragment shader for rasterizing large clusters into a visibility buffer. + +struct VertexOutput { + @builtin(position) position: vec4, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + @location(0) @interpolate(flat) packed_ids: u32, +#endif +} + +@vertex +fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let cluster_in_draw = meshlet_previous_raster_counts[1] + instance_index; + let cluster_id = meshlet_raster_cluster_rightmost_slot - cluster_in_draw; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; + + let triangle_id = vertex_index / 3u; + if triangle_id >= get_meshlet_triangle_count(&meshlet) { return dummy_vertex(); } + let index_id = vertex_index; + let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + index_id); + + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; + + let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); + let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0); + + return VertexOutput( + clip_position, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + (cluster_id << 7u) | triangle_id, +#endif + ); +} + +@fragment +fn fragment(vertex_output: VertexOutput) { + let depth = bitcast(vertex_output.position.z); +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let visibility = (u64(depth) << 32u) | u64(vertex_output.packed_ids); +#else + let visibility = depth; +#endif + textureAtomicMax(meshlet_visibility_buffer, vec2(vertex_output.position.xy), visibility); +} + +fn dummy_vertex() -> VertexOutput { + return VertexOutput( + vec4(divide(0.0, 0.0)), // NaN vertex position +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + 0u, +#endif + ); +} + +// Naga doesn't allow divide by zero literals, but this lets us work around it +fn divide(a: f32, b: f32) -> f32 { + return a / b; +} diff --git a/vendor/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs b/vendor/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs new file mode 100644 index 00000000..fee47413 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -0,0 +1,706 @@ +use super::{ + pipelines::MeshletPipelines, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, +}; +use crate::{ + meshlet::resource_manager::ResourceManager, LightEntity, ShadowView, ViewLightEntities, +}; +use bevy_color::LinearRgba; +use bevy_core_pipeline::prepass::PreviousViewUniformOffset; +use bevy_ecs::{ + query::QueryState, + world::{FromWorld, World}, +}; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::*, + renderer::RenderContext, + view::{ViewDepthTexture, ViewUniformOffset}, +}; + +/// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. +pub struct MeshletVisibilityBufferRasterPassNode { + main_view_query: QueryState<( + &'static ExtractedCamera, + &'static ViewDepthTexture, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + &'static ViewLightEntities, + )>, + view_light_query: QueryState<( + &'static ShadowView, + &'static LightEntity, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + )>, +} + +impl FromWorld for MeshletVisibilityBufferRasterPassNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + view_light_query: QueryState::new(world), + } + } +} + +impl Node for MeshletVisibilityBufferRasterPassNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.view_light_query.update_archetypes(world); + } + + // TODO: Reuse compute/render passes between logical passes where possible, as they're expensive + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let Ok(( + camera, + view_depth, + view_offset, + previous_view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + lights, + )) = self.main_view_query.get_manual(world, graph.view_entity()) + else { + return Ok(()); + }; + + let Some(( + clear_visibility_buffer_pipeline, + clear_visibility_buffer_shadow_view_pipeline, + first_instance_cull_pipeline, + second_instance_cull_pipeline, + first_bvh_cull_pipeline, + second_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + second_meshlet_cull_pipeline, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + visibility_buffer_software_raster_pipeline, + visibility_buffer_software_raster_shadow_view_pipeline, + visibility_buffer_hardware_raster_pipeline, + visibility_buffer_hardware_raster_shadow_view_pipeline, + visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline, + resolve_depth_pipeline, + resolve_depth_shadow_view_pipeline, + resolve_material_depth_pipeline, + remap_1d_to_2d_dispatch_pipeline, + fill_counts_pipeline, + )) = MeshletPipelines::get(world) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + render_context + .command_encoder() + .push_debug_group("meshlet_visibility_buffer_raster"); + let time_span = diagnostics.time_span( + render_context.command_encoder(), + "meshlet_visibility_buffer_raster", + ); + + let resource_manager = world.get_resource::().unwrap(); + render_context.command_encoder().clear_buffer( + &resource_manager.visibility_buffer_raster_cluster_prev_counts, + 0, + None, + ); + + clear_visibility_buffer_pass( + render_context, + &meshlet_view_bind_groups.clear_visibility_buffer, + clear_visibility_buffer_pipeline, + meshlet_view_resources.view_size, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + true, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + Some(camera), + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + false, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + Some(camera), + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + resolve_depth( + render_context, + view_depth.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + resolve_depth_pipeline, + camera, + ); + resolve_material_depth( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + resolve_material_depth_pipeline, + camera, + ); + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + ); + render_context.command_encoder().pop_debug_group(); + + for light_entity in &lights.lights { + let Ok(( + shadow_view, + light_type, + view_offset, + previous_view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + )) = self.view_light_query.get_manual(world, *light_entity) + else { + continue; + }; + + let shadow_visibility_buffer_hardware_raster_pipeline = + if let LightEntity::Directional { .. } = light_type { + visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline + } else { + visibility_buffer_hardware_raster_shadow_view_pipeline + }; + + render_context.command_encoder().push_debug_group(&format!( + "meshlet_visibility_buffer_raster: {}", + shadow_view.pass_name + )); + let time_span_shadow = diagnostics.time_span( + render_context.command_encoder(), + shadow_view.pass_name.clone(), + ); + clear_visibility_buffer_pass( + render_context, + &meshlet_view_bind_groups.clear_visibility_buffer, + clear_visibility_buffer_shadow_view_pipeline, + meshlet_view_resources.view_size, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + true, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_shadow_view_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + None, + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + false, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_shadow_view_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + None, + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + resolve_depth( + render_context, + shadow_view.depth_attachment.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + resolve_depth_shadow_view_pipeline, + camera, + ); + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + ); + render_context.command_encoder().pop_debug_group(); + time_span_shadow.end(render_context.command_encoder()); + } + + time_span.end(render_context.command_encoder()); + + Ok(()) + } +} + +// TODO: Replace this with vkCmdClearColorImage once wgpu supports it +fn clear_visibility_buffer_pass( + render_context: &mut RenderContext, + clear_visibility_buffer_bind_group: &BindGroup, + clear_visibility_buffer_pipeline: &ComputePipeline, + view_size: UVec2, +) { + let command_encoder = render_context.command_encoder(); + let mut clear_visibility_buffer_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("clear_visibility_buffer"), + timestamp_writes: None, + }); + clear_visibility_buffer_pass.set_pipeline(clear_visibility_buffer_pipeline); + clear_visibility_buffer_pass.set_push_constants(0, bytemuck::bytes_of(&view_size)); + clear_visibility_buffer_pass.set_bind_group(0, clear_visibility_buffer_bind_group, &[]); + clear_visibility_buffer_pass.dispatch_workgroups( + view_size.x.div_ceil(16), + view_size.y.div_ceil(16), + 1, + ); +} + +fn first_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + first_instance_cull_pipeline: &ComputePipeline, + first_bvh_cull_pipeline: &ComputePipeline, + first_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + let workgroups = meshlet_view_resources.scene_instance_count.div_ceil(128); + cull_pass( + "meshlet_first_instance_cull", + render_context, + &meshlet_view_bind_groups.first_instance_cull, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups(workgroups, 1, 1); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_first_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.first_bvh_cull_ping + } else { + &meshlet_view_bind_groups.first_bvh_cull_pong + }, + view_offset, + previous_view_offset, + first_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_count_front + } else { + &meshlet_view_resources.first_bvh_cull_count_back + }, + 0, + Some(4), + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + Some(4), + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_first_meshlet_cull", + render_context, + &meshlet_view_bind_groups.first_meshlet_cull, + view_offset, + previous_view_offset, + first_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.front_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn second_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + second_instance_cull_pipeline: &ComputePipeline, + second_bvh_cull_pipeline: &ComputePipeline, + second_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + cull_pass( + "meshlet_second_instance_cull", + render_context, + &meshlet_view_bind_groups.second_instance_cull, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups_indirect(&meshlet_view_resources.second_pass_dispatch, 0); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_second_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.second_bvh_cull_ping + } else { + &meshlet_view_bind_groups.second_bvh_cull_pong + }, + view_offset, + previous_view_offset, + second_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.second_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.second_bvh_cull_dispatch_back + }, + 0, + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_second_meshlet_cull", + render_context, + &meshlet_view_bind_groups.second_meshlet_cull, + view_offset, + previous_view_offset, + second_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.back_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn cull_pass<'a>( + label: &'static str, + render_context: &'a mut RenderContext, + bind_group: &'a BindGroup, + view_offset: &'a ViewUniformOffset, + previous_view_offset: &'a PreviousViewUniformOffset, + pipeline: &'a ComputePipeline, + push_constants: &[u32], +) -> ComputePass<'a> { + let command_encoder = render_context.command_encoder(); + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group( + 0, + bind_group, + &[view_offset.offset, previous_view_offset.offset], + ); + pass.set_push_constants(0, bytemuck::cast_slice(push_constants)); + pass +} + +fn remap_1d_to_2d( + mut pass: ComputePass, + pipeline: Option<&ComputePipeline>, + bind_group: Option<&BindGroup>, +) { + if let (Some(pipeline), Some(bind_group)) = (pipeline, bind_group) { + pass.set_pipeline(pipeline); + pass.set_bind_group(0, bind_group, &[]); + pass.dispatch_workgroups(1, 1, 1); + } +} + +fn raster_pass( + first_pass: bool, + render_context: &mut RenderContext, + visibility_buffer_software_raster_indirect_args: &Buffer, + visibility_buffer_hardware_raster_indirect_args: &Buffer, + dummy_render_target: &TextureView, + meshlet_view_bind_groups: &MeshletViewBindGroups, + view_offset: &ViewUniformOffset, + visibility_buffer_software_raster_pipeline: &ComputePipeline, + visibility_buffer_hardware_raster_pipeline: &RenderPipeline, + fill_counts_pipeline: &ComputePipeline, + camera: Option<&ExtractedCamera>, + raster_cluster_rightmost_slot: u32, +) { + let mut software_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(if first_pass { + "raster_software_first" + } else { + "raster_software_second" + }), + timestamp_writes: None, + }); + software_pass.set_pipeline(visibility_buffer_software_raster_pipeline); + software_pass.set_bind_group( + 0, + &meshlet_view_bind_groups.visibility_buffer_raster, + &[view_offset.offset], + ); + software_pass.dispatch_workgroups_indirect(visibility_buffer_software_raster_indirect_args, 0); + drop(software_pass); + + let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some(if first_pass { + "raster_hardware_first" + } else { + "raster_hardware_second" + }), + color_attachments: &[Some(RenderPassColorAttachment { + view: dummy_render_target, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(LinearRgba::BLACK.into()), + store: StoreOp::Discard, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.and_then(|camera| camera.viewport.as_ref()) { + hardware_pass.set_camera_viewport(viewport); + } + hardware_pass.set_render_pipeline(visibility_buffer_hardware_raster_pipeline); + hardware_pass.set_push_constants( + ShaderStages::VERTEX, + 0, + &raster_cluster_rightmost_slot.to_le_bytes(), + ); + hardware_pass.set_bind_group( + 0, + &meshlet_view_bind_groups.visibility_buffer_raster, + &[view_offset.offset], + ); + hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0); + drop(hardware_pass); + + let mut fill_counts_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("fill_counts"), + timestamp_writes: None, + }); + fill_counts_pass.set_pipeline(fill_counts_pipeline); + fill_counts_pass.set_bind_group(0, &meshlet_view_bind_groups.fill_counts, &[]); + fill_counts_pass.dispatch_workgroups(1, 1, 1); +} + +fn resolve_depth( + render_context: &mut RenderContext, + depth_stencil_attachment: RenderPassDepthStencilAttachment, + meshlet_view_bind_groups: &MeshletViewBindGroups, + resolve_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_depth"), + color_attachments: &[], + depth_stencil_attachment: Some(depth_stencil_attachment), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + resolve_pass.set_camera_viewport(viewport); + } + resolve_pass.set_render_pipeline(resolve_depth_pipeline); + resolve_pass.set_bind_group(0, &meshlet_view_bind_groups.resolve_depth, &[]); + resolve_pass.draw(0..3, 0..1); +} + +fn resolve_material_depth( + render_context: &mut RenderContext, + meshlet_view_resources: &MeshletViewResources, + meshlet_view_bind_groups: &MeshletViewBindGroups, + resolve_material_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + if let (Some(material_depth), Some(resolve_material_depth_bind_group)) = ( + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.resolve_material_depth.as_ref(), + ) { + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_material_depth"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + resolve_pass.set_camera_viewport(viewport); + } + resolve_pass.set_render_pipeline(resolve_material_depth_pipeline); + resolve_pass.set_bind_group(0, resolve_material_depth_bind_group, &[]); + resolve_pass.draw(0..3, 0..1); + } +} diff --git a/vendor/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/vendor/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl new file mode 100644 index 00000000..8d8a22b9 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -0,0 +1,240 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +#import bevy_pbr::{ + meshlet_bindings::{ + Meshlet, + meshlet_visibility_buffer, + meshlet_raster_clusters, + meshlets, + meshlet_instance_uniforms, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + get_meshlet_vertex_normal, + get_meshlet_vertex_uv, + }, + mesh_view_bindings::view, + mesh_functions::mesh_position_local_to_world, + mesh_types::Mesh, + view_transformations::{position_world_to_clip, frag_coord_to_ndc}, +} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS +#import bevy_pbr::{ + prepass_bindings::previous_view_uniforms, + pbr_prepass_functions::calculate_motion_vector, +} +#endif +#endif + +/// Functions to be used by materials for reading from a meshlet visibility buffer texture. + +#ifdef MESHLET_MESH_MATERIAL_PASS +struct PartialDerivatives { + barycentrics: vec3, + ddx: vec3, + ddy: vec3, +} + +// https://github.com/ConfettiFX/The-Forge/blob/9d43e69141a9cd0ce2ce2d2db5122234d3a2d5b5/Common_3/Renderer/VisibilityBuffer2/Shaders/FSL/vb_shading_utilities.h.fsl#L90-L150 +fn compute_partial_derivatives(vertex_world_positions: array, 3>, ndc_uv: vec2, half_screen_size: vec2) -> PartialDerivatives { + var result: PartialDerivatives; + + let vertex_clip_position_0 = position_world_to_clip(vertex_world_positions[0].xyz); + let vertex_clip_position_1 = position_world_to_clip(vertex_world_positions[1].xyz); + let vertex_clip_position_2 = position_world_to_clip(vertex_world_positions[2].xyz); + + let inv_w = 1.0 / vec3(vertex_clip_position_0.w, vertex_clip_position_1.w, vertex_clip_position_2.w); + let ndc_0 = vertex_clip_position_0.xy * inv_w[0]; + let ndc_1 = vertex_clip_position_1.xy * inv_w[1]; + let ndc_2 = vertex_clip_position_2.xy * inv_w[2]; + + let inv_det = 1.0 / determinant(mat2x2(ndc_2 - ndc_1, ndc_0 - ndc_1)); + result.ddx = vec3(ndc_1.y - ndc_2.y, ndc_2.y - ndc_0.y, ndc_0.y - ndc_1.y) * inv_det * inv_w; + result.ddy = vec3(ndc_2.x - ndc_1.x, ndc_0.x - ndc_2.x, ndc_1.x - ndc_0.x) * inv_det * inv_w; + + var ddx_sum = dot(result.ddx, vec3(1.0)); + var ddy_sum = dot(result.ddy, vec3(1.0)); + + let delta_v = ndc_uv - ndc_0; + let interp_inv_w = inv_w.x + delta_v.x * ddx_sum + delta_v.y * ddy_sum; + let interp_w = 1.0 / interp_inv_w; + + result.barycentrics = vec3( + interp_w * (inv_w[0] + delta_v.x * result.ddx.x + delta_v.y * result.ddy.x), + interp_w * (delta_v.x * result.ddx.y + delta_v.y * result.ddy.y), + interp_w * (delta_v.x * result.ddx.z + delta_v.y * result.ddy.z), + ); + + result.ddx *= half_screen_size.x; + result.ddy *= half_screen_size.y; + ddx_sum *= half_screen_size.x; + ddy_sum *= half_screen_size.y; + + result.ddy *= -1.0; + ddy_sum *= -1.0; + + let interp_ddx_w = 1.0 / (interp_inv_w + ddx_sum); + let interp_ddy_w = 1.0 / (interp_inv_w + ddy_sum); + + result.ddx = interp_ddx_w * (result.barycentrics * interp_inv_w + result.ddx) - result.barycentrics; + result.ddy = interp_ddy_w * (result.barycentrics * interp_inv_w + result.ddy) - result.barycentrics; + return result; +} + +struct VertexOutput { + position: vec4, + world_position: vec4, + world_normal: vec3, + uv: vec2, + ddx_uv: vec2, + ddy_uv: vec2, + world_tangent: vec4, + mesh_flags: u32, + cluster_id: u32, + material_bind_group_slot: u32, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector: vec2, +#endif +#endif +} + +/// Load the visibility buffer texture and resolve it into a VertexOutput. +fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { + let packed_ids = u32(textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy)).r); + let cluster_id = packed_ids >> 7u; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + let meshlet_id = instanced_offset.offset; + var meshlet = meshlets[meshlet_id]; + + let triangle_id = extractBits(packed_ids, 0u, 7u); + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); + let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2])); + let vertex_0 = load_vertex(&meshlet, vertex_ids[0]); + let vertex_1 = load_vertex(&meshlet, vertex_ids[1]); + let vertex_2 = load_vertex(&meshlet, vertex_ids[2]); + + let instance_id = instanced_offset.instance_id; + var instance_uniform = meshlet_instance_uniforms[instance_id]; + + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + let world_position_0 = mesh_position_local_to_world(world_from_local, vec4(vertex_0.position, 1.0)); + let world_position_1 = mesh_position_local_to_world(world_from_local, vec4(vertex_1.position, 1.0)); + let world_position_2 = mesh_position_local_to_world(world_from_local, vec4(vertex_2.position, 1.0)); + + let frag_coord_ndc = frag_coord_to_ndc(frag_coord).xy; + let partial_derivatives = compute_partial_derivatives( + array(world_position_0, world_position_1, world_position_2), + frag_coord_ndc, + view.viewport.zw / 2.0, + ); + + let world_position = mat3x4(world_position_0, world_position_1, world_position_2) * partial_derivatives.barycentrics; + let world_positions_camera_relative = mat3x3( + world_position_0.xyz - view.world_position, + world_position_1.xyz - view.world_position, + world_position_2.xyz - view.world_position, + ); + let ddx_world_position = world_positions_camera_relative * partial_derivatives.ddx; + let ddy_world_position = world_positions_camera_relative * partial_derivatives.ddy; + + let world_normal = mat3x3( + normal_local_to_world(vertex_0.normal, &instance_uniform), + normal_local_to_world(vertex_1.normal, &instance_uniform), + normal_local_to_world(vertex_2.normal, &instance_uniform), + ) * partial_derivatives.barycentrics; + + let uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.barycentrics; + let ddx_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddx; + let ddy_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddy; + + let world_tangent = calculate_world_tangent(world_normal, ddx_world_position, ddy_world_position, ddx_uv, ddy_uv); + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local); + let previous_world_position_0 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_0.position, 1.0)); + let previous_world_position_1 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_1.position, 1.0)); + let previous_world_position_2 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_2.position, 1.0)); + let previous_world_position = mat3x4(previous_world_position_0, previous_world_position_1, previous_world_position_2) * partial_derivatives.barycentrics; + let motion_vector = calculate_motion_vector(world_position, previous_world_position); +#endif +#endif + + return VertexOutput( + frag_coord, + world_position, + world_normal, + uv, + ddx_uv, + ddy_uv, + world_tangent, + instance_uniform.flags, + instance_id ^ meshlet_id, + instance_uniform.material_and_lightmap_bind_group_slot & 0xffffu, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector, +#endif +#endif + ); +} + +struct MeshletVertex { + position: vec3, + normal: vec3, + uv: vec2, +} + +fn load_vertex(meshlet: ptr, vertex_id: u32) -> MeshletVertex { + return MeshletVertex( + get_meshlet_vertex_position(meshlet, vertex_id), + get_meshlet_vertex_normal(meshlet, vertex_id), + get_meshlet_vertex_uv(meshlet, vertex_id), + ); +} + +fn normal_local_to_world(vertex_normal: vec3, instance_uniform: ptr) -> vec3 { + if any(vertex_normal != vec3(0.0)) { + return normalize( + mat2x4_f32_to_mat3x3_unpack( + (*instance_uniform).local_from_world_transpose_a, + (*instance_uniform).local_from_world_transpose_b, + ) * vertex_normal + ); + } else { + return vertex_normal; + } +} + +// https://www.jeremyong.com/graphics/2023/12/16/surface-gradient-bump-mapping/#surface-gradient-from-a-tangent-space-normal-vector-without-an-explicit-tangent-basis +fn calculate_world_tangent( + world_normal: vec3, + ddx_world_position: vec3, + ddy_world_position: vec3, + ddx_uv: vec2, + ddy_uv: vec2, +) -> vec4 { + // Project the position gradients onto the tangent plane + let ddx_world_position_s = ddx_world_position - dot(ddx_world_position, world_normal) * world_normal; + let ddy_world_position_s = ddy_world_position - dot(ddy_world_position, world_normal) * world_normal; + + // Compute the jacobian matrix to leverage the chain rule + let jacobian_sign = sign(ddx_uv.x * ddy_uv.y - ddx_uv.y * ddy_uv.x); + + var world_tangent = jacobian_sign * (ddy_uv.y * ddx_world_position_s - ddx_uv.y * ddy_world_position_s); + + // The sign intrinsic returns 0 if the argument is 0 + if jacobian_sign != 0.0 { + world_tangent = normalize(world_tangent); + } + + // The second factor here ensures a consistent handedness between + // the tangent frame and surface basis w.r.t. screenspace. + let w = jacobian_sign * sign(dot(ddy_world_position, cross(world_normal, ddx_world_position))); + + return vec4(world_tangent, -w); // TODO: Unclear why we need to negate this to match mikktspace generated tangents +} +#endif diff --git a/vendor/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl b/vendor/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl new file mode 100644 index 00000000..13091e87 --- /dev/null +++ b/vendor/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl @@ -0,0 +1,189 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_cluster_meshlet_ids, + meshlets, + meshlet_cluster_instance_ids, + meshlet_instance_uniforms, + meshlet_raster_clusters, + meshlet_previous_raster_counts, + meshlet_software_raster_cluster_count, + meshlet_visibility_buffer, + view, + get_meshlet_vertex_count, + get_meshlet_triangle_count, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + }, + mesh_functions::mesh_position_local_to_world, + view_transformations::ndc_to_uv, +} +#import bevy_render::maths::affine3_to_square + +/// Compute shader for rasterizing small clusters into a visibility buffer. + +// TODO: Fixed-point math and top-left rule + +var viewport_vertices: array; + +@compute +@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1-2 vertices per thread, 1 triangle per thread, 1 cluster per workgroup +fn rasterize_cluster( + @builtin(workgroup_id) workgroup_id: vec3, + @builtin(local_invocation_index) local_invocation_index: u32, +#ifdef MESHLET_2D_DISPATCH + @builtin(num_workgroups) num_workgroups: vec3, +#endif +) { + var workgroup_id_1d = workgroup_id.x; + +#ifdef MESHLET_2D_DISPATCH + workgroup_id_1d += workgroup_id.y * num_workgroups.x; + if workgroup_id_1d >= meshlet_software_raster_cluster_count { return; } +#endif + + let cluster_id = workgroup_id_1d + meshlet_previous_raster_counts[0]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; + + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + + // Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet + for (var i = 0u; i <= 128u; i += 128u) { + let vertex_id = local_invocation_index + i; + if vertex_id < get_meshlet_vertex_count(&meshlet) { + let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); + + // Project vertex to viewport space + let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); + let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0); + let ndc_position = clip_position.xyz / clip_position.w; + let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw; + + // Write vertex to workgroup shared memory + viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z); + } + } + workgroupBarrier(); + + // Load 1 triangle's worth of vertex data per thread + let triangle_id = local_invocation_index; + if triangle_id >= get_meshlet_triangle_count(&meshlet) { return; } + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); + let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2])); + let vertex_0 = viewport_vertices[vertex_ids[2]]; + let vertex_1 = viewport_vertices[vertex_ids[1]]; + let vertex_2 = viewport_vertices[vertex_ids[0]]; + let packed_ids = (cluster_id << 7u) | triangle_id; + + // Backface culling + let triangle_double_area = edge_function(vertex_0.xy, vertex_1.xy, vertex_2.xy); + if triangle_double_area <= 0.0 { return; } + + // Setup triangle gradients + let w_x = vec3(vertex_1.y - vertex_2.y, vertex_2.y - vertex_0.y, vertex_0.y - vertex_1.y); + let w_y = vec3(vertex_2.x - vertex_1.x, vertex_0.x - vertex_2.x, vertex_1.x - vertex_0.x); + let vertices_z = vec3(vertex_0.z, vertex_1.z, vertex_2.z) / triangle_double_area; + let z_x = dot(vertices_z, w_x); + let z_y = dot(vertices_z, w_y); + + // Compute triangle bounding box + var min_x = floor(min3(vertex_0.x, vertex_1.x, vertex_2.x)); + var min_y = floor(min3(vertex_0.y, vertex_1.y, vertex_2.y)); + var max_x = ceil(max3(vertex_0.x, vertex_1.x, vertex_2.x)); + var max_y = ceil(max3(vertex_0.y, vertex_1.y, vertex_2.y)); + min_x = max(min_x, 0.0); + min_y = max(min_y, 0.0); + max_x = min(max_x, view.viewport.z - 1.0); + max_y = min(max_y, view.viewport.w - 1.0); + + // Setup initial triangle equations + let starting_pixel = vec2(min_x, min_y) + 0.5; + var w_row = vec3( + edge_function(vertex_1.xy, vertex_2.xy, starting_pixel), + edge_function(vertex_2.xy, vertex_0.xy, starting_pixel), + edge_function(vertex_0.xy, vertex_1.xy, starting_pixel), + ); + var z_row = dot(vertices_z, w_row); + + // Rasterize triangle + if subgroupAny(max_x - min_x > 4.0) { + // Scanline setup + let edge_012 = -w_x; + let open_edge = edge_012 < vec3(0.0); + let inverse_edge_012 = select(1.0 / edge_012, vec3(1e8), edge_012 == vec3(0.0)); + let max_x_diff = vec3(max_x - min_x); + for (var y = min_y; y <= max_y; y += 1.0) { + // Calculate start and end X interval for pixels in this row within the triangle + let cross_x = w_row * inverse_edge_012; + let min_x2 = select(vec3(0.0), cross_x, open_edge); + let max_x2 = select(cross_x, max_x_diff, open_edge); + var x0 = ceil(max3(min_x2[0], min_x2[1], min_x2[2])); + var x1 = min3(max_x2[0], max_x2[1], max_x2[2]); + + var w = w_row + w_x * x0; + var z = z_row + z_x * x0; + x0 += min_x; + x1 += min_x; + + // Iterate scanline X interval + for (var x = x0; x <= x1; x += 1.0) { + // Check if point at pixel is within triangle (TODO: this shouldn't be needed, but there's bugs without it) + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(x, y, z, packed_ids); + } + + // Increment triangle equations along the X-axis + w += w_x; + z += z_x; + } + + // Increment triangle equations along the Y-axis + w_row += w_y; + z_row += z_y; + } + } else { + // Iterate over every pixel in the triangle's bounding box + for (var y = min_y; y <= max_y; y += 1.0) { + var w = w_row; + var z = z_row; + + for (var x = min_x; x <= max_x; x += 1.0) { + // Check if point at pixel is within triangle + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(x, y, z, packed_ids); + } + + // Increment triangle equations along the X-axis + w += w_x; + z += z_x; + } + + // Increment triangle equations along the Y-axis + w_row += w_y; + z_row += z_y; + } + } +} + +fn write_visibility_buffer_pixel(x: f32, y: f32, z: f32, packed_ids: u32) { + let depth = bitcast(z); +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let visibility = (u64(depth) << 32u) | u64(packed_ids); +#else + let visibility = depth; +#endif + textureAtomicMax(meshlet_visibility_buffer, vec2(u32(x), u32(y)), visibility); +} + +fn edge_function(a: vec2, b: vec2, c: vec2) -> f32 { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); +} + +fn min3(a: f32, b: f32, c: f32) -> f32 { + return min(a, min(b, c)); +} + +fn max3(a: f32, b: f32, c: f32) -> f32 { + return max(a, max(b, c)); +} diff --git a/vendor/bevy_pbr/src/parallax.rs b/vendor/bevy_pbr/src/parallax.rs new file mode 100644 index 00000000..be588ca8 --- /dev/null +++ b/vendor/bevy_pbr/src/parallax.rs @@ -0,0 +1,47 @@ +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +/// The [parallax mapping] method to use to compute depth based on the +/// material's [`depth_map`]. +/// +/// Parallax Mapping uses a depth map texture to give the illusion of depth +/// variation on a mesh surface that is geometrically flat. +/// +/// See the `parallax_mapping.wgsl` shader code for implementation details +/// and explanation of the methods used. +/// +/// [`depth_map`]: crate::StandardMaterial::depth_map +/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect)] +#[reflect(Default, Clone, PartialEq)] +pub enum ParallaxMappingMethod { + /// A simple linear interpolation, using a single texture sample. + /// + /// This method is named "Parallax Occlusion Mapping". + /// + /// Unlike [`ParallaxMappingMethod::Relief`], only requires a single lookup, + /// but may skip small details and result in writhing material artifacts. + #[default] + Occlusion, + /// Discovers the best depth value based on binary search. + /// + /// Each iteration incurs a texture sample. + /// The result has fewer visual artifacts than [`ParallaxMappingMethod::Occlusion`]. + /// + /// This method is named "Relief Mapping". + Relief { + /// How many additional steps to use at most to find the depth value. + max_steps: u32, + }, +} + +impl ParallaxMappingMethod { + /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. + pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; + + pub(crate) fn max_steps(&self) -> u32 { + match self { + ParallaxMappingMethod::Occlusion => 0, + ParallaxMappingMethod::Relief { max_steps } => *max_steps, + } + } +} diff --git a/vendor/bevy_pbr/src/pbr_material.rs b/vendor/bevy_pbr/src/pbr_material.rs new file mode 100644 index 00000000..c1d08acc --- /dev/null +++ b/vendor/bevy_pbr/src/pbr_material.rs @@ -0,0 +1,1567 @@ +use bevy_asset::Asset; +use bevy_color::{Alpha, ColorToComponents}; +use bevy_math::{Affine2, Affine3, Mat2, Mat3, Vec2, Vec3, Vec4}; +use bevy_mesh::MeshVertexBufferLayoutRef; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; +use bitflags::bitflags; + +use crate::{deferred::DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, *}; + +/// An enum to define which UV attribute to use for a texture. +/// +/// It is used for every texture in the [`StandardMaterial`]. +/// It only supports two UV attributes, [`bevy_mesh::Mesh::ATTRIBUTE_UV_0`] and +/// [`bevy_mesh::Mesh::ATTRIBUTE_UV_1`]. +/// The default is [`UvChannel::Uv0`]. +#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq)] +#[reflect(Default, Debug, Clone, PartialEq)] +pub enum UvChannel { + #[default] + Uv0, + Uv1, +} + +/// A material with "standard" properties used in PBR lighting. +/// Standard property values with pictures here: +/// . +/// +/// May be created directly from a [`Color`] or an [`Image`]. +#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] +#[bind_group_data(StandardMaterialKey)] +#[data(0, StandardMaterialUniform, binding_array(10))] +#[bindless(index_table(range(0..31)))] +#[reflect(Default, Debug, Clone)] +pub struct StandardMaterial { + /// The color of the surface of the material before lighting. + /// + /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything + /// in between. If used together with a `base_color_texture`, this is factored into the final + /// base color as `base_color * base_color_texture_value`. + /// + /// Defaults to [`Color::WHITE`]. + pub base_color: Color, + + /// The UV channel to use for the [`StandardMaterial::base_color_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub base_color_channel: UvChannel, + + /// The texture component of the material's color before lighting. + /// The actual pre-lighting color is `base_color * this_texture`. + /// + /// See [`base_color`] for details. + /// + /// You should set `base_color` to [`Color::WHITE`] (the default) + /// if you want the texture to show as-is. + /// + /// Setting `base_color` to something else than white will tint + /// the texture. For example, setting `base_color` to pure red will + /// tint the texture red. + /// + /// [`base_color`]: StandardMaterial::base_color + #[texture(1)] + #[sampler(2)] + #[dependency] + pub base_color_texture: Option>, + + // Use a color for user friendliness even though we technically don't use the alpha channel + // Might be used in the future for exposure correction in HDR + /// Color the material "emits" to the camera. + /// + /// This is typically used for monitor screens or LED lights. + /// Anything that can be visible even in darkness. + /// + /// The emissive color is added to what would otherwise be the material's visible color. + /// This means that for a light emissive value, in darkness, + /// you will mostly see the emissive component. + /// + /// The default emissive color is [`LinearRgba::BLACK`], which doesn't add anything to the material color. + /// + /// Emissive strength is controlled by the value of the color channels, + /// while the hue is controlled by their relative values. + /// + /// As a result, channel values for `emissive` + /// colors can exceed `1.0`. For instance, a `base_color` of + /// `LinearRgba::rgb(1.0, 0.0, 0.0)` represents the brightest + /// red for objects that reflect light, but an emissive color + /// like `LinearRgba::rgb(1000.0, 0.0, 0.0)` can be used to create + /// intensely bright red emissive effects. + /// + /// This results in a final luminance value when multiplied + /// by the value of the greyscale emissive texture (which ranges from 0 for black to 1 for white). + /// Luminance is a measure of the amount of light emitted per unit area, + /// and can be thought of as the "brightness" of the effect. + /// In Bevy, we treat these luminance values as the physical units of cd/m², aka nits. + /// + /// Increasing the emissive strength of the color will impact visual effects + /// like bloom, but it's important to note that **an emissive material won't + /// typically light up surrounding areas like a light source**, + /// it just adds a value to the color seen on screen. + pub emissive: LinearRgba, + + /// The weight in which the camera exposure influences the emissive color. + /// A value of `0.0` means the emissive color is not affected by the camera exposure. + /// In opposition, a value of `1.0` means the emissive color is multiplied by the camera exposure. + /// + /// Defaults to `0.0` + pub emissive_exposure_weight: f32, + + /// The UV channel to use for the [`StandardMaterial::emissive_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub emissive_channel: UvChannel, + + /// The emissive map, multiplies pixels with [`emissive`] + /// to get the final "emitting" color of a surface. + /// + /// This color is multiplied by [`emissive`] to get the final emitted color. + /// Meaning that you should set [`emissive`] to [`Color::WHITE`] + /// if you want to use the full range of color of the emissive texture. + /// + /// [`emissive`]: StandardMaterial::emissive + #[texture(3)] + #[sampler(4)] + #[dependency] + pub emissive_texture: Option>, + + /// Linear perceptual roughness, clamped to `[0.089, 1.0]` in the shader. + /// + /// Defaults to `0.5`. + /// + /// Low values result in a "glossy" material with specular highlights, + /// while values close to `1` result in rough materials. + /// + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `roughness * roughness_texture_value`. + /// + /// 0.089 is the minimum floating point value that won't be rounded down to 0 in the + /// calculations used. + // Technically for 32-bit floats, 0.045 could be used. + // See + pub perceptual_roughness: f32, + + /// How "metallic" the material appears, within `[0.0, 1.0]`. + /// + /// This should be set to 0.0 for dielectric materials or 1.0 for metallic materials. + /// For a hybrid surface such as corroded metal, you may need to use in-between values. + /// + /// Defaults to `0.00`, for dielectric. + /// + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `metallic * metallic_texture_value`. + pub metallic: f32, + + /// The UV channel to use for the [`StandardMaterial::metallic_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub metallic_roughness_channel: UvChannel, + + /// Metallic and roughness maps, stored as a single texture. + /// + /// The blue channel contains metallic values, + /// and the green channel contains the roughness values. + /// Other channels are unused. + /// + /// Those values are multiplied by the scalar ones of the material, + /// see [`metallic`] and [`perceptual_roughness`] for details. + /// + /// Note that with the default values of [`metallic`] and [`perceptual_roughness`], + /// setting this texture has no effect. If you want to exclusively use the + /// `metallic_roughness_texture` values for your material, make sure to set [`metallic`] + /// and [`perceptual_roughness`] to `1.0`. + /// + /// [`metallic`]: StandardMaterial::metallic + /// [`perceptual_roughness`]: StandardMaterial::perceptual_roughness + #[texture(5)] + #[sampler(6)] + #[dependency] + pub metallic_roughness_texture: Option>, + + /// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`. + /// + /// Use the value as a way to control the intensity of the + /// specular highlight of the material, i.e. how reflective is the material, + /// rather than the physical property "reflectance." + /// + /// Set to `0.0`, no specular highlight is visible, the highlight is strongest + /// when `reflectance` is set to `1.0`. + /// + /// Defaults to `0.5` which is mapped to 4% reflectance in the shader. + #[doc(alias = "specular_intensity")] + pub reflectance: f32, + + /// A color with which to modulate the [`StandardMaterial::reflectance`] for + /// non-metals. + /// + /// The specular highlights and reflection are tinted with this color. Note + /// that it has no effect for non-metals. + /// + /// This feature is currently unsupported in the deferred rendering path, in + /// order to reduce the size of the geometry buffers. + /// + /// Defaults to [`Color::WHITE`]. + #[doc(alias = "specular_color")] + pub specular_tint: Color, + + /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”). + /// + /// Implemented as a second, flipped [Lambertian diffuse](https://en.wikipedia.org/wiki/Lambertian_reflectance) lobe, + /// which provides an inexpensive but plausible approximation of translucency for thin dielectric objects (e.g. paper, + /// leaves, some fabrics) or thicker volumetric materials with short scattering distances (e.g. porcelain, wax). + /// + /// For specular transmission usecases with refraction (e.g. glass) use the [`StandardMaterial::specular_transmission`] and + /// [`StandardMaterial::ior`] properties instead. + /// + /// - When set to `0.0` (the default) no diffuse light is transmitted; + /// - When set to `1.0` all diffuse light is transmitted through the material; + /// - Values higher than `0.5` will cause more diffuse light to be transmitted than reflected, resulting in a “darker” + /// appearance on the side facing the light than the opposite side. (e.g. plant leaves) + /// + /// ## Notes + /// + /// - The material's [`StandardMaterial::base_color`] also modulates the transmitted light; + /// - To receive transmitted shadows on the diffuse transmission lobe (i.e. the “backside”) of the material, + /// use the [`TransmittedShadowReceiver`](bevy_light::TransmittedShadowReceiver) component. + #[doc(alias = "translucency")] + pub diffuse_transmission: f32, + + /// The UV channel to use for the [`StandardMaterial::diffuse_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub diffuse_transmission_channel: UvChannel, + + /// A map that modulates diffuse transmission via its alpha channel. Multiplied by [`StandardMaterial::diffuse_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::diffuse_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(19))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(20))] + #[cfg(feature = "pbr_transmission_textures")] + #[dependency] + pub diffuse_transmission_texture: Option>, + + /// The amount of light transmitted _specularly_ through the material (i.e. via refraction). + /// + /// - When set to `0.0` (the default) no light is transmitted. + /// - When set to `1.0` all light is transmitted through the material. + /// + /// The material's [`StandardMaterial::base_color`] also modulates the transmitted light. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::thickness`], [`StandardMaterial::ior`] and [`StandardMaterial::perceptual_roughness`]. + /// + /// ## Performance + /// + /// Specular transmission is implemented as a relatively expensive screen-space effect that allows occluded objects to be seen through the material, + /// with distortion and blur effects. + /// + /// - [`Camera3d::screen_space_specular_transmission_steps`](bevy_camera::Camera3d::screen_space_specular_transmission_steps) can be used to enable transmissive objects + /// to be seen through other transmissive objects, at the cost of additional draw calls and texture copies; (Use with caution!) + /// - If a simplified approximation of specular transmission using only environment map lighting is sufficient, consider setting + /// [`Camera3d::screen_space_specular_transmission_steps`](bevy_camera::Camera3d::screen_space_specular_transmission_steps) to `0`. + /// - If purely diffuse light transmission is needed, (i.e. “translucency”) consider using [`StandardMaterial::diffuse_transmission`] instead, + /// for a much less expensive effect. + /// - Specular transmission is rendered before alpha blending, so any material with [`AlphaMode::Blend`], [`AlphaMode::Premultiplied`], [`AlphaMode::Add`] or [`AlphaMode::Multiply`] + /// won't be visible through specular transmissive materials. + #[doc(alias = "refraction")] + pub specular_transmission: f32, + + /// The UV channel to use for the [`StandardMaterial::specular_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub specular_transmission_channel: UvChannel, + + /// A map that modulates specular transmission via its red channel. Multiplied by [`StandardMaterial::specular_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::specular_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(15))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(16))] + #[cfg(feature = "pbr_transmission_textures")] + #[dependency] + pub specular_transmission_texture: Option>, + + /// Thickness of the volume beneath the material surface. + /// + /// When set to `0.0` (the default) the material appears as an infinitely-thin film, + /// transmitting light without distorting it. + /// + /// When set to any other value, the material distorts light like a thick lens. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::ior`], or with + /// [`StandardMaterial::diffuse_transmission`]. + #[doc(alias = "volume")] + #[doc(alias = "thin_walled")] + pub thickness: f32, + + /// The UV channel to use for the [`StandardMaterial::thickness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub thickness_channel: UvChannel, + + /// A map that modulates thickness via its green channel. Multiplied by [`StandardMaterial::thickness`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::thickness`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(17))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(18))] + #[cfg(feature = "pbr_transmission_textures")] + #[dependency] + pub thickness_texture: Option>, + + /// The [index of refraction](https://en.wikipedia.org/wiki/Refractive_index) of the material. + /// + /// Defaults to 1.5. + /// + /// | Material | Index of Refraction | + /// |:----------------|:---------------------| + /// | Vacuum | 1 | + /// | Air | 1.00 | + /// | Ice | 1.31 | + /// | Water | 1.33 | + /// | Eyes | 1.38 | + /// | Quartz | 1.46 | + /// | Olive Oil | 1.47 | + /// | Honey | 1.49 | + /// | Acrylic | 1.49 | + /// | Window Glass | 1.52 | + /// | Polycarbonate | 1.58 | + /// | Flint Glass | 1.69 | + /// | Ruby | 1.71 | + /// | Glycerine | 1.74 | + /// | Sapphire | 1.77 | + /// | Cubic Zirconia | 2.15 | + /// | Diamond | 2.42 | + /// | Moissanite | 2.65 | + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::thickness`]. + #[doc(alias = "index_of_refraction")] + #[doc(alias = "refraction_index")] + #[doc(alias = "refractive_index")] + pub ior: f32, + + /// How far, on average, light travels through the volume beneath the material's + /// surface before being absorbed. + /// + /// Defaults to [`f32::INFINITY`], i.e. light is never absorbed. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_color`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_distance")] + #[doc(alias = "extinction_distance")] + pub attenuation_distance: f32, + + /// The resulting (non-absorbed) color after white light travels through the attenuation distance. + /// + /// Defaults to [`Color::WHITE`], i.e. no change. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_distance`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_color")] + #[doc(alias = "extinction_color")] + pub attenuation_color: Color, + + /// The UV channel to use for the [`StandardMaterial::normal_map_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub normal_map_channel: UvChannel, + + /// Used to fake the lighting of bumps and dents on a material. + /// + /// A typical usage would be faking cobblestones on a flat plane mesh in 3D. + /// + /// # Notes + /// + /// Normal mapping with `StandardMaterial` and the core bevy PBR shaders requires: + /// - A normal map texture + /// - Vertex UVs + /// - Vertex tangents + /// - Vertex normals + /// + /// Tangents do not have to be stored in your model, + /// they can be generated using the [`Mesh::generate_tangents`] or + /// [`Mesh::with_generated_tangents`] methods. + /// If your material has a normal map, but still renders as a flat surface, + /// make sure your meshes have their tangents set. + /// + /// [`Mesh::generate_tangents`]: bevy_mesh::Mesh::generate_tangents + /// [`Mesh::with_generated_tangents`]: bevy_mesh::Mesh::with_generated_tangents + /// + /// # Usage + /// + /// ``` + /// # use bevy_asset::{AssetServer, Handle}; + /// # use bevy_ecs::change_detection::Res; + /// # use bevy_image::{Image, ImageLoaderSettings}; + /// # + /// fn load_normal_map(asset_server: Res) { + /// let normal_handle: Handle = asset_server.load_with_settings( + /// "textures/parallax_example/cube_normal.png", + /// // The normal map texture is in linear color space. Lighting won't look correct + /// // if `is_srgb` is `true`, which is the default. + /// |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + /// ); + /// } + /// ``` + #[texture(9)] + #[sampler(10)] + #[dependency] + pub normal_map_texture: Option>, + + /// Normal map textures authored for DirectX have their y-component flipped. Set this to flip + /// it to right-handed conventions. + pub flip_normal_map_y: bool, + + /// The UV channel to use for the [`StandardMaterial::occlusion_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub occlusion_channel: UvChannel, + + /// Specifies the level of exposure to ambient light. + /// + /// This is usually generated and stored automatically ("baked") by 3D-modeling software. + /// + /// Typically, steep concave parts of a model (such as the armpit of a shirt) are darker, + /// because they have little exposure to light. + /// An occlusion map specifies those parts of the model that light doesn't reach well. + /// + /// The material will be less lit in places where this texture is dark. + /// This is similar to ambient occlusion, but built into the model. + #[texture(7)] + #[sampler(8)] + #[dependency] + pub occlusion_texture: Option>, + + /// The UV channel to use for the [`StandardMaterial::specular_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_channel: UvChannel, + + /// A map that specifies reflectance for non-metallic materials. + /// + /// Alpha values from [0.0, 1.0] in this texture are linearly mapped to + /// reflectance values of [0.0, 0.5] and multiplied by the constant + /// [`StandardMaterial::reflectance`] value. This follows the + /// `KHR_materials_specular` specification. The map will have no effect if + /// the material is fully metallic. + /// + /// When using this map, you may wish to set the + /// [`StandardMaterial::reflectance`] value to 2.0 so that this map can + /// express the full [0.0, 1.0] range of values. + /// + /// Note that, because the reflectance is stored in the alpha channel, and + /// the [`StandardMaterial::specular_tint_texture`] has no alpha value, it + /// may be desirable to pack the values together and supply the same + /// texture to both fields. + #[cfg_attr(feature = "pbr_specular_textures", texture(27))] + #[cfg_attr(feature = "pbr_specular_textures", sampler(28))] + #[cfg(feature = "pbr_specular_textures")] + #[dependency] + pub specular_texture: Option>, + + /// The UV channel to use for the + /// [`StandardMaterial::specular_tint_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_tint_channel: UvChannel, + + /// A map that specifies color adjustment to be applied to the specular + /// reflection for non-metallic materials. + /// + /// The RGB values of this texture modulate the + /// [`StandardMaterial::specular_tint`] value. See the documentation for + /// that field for more information. + /// + /// Like the fixed specular tint value, this texture map isn't supported in + /// the deferred renderer. + #[cfg_attr(feature = "pbr_specular_textures", texture(29))] + #[cfg_attr(feature = "pbr_specular_textures", sampler(30))] + #[cfg(feature = "pbr_specular_textures")] + #[dependency] + pub specular_tint_texture: Option>, + + /// An extra thin translucent layer on top of the main PBR layer. This is + /// typically used for painted surfaces. + /// + /// This value specifies the strength of the layer, which affects how + /// visible the clearcoat layer will be. + /// + /// Defaults to zero, specifying no clearcoat layer. + pub clearcoat: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_channel: UvChannel, + + /// An image texture that specifies the strength of the clearcoat layer in + /// the red channel. Values sampled from this texture are multiplied by the + /// main [`StandardMaterial::clearcoat`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(21))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(22))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + #[dependency] + pub clearcoat_texture: Option>, + + /// The roughness of the clearcoat material. This is specified in exactly + /// the same way as the [`StandardMaterial::perceptual_roughness`]. + /// + /// If the [`StandardMaterial::clearcoat`] value if zero, this has no + /// effect. + /// + /// Defaults to 0.5. + pub clearcoat_perceptual_roughness: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_channel: UvChannel, + + /// An image texture that specifies the roughness of the clearcoat level in + /// the green channel. Values from this texture are multiplied by the main + /// [`StandardMaterial::clearcoat_perceptual_roughness`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(23))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(24))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + #[dependency] + pub clearcoat_roughness_texture: Option>, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_normal_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_channel: UvChannel, + + /// An image texture that specifies a normal map that is to be applied to + /// the clearcoat layer. This can be used to simulate, for example, + /// scratches on an outer layer of varnish. Normal maps are in the same + /// format as [`StandardMaterial::normal_map_texture`]. + /// + /// Note that, if a clearcoat normal map isn't specified, the main normal + /// map, if any, won't be applied to the clearcoat. If you want a normal map + /// that applies to both the main material and to the clearcoat, specify it + /// in both [`StandardMaterial::normal_map_texture`] and this field. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(25))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(26))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + #[dependency] + pub clearcoat_normal_texture: Option>, + + /// Increases the roughness along a specific direction, so that the specular + /// highlight will be stretched instead of being a circular lobe. + /// + /// This value ranges from 0 (perfectly circular) to 1 (maximally + /// stretched). The default direction (corresponding to a + /// [`StandardMaterial::anisotropy_rotation`] of 0) aligns with the + /// *tangent* of the mesh; thus mesh tangents must be specified in order for + /// this parameter to have any meaning. The direction can be changed using + /// the [`StandardMaterial::anisotropy_rotation`] parameter. + /// + /// This is typically used for modeling surfaces such as brushed metal and + /// hair, in which one direction of the surface but not the other is smooth. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_strength: f32, + + /// The direction of increased roughness, in radians relative to the mesh + /// tangent. + /// + /// This parameter causes the roughness to vary according to the + /// [`StandardMaterial::anisotropy_strength`]. The rotation is applied in + /// tangent-bitangent space; thus, mesh tangents must be present for this + /// parameter to have any meaning. + /// + /// This parameter has no effect if + /// [`StandardMaterial::anisotropy_strength`] is zero. Its value can + /// optionally be adjusted across the mesh with the + /// [`StandardMaterial::anisotropy_texture`]. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_rotation: f32, + + /// The UV channel to use for the [`StandardMaterial::anisotropy_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_anisotropy_texture")] + pub anisotropy_channel: UvChannel, + + /// An image texture that allows the + /// [`StandardMaterial::anisotropy_strength`] and + /// [`StandardMaterial::anisotropy_rotation`] to vary across the mesh. + /// + /// The [`KHR_materials_anisotropy` specification] defines the format that + /// this texture must take. To summarize: the direction vector is encoded in + /// the red and green channels, while the strength is encoded in the blue + /// channels. For the direction vector, the red and green channels map the + /// color range [0, 1] to the vector range [-1, 1]. The direction vector + /// encoded in this texture modifies the default rotation direction in + /// tangent-bitangent space, before the + /// [`StandardMaterial::anisotropy_rotation`] parameter is applied. The + /// value in the blue channel is multiplied by the + /// [`StandardMaterial::anisotropy_strength`] value to produce the final + /// anisotropy strength. + /// + /// As the texel values don't represent colors, this texture must be in + /// linear color space, not sRGB. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + #[cfg_attr(feature = "pbr_anisotropy_texture", texture(13))] + #[cfg_attr(feature = "pbr_anisotropy_texture", sampler(14))] + #[cfg(feature = "pbr_anisotropy_texture")] + #[dependency] + pub anisotropy_texture: Option>, + + /// Support two-sided lighting by automatically flipping the normals for "back" faces + /// within the PBR lighting shader. + /// + /// Defaults to `false`. + /// This does not automatically configure backface culling, + /// which can be done via `cull_mode`. + pub double_sided: bool, + + /// Whether to cull the "front", "back" or neither side of a mesh. + /// If set to `None`, the two sides of the mesh are visible. + /// + /// Defaults to `Some(Face::Back)`. + /// In bevy, the order of declaration of a triangle's vertices + /// in [`Mesh`] defines the triangle's front face. + /// + /// When a triangle is in a viewport, + /// if its vertices appear counter-clockwise from the viewport's perspective, + /// then the viewport is seeing the triangle's front face. + /// Conversely, if the vertices appear clockwise, you are seeing the back face. + /// + /// In short, in bevy, front faces winds counter-clockwise. + /// + /// Your 3D editing software should manage all of that. + /// + /// [`Mesh`]: bevy_mesh::Mesh + // TODO: include this in reflection somehow (maybe via remote types like serde https://serde.rs/remote-derive.html) + #[reflect(ignore, clone)] + pub cull_mode: Option, + + /// Whether to apply only the base color to this material. + /// + /// Normals, occlusion textures, roughness, metallic, reflectance, emissive, + /// shadows, alpha mode and ambient light are ignored if this is set to `true`. + pub unlit: bool, + + /// Whether to enable fog for this material. + pub fog_enabled: bool, + + /// How to apply the alpha channel of the `base_color_texture`. + /// + /// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`]. + pub alpha_mode: AlphaMode, + + /// Adjust rendered depth. + /// + /// A material with a positive depth bias will render closer to the + /// camera while negative values cause the material to render behind + /// other objects. This is independent of the viewport. + /// + /// `depth_bias` affects render ordering and depth write operations + /// using the `wgpu::DepthBiasState::Constant` field. + /// + /// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting + pub depth_bias: f32, + + /// The depth map used for [parallax mapping]. + /// + /// It is a grayscale image where white represents bottom and black the top. + /// If this field is set, bevy will apply [parallax mapping]. + /// Parallax mapping, unlike simple normal maps, will move the texture + /// coordinate according to the current perspective, + /// giving actual depth to the texture. + /// + /// The visual result is similar to a displacement map, + /// but does not require additional geometry. + /// + /// Use the [`parallax_depth_scale`] field to control the depth of the parallax. + /// + /// ## Limitations + /// + /// - It will look weird on bent/non-planar surfaces. + /// - The depth of the pixel does not reflect its visual position, resulting + /// in artifacts for depth-dependent features such as fog or SSAO. + /// - For the same reason, the geometry silhouette will always be + /// the one of the actual geometry, not the parallaxed version, resulting + /// in awkward looks on intersecting parallaxed surfaces. + /// + /// ## Performance + /// + /// Parallax mapping requires multiple texture lookups, proportional to + /// [`max_parallax_layer_count`], which might be costly. + /// + /// Use the [`parallax_mapping_method`] and [`max_parallax_layer_count`] fields + /// to tweak the shader, trading graphical quality for performance. + /// + /// To improve performance, set your `depth_map`'s [`Image::sampler`] + /// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves + /// performance a bit. + /// + /// To reduce artifacts, avoid steep changes in depth, blurring the depth + /// map helps with this. + /// + /// Larger depth maps haves a disproportionate performance impact. + /// + /// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf + /// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping + /// [`parallax_depth_scale`]: StandardMaterial::parallax_depth_scale + /// [`parallax_mapping_method`]: StandardMaterial::parallax_mapping_method + /// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count + #[texture(11)] + #[sampler(12)] + #[dependency] + pub depth_map: Option>, + + /// How deep the offset introduced by the depth map should be. + /// + /// Default is `0.1`, anything over that value may look distorted. + /// Lower values lessen the effect. + /// + /// The depth is relative to texture size. This means that if your texture + /// occupies a surface of `1` world unit, and `parallax_depth_scale` is `0.1`, then + /// the in-world depth will be of `0.1` world units. + /// If the texture stretches for `10` world units, then the final depth + /// will be of `1` world unit. + pub parallax_depth_scale: f32, + + /// Which parallax mapping method to use. + /// + /// We recommend that all objects use the same [`ParallaxMappingMethod`], to avoid + /// duplicating and running two shaders. + pub parallax_mapping_method: ParallaxMappingMethod, + + /// In how many layers to split the depth maps for parallax mapping. + /// + /// If you are seeing jaggy edges, increase this value. + /// However, this incurs a performance cost. + /// + /// Dependent on the situation, switching to [`ParallaxMappingMethod::Relief`] + /// and keeping this value low might have better performance than increasing the + /// layer count while using [`ParallaxMappingMethod::Occlusion`]. + /// + /// Default is `16.0`. + pub max_parallax_layer_count: f32, + + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, + + /// Render method used for opaque materials. (Where `alpha_mode` is [`AlphaMode::Opaque`] or [`AlphaMode::Mask`]) + pub opaque_render_method: OpaqueRendererMethod, + + /// Used for selecting the deferred lighting pass for deferred materials. + /// Default is [`DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID`] for default + /// PBR deferred lighting pass. Ignored in the case of forward materials. + pub deferred_lighting_pass_id: u8, + + /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. + pub uv_transform: Affine2, +} + +impl StandardMaterial { + /// Horizontal flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with horizontally flipped texture coords + pub const FLIP_HORIZONTAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::new(-1.0, 0.0), Vec2::Y), + translation: Vec2::X, + }; + + /// Vertical flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with vertically flipped texture coords + pub const FLIP_VERTICAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::X, Vec2::new(0.0, -1.0)), + translation: Vec2::Y, + }; + + /// Flipping X 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped X coords + pub const FLIP_X: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::new(-1.0, 0.0, 0.0), Vec3::Y, Vec3::Z), + translation: Vec3::X, + }; + + /// Flipping Y 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Y coords + pub const FLIP_Y: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::new(0.0, -1.0, 0.0), Vec3::Z), + translation: Vec3::Y, + }; + + /// Flipping Z 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Z coords + pub const FLIP_Z: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::Y, Vec3::new(0.0, 0.0, -1.0)), + translation: Vec3::Z, + }; + + /// Flip the texture coordinates of the material. + pub fn flip(&mut self, horizontal: bool, vertical: bool) { + if horizontal { + // Multiplication of `Affine2` is order dependent, which is why + // we do not use the `*=` operator. + self.uv_transform = Self::FLIP_HORIZONTAL * self.uv_transform; + } + if vertical { + self.uv_transform = Self::FLIP_VERTICAL * self.uv_transform; + } + } + + /// Consumes the material and returns a material with flipped texture coordinates + pub fn flipped(mut self, horizontal: bool, vertical: bool) -> Self { + self.flip(horizontal, vertical); + self + } + + /// Creates a new material from a given color + pub fn from_color(color: impl Into) -> Self { + Self::from(color.into()) + } +} + +impl Default for StandardMaterial { + fn default() -> Self { + StandardMaterial { + // White because it gets multiplied with texture values if someone uses + // a texture. + base_color: Color::WHITE, + base_color_channel: UvChannel::Uv0, + base_color_texture: None, + emissive: LinearRgba::BLACK, + emissive_exposure_weight: 0.0, + emissive_channel: UvChannel::Uv0, + emissive_texture: None, + // Matches Blender's default roughness. + perceptual_roughness: 0.5, + // Metallic should generally be set to 0.0 or 1.0. + metallic: 0.0, + metallic_roughness_channel: UvChannel::Uv0, + metallic_roughness_texture: None, + // Minimum real-world reflectance is 2%, most materials between 2-5% + // Expressed in a linear scale and equivalent to 4% reflectance see + // + reflectance: 0.5, + diffuse_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_texture: None, + specular_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture: None, + thickness: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + thickness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture: None, + ior: 1.5, + attenuation_color: Color::WHITE, + attenuation_distance: f32::INFINITY, + occlusion_channel: UvChannel::Uv0, + occlusion_texture: None, + normal_map_channel: UvChannel::Uv0, + normal_map_texture: None, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: None, + specular_tint: Color::WHITE, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: None, + clearcoat: 0.0, + clearcoat_perceptual_roughness: 0.5, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: None, + anisotropy_strength: 0.0, + anisotropy_rotation: 0.0, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture: None, + flip_normal_map_y: false, + double_sided: false, + cull_mode: Some(Face::Back), + unlit: false, + fog_enabled: true, + alpha_mode: AlphaMode::Opaque, + depth_bias: 0.0, + depth_map: None, + parallax_depth_scale: 0.1, + max_parallax_layer_count: 16.0, + lightmap_exposure: 1.0, + parallax_mapping_method: ParallaxMappingMethod::Occlusion, + opaque_render_method: OpaqueRendererMethod::Auto, + deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, + uv_transform: Affine2::IDENTITY, + } + } +} + +impl From for StandardMaterial { + fn from(color: Color) -> Self { + StandardMaterial { + base_color: color, + alpha_mode: if color.alpha() < 1.0 { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, + ..Default::default() + } + } +} + +impl From> for StandardMaterial { + fn from(texture: Handle) -> Self { + StandardMaterial { + base_color_texture: Some(texture), + ..Default::default() + } + } +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/pbr_types.wgsl! +bitflags::bitflags! { + /// Bitflags info about the material a shader is currently rendering. + /// This is accessible in the shader in the [`StandardMaterialUniform`] + #[repr(transparent)] + pub struct StandardMaterialFlags: u32 { + const BASE_COLOR_TEXTURE = 1 << 0; + const EMISSIVE_TEXTURE = 1 << 1; + const METALLIC_ROUGHNESS_TEXTURE = 1 << 2; + const OCCLUSION_TEXTURE = 1 << 3; + const DOUBLE_SIDED = 1 << 4; + const UNLIT = 1 << 5; + const TWO_COMPONENT_NORMAL_MAP = 1 << 6; + const FLIP_NORMAL_MAP_Y = 1 << 7; + const FOG_ENABLED = 1 << 8; + const DEPTH_MAP = 1 << 9; // Used for parallax mapping + const SPECULAR_TRANSMISSION_TEXTURE = 1 << 10; + const THICKNESS_TEXTURE = 1 << 11; + const DIFFUSE_TRANSMISSION_TEXTURE = 1 << 12; + const ATTENUATION_ENABLED = 1 << 13; + const CLEARCOAT_TEXTURE = 1 << 14; + const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; + const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; + const ANISOTROPY_TEXTURE = 1 << 17; + const SPECULAR_TEXTURE = 1 << 18; + const SPECULAR_TINT_TEXTURE = 1 << 19; + const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` + const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into + const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. + const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_PREMULTIPLIED = 3 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_ADD = 4 << Self::ALPHA_MODE_SHIFT_BITS; // Right now only values 0–5 are used, which still gives + const ALPHA_MODE_MULTIPLY = 5 << Self::ALPHA_MODE_SHIFT_BITS; // ← us "room" for two more modes without adding more bits + const ALPHA_MODE_ALPHA_TO_COVERAGE = 6 << Self::ALPHA_MODE_SHIFT_BITS; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +impl StandardMaterialFlags { + const ALPHA_MODE_MASK_BITS: u32 = 0b111; + const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones(); +} + +/// The GPU representation of the uniform data of a [`StandardMaterial`]. +#[derive(Clone, Default, ShaderType)] +pub struct StandardMaterialUniform { + /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything + /// in between. + pub base_color: Vec4, + // Use a color for user-friendliness even though we technically don't use the alpha channel + // Might be used in the future for exposure correction in HDR + pub emissive: Vec4, + /// Color white light takes after traveling through the attenuation distance underneath the material surface + pub attenuation_color: Vec4, + /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. + pub uv_transform: Mat3, + /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] + /// defaults to 0.5 which is mapped to 4% reflectance in the shader + pub reflectance: Vec3, + /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader + /// Defaults to minimum of 0.089 + pub roughness: f32, + /// From [0.0, 1.0], dielectric to pure metallic + pub metallic: f32, + /// Amount of diffuse light transmitted through the material + pub diffuse_transmission: f32, + /// Amount of specular light transmitted through the material + pub specular_transmission: f32, + /// Thickness of the volume underneath the material surface + pub thickness: f32, + /// Index of Refraction + pub ior: f32, + /// How far light travels through the volume underneath the material surface before being absorbed + pub attenuation_distance: f32, + pub clearcoat: f32, + pub clearcoat_perceptual_roughness: f32, + pub anisotropy_strength: f32, + pub anisotropy_rotation: Vec2, + /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. + pub flags: u32, + /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, + /// and any below means fully transparent. + pub alpha_cutoff: f32, + /// The depth of the [`StandardMaterial::depth_map`] to apply. + pub parallax_depth_scale: f32, + /// In how many layers to split the depth maps for Steep parallax mapping. + /// + /// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges, + /// increase this value. However, this incurs a performance cost. + pub max_parallax_layer_count: f32, + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, + /// Using [`ParallaxMappingMethod::Relief`], how many additional + /// steps to use at most to find the depth value. + pub max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + pub deferred_lighting_pass_id: u32, +} + +impl AsBindGroupShaderType for StandardMaterial { + fn as_bind_group_shader_type( + &self, + images: &RenderAssets, + ) -> StandardMaterialUniform { + let mut flags = StandardMaterialFlags::NONE; + if self.base_color_texture.is_some() { + flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE; + } + if self.emissive_texture.is_some() { + flags |= StandardMaterialFlags::EMISSIVE_TEXTURE; + } + if self.metallic_roughness_texture.is_some() { + flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE; + } + if self.occlusion_texture.is_some() { + flags |= StandardMaterialFlags::OCCLUSION_TEXTURE; + } + if self.double_sided { + flags |= StandardMaterialFlags::DOUBLE_SIDED; + } + if self.unlit { + flags |= StandardMaterialFlags::UNLIT; + } + if self.fog_enabled { + flags |= StandardMaterialFlags::FOG_ENABLED; + } + if self.depth_map.is_some() { + flags |= StandardMaterialFlags::DEPTH_MAP; + } + #[cfg(feature = "pbr_transmission_textures")] + { + if self.specular_transmission_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TRANSMISSION_TEXTURE; + } + if self.thickness_texture.is_some() { + flags |= StandardMaterialFlags::THICKNESS_TEXTURE; + } + if self.diffuse_transmission_texture.is_some() { + flags |= StandardMaterialFlags::DIFFUSE_TRANSMISSION_TEXTURE; + } + } + + #[cfg(feature = "pbr_anisotropy_texture")] + { + if self.anisotropy_texture.is_some() { + flags |= StandardMaterialFlags::ANISOTROPY_TEXTURE; + } + } + + #[cfg(feature = "pbr_specular_textures")] + { + if self.specular_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TEXTURE; + } + if self.specular_tint_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TINT_TEXTURE; + } + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + if self.clearcoat_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_TEXTURE; + } + if self.clearcoat_roughness_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_ROUGHNESS_TEXTURE; + } + if self.clearcoat_normal_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_NORMAL_TEXTURE; + } + } + + let has_normal_map = self.normal_map_texture.is_some(); + if has_normal_map { + let normal_map_id = self.normal_map_texture.as_ref().map(Handle::id).unwrap(); + if let Some(texture) = images.get(normal_map_id) { + match texture.texture_format { + // All 2-component unorm formats + TextureFormat::Rg8Unorm + | TextureFormat::Rg16Unorm + | TextureFormat::Bc5RgUnorm + | TextureFormat::EacRg11Unorm => { + flags |= StandardMaterialFlags::TWO_COMPONENT_NORMAL_MAP; + } + _ => {} + } + } + if self.flip_normal_map_y { + flags |= StandardMaterialFlags::FLIP_NORMAL_MAP_Y; + } + } + // NOTE: 0.5 is from the glTF default - do we want this? + let mut alpha_cutoff = 0.5; + match self.alpha_mode { + AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE, + AlphaMode::Mask(c) => { + alpha_cutoff = c; + flags |= StandardMaterialFlags::ALPHA_MODE_MASK; + } + AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, + AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED, + AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD, + AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, + AlphaMode::AlphaToCoverage => { + flags |= StandardMaterialFlags::ALPHA_MODE_ALPHA_TO_COVERAGE; + } + }; + + if self.attenuation_distance.is_finite() { + flags |= StandardMaterialFlags::ATTENUATION_ENABLED; + } + + let mut emissive = self.emissive.to_vec4(); + emissive[3] = self.emissive_exposure_weight; + + // Doing this up front saves having to do this repeatedly in the fragment shader. + let anisotropy_rotation = Vec2::from_angle(self.anisotropy_rotation); + + StandardMaterialUniform { + base_color: LinearRgba::from(self.base_color).to_vec4(), + emissive, + roughness: self.perceptual_roughness, + metallic: self.metallic, + reflectance: LinearRgba::from(self.specular_tint).to_vec3() * self.reflectance, + clearcoat: self.clearcoat, + clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, + anisotropy_strength: self.anisotropy_strength, + anisotropy_rotation, + diffuse_transmission: self.diffuse_transmission, + specular_transmission: self.specular_transmission, + thickness: self.thickness, + ior: self.ior, + attenuation_distance: self.attenuation_distance, + attenuation_color: LinearRgba::from(self.attenuation_color) + .to_f32_array() + .into(), + flags: flags.bits(), + alpha_cutoff, + parallax_depth_scale: self.parallax_depth_scale, + max_parallax_layer_count: self.max_parallax_layer_count, + lightmap_exposure: self.lightmap_exposure, + max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), + deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, + uv_transform: self.uv_transform.into(), + } + } +} + +bitflags! { + /// The pipeline key for `StandardMaterial`, packed into 64 bits. + #[repr(C)] + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct StandardMaterialKey: u64 { + const CULL_FRONT = 0x000001; + const CULL_BACK = 0x000002; + const NORMAL_MAP = 0x000004; + const RELIEF_MAPPING = 0x000008; + const DIFFUSE_TRANSMISSION = 0x000010; + const SPECULAR_TRANSMISSION = 0x000020; + const CLEARCOAT = 0x000040; + const CLEARCOAT_NORMAL_MAP = 0x000080; + const ANISOTROPY = 0x000100; + const BASE_COLOR_UV = 0x000200; + const EMISSIVE_UV = 0x000400; + const METALLIC_ROUGHNESS_UV = 0x000800; + const OCCLUSION_UV = 0x001000; + const SPECULAR_TRANSMISSION_UV = 0x002000; + const THICKNESS_UV = 0x004000; + const DIFFUSE_TRANSMISSION_UV = 0x008000; + const NORMAL_MAP_UV = 0x010000; + const ANISOTROPY_UV = 0x020000; + const CLEARCOAT_UV = 0x040000; + const CLEARCOAT_ROUGHNESS_UV = 0x080000; + const CLEARCOAT_NORMAL_UV = 0x100000; + const SPECULAR_UV = 0x200000; + const SPECULAR_TINT_UV = 0x400000; + const DEPTH_BIAS = 0xffffffff_00000000; + } +} + +const STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT: u64 = 32; + +impl From<&StandardMaterial> for StandardMaterialKey { + fn from(material: &StandardMaterial) -> Self { + let mut key = StandardMaterialKey::empty(); + key.set( + StandardMaterialKey::CULL_FRONT, + material.cull_mode == Some(Face::Front), + ); + key.set( + StandardMaterialKey::CULL_BACK, + material.cull_mode == Some(Face::Back), + ); + key.set( + StandardMaterialKey::NORMAL_MAP, + material.normal_map_texture.is_some(), + ); + key.set( + StandardMaterialKey::RELIEF_MAPPING, + matches!( + material.parallax_mapping_method, + ParallaxMappingMethod::Relief { .. } + ), + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + material.diffuse_transmission > 0.0, + ); + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION, + material.specular_transmission > 0.0, + ); + + key.set(StandardMaterialKey::CLEARCOAT, material.clearcoat > 0.0); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + material.clearcoat > 0.0 && material.clearcoat_normal_texture.is_some(), + ); + + key.set( + StandardMaterialKey::ANISOTROPY, + material.anisotropy_strength > 0.0, + ); + + key.set( + StandardMaterialKey::BASE_COLOR_UV, + material.base_color_channel != UvChannel::Uv0, + ); + + key.set( + StandardMaterialKey::EMISSIVE_UV, + material.emissive_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + material.metallic_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::OCCLUSION_UV, + material.occlusion_channel != UvChannel::Uv0, + ); + #[cfg(feature = "pbr_transmission_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + material.specular_transmission_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::THICKNESS_UV, + material.thickness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + material.diffuse_transmission_channel != UvChannel::Uv0, + ); + } + + key.set( + StandardMaterialKey::NORMAL_MAP_UV, + material.normal_map_channel != UvChannel::Uv0, + ); + + #[cfg(feature = "pbr_anisotropy_texture")] + { + key.set( + StandardMaterialKey::ANISOTROPY_UV, + material.anisotropy_channel != UvChannel::Uv0, + ); + } + + #[cfg(feature = "pbr_specular_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_UV, + material.specular_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::SPECULAR_TINT_UV, + material.specular_tint_channel != UvChannel::Uv0, + ); + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + key.set( + StandardMaterialKey::CLEARCOAT_UV, + material.clearcoat_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + material.clearcoat_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + material.clearcoat_normal_channel != UvChannel::Uv0, + ); + } + + key.insert(StandardMaterialKey::from_bits_retain( + // Casting to i32 first to ensure the full i32 range is preserved. + // (wgpu expects the depth_bias as an i32 when this is extracted in a later step) + (material.depth_bias as i32 as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, + )); + key + } +} + +impl Material for StandardMaterial { + fn fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) + } + + #[inline] + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } + + #[inline] + fn opaque_render_method(&self) -> OpaqueRendererMethod { + match self.opaque_render_method { + // For now, diffuse transmission doesn't work under deferred rendering as we don't pack + // the required data into the GBuffer. If this material is set to `Auto`, we report it as + // `Forward` so that it's rendered correctly, even when the `DefaultOpaqueRendererMethod` + // is set to `Deferred`. + // + // If the developer explicitly sets the `OpaqueRendererMethod` to `Deferred`, we assume + // they know what they're doing and don't override it. + OpaqueRendererMethod::Auto if self.diffuse_transmission > 0.0 => { + OpaqueRendererMethod::Forward + } + other => other, + } + } + + #[inline] + fn depth_bias(&self) -> f32 { + self.depth_bias + } + + #[inline] + fn reads_view_transmission_texture(&self) -> bool { + self.specular_transmission > 0.0 + } + + fn prepass_fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr_prepass.wgsl")) + } + + fn deferred_fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + Self::fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + Self::prepass_fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + Self::deferred_fragment_shader() + } + + fn specialize( + _pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + if let Some(fragment) = descriptor.fragment.as_mut() { + let shader_defs = &mut fragment.shader_defs; + + for (flags, shader_def) in [ + ( + StandardMaterialKey::NORMAL_MAP, + "STANDARD_MATERIAL_NORMAL_MAP", + ), + (StandardMaterialKey::RELIEF_MAPPING, "RELIEF_MAPPING"), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION + | StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::CLEARCOAT, + "STANDARD_MATERIAL_CLEARCOAT", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP", + ), + ( + StandardMaterialKey::ANISOTROPY, + "STANDARD_MATERIAL_ANISOTROPY", + ), + ( + StandardMaterialKey::BASE_COLOR_UV, + "STANDARD_MATERIAL_BASE_COLOR_UV_B", + ), + ( + StandardMaterialKey::EMISSIVE_UV, + "STANDARD_MATERIAL_EMISSIVE_UV_B", + ), + ( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + "STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::OCCLUSION_UV, + "STANDARD_MATERIAL_OCCLUSION_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::THICKNESS_UV, + "STANDARD_MATERIAL_THICKNESS_UV_B", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::NORMAL_MAP_UV, + "STANDARD_MATERIAL_NORMAL_MAP_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_UV, + "STANDARD_MATERIAL_CLEARCOAT_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + "STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B", + ), + ( + StandardMaterialKey::ANISOTROPY_UV, + "STANDARD_MATERIAL_ANISOTROPY_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_UV, + "STANDARD_MATERIAL_SPECULAR_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TINT_UV, + "STANDARD_MATERIAL_SPECULAR_TINT_UV_B", + ), + ] { + if key.bind_group_data.intersects(flags) { + shader_defs.push(shader_def.into()); + } + } + } + + // Generally, we want to cull front faces if `CULL_FRONT` is present and + // backfaces if `CULL_BACK` is present. However, if the view has + // `INVERT_CULLING` on (usually used for mirrors and the like), we do + // the opposite. + descriptor.primitive.cull_mode = match ( + key.bind_group_data + .contains(StandardMaterialKey::CULL_FRONT), + key.bind_group_data.contains(StandardMaterialKey::CULL_BACK), + key.mesh_key.contains(MeshPipelineKey::INVERT_CULLING), + ) { + (true, false, false) | (false, true, true) => Some(Face::Front), + (false, true, false) | (true, false, true) => Some(Face::Back), + _ => None, + }; + + if let Some(label) = &mut descriptor.label { + *label = format!("pbr_{}", *label).into(); + } + if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { + depth_stencil.bias.constant = + (key.bind_group_data.bits() >> STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT) as i32; + } + Ok(()) + } +} diff --git a/vendor/bevy_pbr/src/prepass/mod.rs b/vendor/bevy_pbr/src/prepass/mod.rs new file mode 100644 index 00000000..d6e6ce69 --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/mod.rs @@ -0,0 +1,1289 @@ +mod prepass_bindings; + +use crate::{ + alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, + collect_meshes_for_gpu_building, init_material_pipeline, set_mesh_motion_vector_flags, + setup_morph_and_skinning_defs, skin, DeferredAlphaMaskDrawFunction, DeferredFragmentShader, + DeferredOpaqueDrawFunction, DeferredVertexShader, DrawMesh, EntitySpecializationTicks, + ErasedMaterialPipelineKey, MaterialPipeline, MaterialProperties, MeshLayouts, MeshPipeline, + MeshPipelineKey, OpaqueRendererMethod, PreparedMaterial, PrepassAlphaMaskDrawFunction, + PrepassFragmentShader, PrepassOpaqueDrawFunction, PrepassVertexShader, RenderLightmaps, + RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, RenderPhaseType, + SetMaterialBindGroup, SetMeshBindGroup, ShadowView, +}; +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_camera::{Camera, Camera3d}; +use bevy_core_pipeline::{core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prepass::*}; +use bevy_ecs::{ + prelude::*, + system::{ + lifetimeless::{Read, SRes}, + SystemParamItem, + }, +}; +use bevy_math::{Affine3A, Mat4, Vec4}; +use bevy_mesh::{Mesh, Mesh3d, MeshVertexBufferLayoutRef}; +use bevy_render::{ + alpha::AlphaMode, + batching::gpu_preprocessing::GpuPreprocessingSupport, + globals::{GlobalsBuffer, GlobalsUniform}, + mesh::{allocator::MeshAllocator, RenderMesh}, + render_asset::{prepare_assets, RenderAssets}, + render_phase::*, + render_resource::{binding_types::uniform_buffer, *}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + view::{ + ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewUniform, + ViewUniformOffset, ViewUniforms, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, + Extract, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bevy_transform::prelude::GlobalTransform; +pub use prepass_bindings::*; +use tracing::{error, warn}; + +#[cfg(feature = "meshlet")] +use crate::meshlet::{ + prepare_material_meshlet_meshes_prepass, queue_material_meshlet_meshes, InstanceManager, + MeshletMesh3d, +}; + +use alloc::sync::Arc; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{change_detection::Tick, system::SystemChangeTick}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + erased_render_asset::ErasedRenderAssets, + sync_world::MainEntityHashMap, + view::RenderVisibleEntities, + RenderSystems::{PrepareAssets, PrepareResources}, +}; +use bevy_utils::default; + +/// Sets up everything required to use the prepass pipeline. +/// +/// This does not add the actual prepasses, see [`PrepassPlugin`] for that. +pub struct PrepassPipelinePlugin; + +impl Plugin for PrepassPipelinePlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "prepass.wgsl"); + + load_shader_library!(app, "prepass_bindings.wgsl"); + load_shader_library!(app, "prepass_utils.wgsl"); + load_shader_library!(app, "prepass_io.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_systems( + RenderStartup, + ( + init_prepass_pipeline.after(init_material_pipeline), + init_prepass_view_bind_group, + ) + .chain(), + ) + .add_systems( + Render, + prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), + ) + .init_resource::>(); + } +} + +/// Sets up the prepasses for a material. +/// +/// This depends on the [`PrepassPipelinePlugin`]. +pub struct PrepassPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl PrepassPlugin { + /// Creates a new [`PrepassPlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + PrepassPlugin { debug_flags } + } +} + +impl Plugin for PrepassPlugin { + fn build(&self, app: &mut App) { + let no_prepass_plugin_loaded = app + .world() + .get_resource::() + .is_none(); + + if no_prepass_plugin_loaded { + app.insert_resource(AnyPrepassPluginLoaded) + // At the start of each frame, last frame's GlobalTransforms become this frame's PreviousGlobalTransforms + // and last frame's view projection matrices become this frame's PreviousViewProjections + .add_systems( + PreUpdate, + ( + update_mesh_previous_global_transforms, + update_previous_view_data, + ), + ) + .add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new( + self.debug_flags, + ), + )); + } + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + if no_prepass_plugin_loaded { + render_app + .add_systems(ExtractSchedule, extract_camera_previous_view_data) + .add_systems( + Render, + prepare_previous_view_uniforms.in_set(PrepareResources), + ); + } + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems( + Render, + ( + check_prepass_views_need_specialization.in_set(PrepareAssets), + specialize_prepass_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_prepass_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ); + + #[cfg(feature = "meshlet")] + render_app.add_systems( + Render, + prepare_material_meshlet_meshes_prepass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes) + .run_if(resource_exists::), + ); + } +} + +#[derive(Resource)] +struct AnyPrepassPluginLoaded; + +pub fn update_previous_view_data( + mut commands: Commands, + query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, +) { + for (entity, camera, camera_transform) in &query { + let world_from_view = camera_transform.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); + let view_from_clip = camera.clip_from_view().inverse(); + + commands.entity(entity).try_insert(PreviousViewData { + view_from_world, + clip_from_world: camera.clip_from_view() * view_from_world, + clip_from_view: camera.clip_from_view(), + world_from_clip: Mat4::from(world_from_view) * view_from_clip, + view_from_clip, + }); + } +} + +#[derive(Component, PartialEq, Default)] +pub struct PreviousGlobalTransform(pub Affine3A); + +#[cfg(not(feature = "meshlet"))] +type PreviousMeshFilter = With; +#[cfg(feature = "meshlet")] +type PreviousMeshFilter = Or<(With, With)>; + +pub fn update_mesh_previous_global_transforms( + mut commands: Commands, + views: Query<&Camera, Or<(With, With)>>, + new_meshes: Query< + (Entity, &GlobalTransform), + (PreviousMeshFilter, Without), + >, + mut meshes: Query<(&GlobalTransform, &mut PreviousGlobalTransform), PreviousMeshFilter>, +) { + let should_run = views.iter().any(|camera| camera.is_active); + + if should_run { + for (entity, transform) in &new_meshes { + let new_previous_transform = PreviousGlobalTransform(transform.affine()); + commands.entity(entity).try_insert(new_previous_transform); + } + meshes.par_iter_mut().for_each(|(transform, mut previous)| { + previous.set_if_neq(PreviousGlobalTransform(transform.affine())); + }); + } +} + +#[derive(Resource, Clone)] +pub struct PrepassPipeline { + pub view_layout_motion_vectors: BindGroupLayoutDescriptor, + pub view_layout_no_motion_vectors: BindGroupLayoutDescriptor, + pub mesh_layouts: MeshLayouts, + pub empty_layout: BindGroupLayoutDescriptor, + pub default_prepass_shader: Handle, + + /// Whether skins will use uniform buffers on account of storage buffers + /// being unavailable on this platform. + pub skins_use_uniform_buffers: bool, + + pub depth_clip_control_supported: bool, + + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + pub binding_arrays_are_usable: bool, + pub material_pipeline: MaterialPipeline, +} + +pub fn init_prepass_pipeline( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + mesh_pipeline: Res, + material_pipeline: Res, + asset_server: Res, +) { + let visibility_ranges_buffer_binding_type = + render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + let view_layout_motion_vectors = BindGroupLayoutDescriptor::new( + "prepass_view_layout_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // PreviousViewUniforms + (2, uniform_buffer::(true)), + // VisibilityRanges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + ), + ), + ); + + let view_layout_no_motion_vectors = BindGroupLayoutDescriptor::new( + "prepass_view_layout_no_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // VisibilityRanges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + ), + ), + ); + + let depth_clip_control_supported = render_device + .features() + .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); + commands.insert_resource(PrepassPipeline { + view_layout_motion_vectors, + view_layout_no_motion_vectors, + mesh_layouts: mesh_pipeline.mesh_layouts.clone(), + default_prepass_shader: load_embedded_asset!(asset_server.as_ref(), "prepass.wgsl"), + skins_use_uniform_buffers: skin::skins_use_uniform_buffers(&render_device.limits()), + depth_clip_control_supported, + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + empty_layout: BindGroupLayoutDescriptor::new("prepass_empty_layout", &[]), + material_pipeline: material_pipeline.clone(), + }); +} + +pub struct PrepassPipelineSpecializer { + pub pipeline: PrepassPipeline, + pub properties: Arc, +} + +impl SpecializedMeshPipeline for PrepassPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut shader_defs = Vec::new(); + if self.properties.bindless { + shader_defs.push("BINDLESS".into()); + } + let mut descriptor = + self.pipeline + .specialize(key.mesh_key, shader_defs, layout, &self.properties)?; + + // This is a bit risky because it's possible to change something that would + // break the prepass but be fine in the main pass. + // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. + if let Some(specialize) = self.properties.specialize { + specialize( + &self.pipeline.material_pipeline, + &mut descriptor, + layout, + key, + )?; + } + + Ok(descriptor) + } +} + +impl PrepassPipeline { + fn specialize( + &self, + mesh_key: MeshPipelineKey, + shader_defs: Vec, + layout: &MeshVertexBufferLayoutRef, + material_properties: &MaterialProperties, + ) -> Result { + let mut shader_defs = shader_defs; + let mut bind_group_layouts = vec![ + if mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + self.view_layout_motion_vectors.clone() + } else { + self.view_layout_no_motion_vectors.clone() + }, + self.empty_layout.clone(), + ]; + let mut vertex_attributes = Vec::new(); + + // Let the shader code know that it's running in a prepass pipeline. + // (PBR code will use this to detect that it's running in deferred mode, + // since that's the only time it gets called from a prepass pipeline.) + shader_defs.push("PREPASS_PIPELINE".into()); + + shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + crate::MATERIAL_BIND_GROUP_INDEX as u32, + )); + // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. + // The main limitation right now is that bind group order is hardcoded in shaders. + bind_group_layouts.push( + material_properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ); + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); + if mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + let blend_key = mesh_key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); + if blend_key == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA { + shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into()); + } + if blend_key == MeshPipelineKey::BLEND_ALPHA { + shader_defs.push("BLEND_ALPHA".into()); + } + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + // For directional light shadow map views, use unclipped depth via either the native GPU feature, + // or emulated by setting depth in the fragment shader for GPUs that don't support it natively. + let emulate_unclipped_depth = mesh_key.contains(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO) + && !self.depth_clip_control_supported; + if emulate_unclipped_depth { + shader_defs.push("UNCLIPPED_DEPTH_ORTHO_EMULATION".into()); + // PERF: This line forces the "prepass fragment shader" to always run in + // common scenarios like "directional light calculation". Doing so resolves + // a pretty nasty depth clamping bug, but it also feels a bit excessive. + // We should try to find a way to resolve this without forcing the fragment + // shader to run. + // https://github.com/bevyengine/bevy/pull/8877 + shader_defs.push("PREPASS_FRAGMENT".into()); + } + let unclipped_depth = mesh_key.contains(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO) + && self.depth_clip_control_supported; + if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); + } + if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_B".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(2)); + } + if mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + if mesh_key.intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) + { + shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into()); + if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { + shader_defs.push("VERTEX_NORMALS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(3)); + } else if mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + warn!( + "The default normal prepass expects the mesh to have vertex normal attributes." + ); + } + if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); + } + } + if mesh_key + .intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) + { + shader_defs.push("MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + shader_defs.push("DEFERRED_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::LIGHTMAPPED) { + shader_defs.push("LIGHTMAP".into()); + } + if mesh_key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(7)); + } + if mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + if mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + if mesh_key.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) { + shader_defs.push("VISIBILITY_RANGE_DITHER".into()); + } + if mesh_key.intersects( + MeshPipelineKey::NORMAL_PREPASS + | MeshPipelineKey::MOTION_VECTOR_PREPASS + | MeshPipelineKey::DEFERRED_PREPASS, + ) { + shader_defs.push("PREPASS_FRAGMENT".into()); + } + let bind_group = setup_morph_and_skinning_defs( + &self.mesh_layouts, + layout, + 5, + &mesh_key, + &mut shader_defs, + &mut vertex_attributes, + self.skins_use_uniform_buffers, + ); + bind_group_layouts.insert(2, bind_group); + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + // Setup prepass fragment targets - normals in slot 0 (or None if not needed), motion vectors in slot 1 + let mut targets = prepass_target_descriptors( + mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS), + mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS), + mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS), + ); + + if targets.iter().all(Option::is_none) { + // if no targets are required then clear the list, so that no fragment shader is required + // (though one may still be used for discarding depth buffer writes) + targets.clear(); + } + + // The fragment shader is only used when the normal prepass or motion vectors prepass + // is enabled, the material uses alpha cutoff values and doesn't rely on the standard + // prepass shader, or we are emulating unclipped depth in the fragment shader. + let fragment_required = !targets.is_empty() + || emulate_unclipped_depth + || (mesh_key.contains(MeshPipelineKey::MAY_DISCARD) + && material_properties + .get_shader(PrepassFragmentShader) + .is_some()); + + let fragment = fragment_required.then(|| { + // Use the fragment shader from the material + let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + match material_properties.get_shader(DeferredFragmentShader) { + Some(frag_shader_handle) => frag_shader_handle, + None => self.default_prepass_shader.clone(), + } + } else { + match material_properties.get_shader(PrepassFragmentShader) { + Some(frag_shader_handle) => frag_shader_handle, + None => self.default_prepass_shader.clone(), + } + }; + + FragmentState { + shader: frag_shader_handle, + shader_defs: shader_defs.clone(), + targets, + ..default() + } + }); + + // Use the vertex shader from the material if present + let vert_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + if let Some(handle) = material_properties.get_shader(DeferredVertexShader) { + handle + } else { + self.default_prepass_shader.clone() + } + } else if let Some(handle) = material_properties.get_shader(PrepassVertexShader) { + handle + } else { + self.default_prepass_shader.clone() + }; + let descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: vert_shader_handle, + shader_defs, + buffers: vec![vertex_buffer_layout], + ..default() + }, + fragment, + layout: bind_group_layouts, + primitive: PrimitiveState { + topology: mesh_key.primitive_topology(), + unclipped_depth, + ..default() + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("prepass_pipeline".into()), + ..default() + }; + Ok(descriptor) + } +} + +// Extract the render phases for the prepass +pub fn extract_camera_previous_view_data( + mut commands: Commands, + cameras_3d: Extract), With>>, +) { + for (entity, camera, maybe_previous_view_data) in cameras_3d.iter() { + let mut entity = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if camera.is_active { + if let Some(previous_view_data) = maybe_previous_view_data { + entity.insert(previous_view_data.clone()); + } + } else { + entity.remove::(); + } + } +} + +pub fn prepare_previous_view_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut previous_view_uniforms: ResMut, + views: Query< + (Entity, &ExtractedView, Option<&PreviousViewData>), + Or<(With, With)>, + >, +) { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = + previous_view_uniforms + .uniforms + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + + for (entity, camera, maybe_previous_view_uniforms) in views_iter { + let prev_view_data = match maybe_previous_view_uniforms { + Some(previous_view) => previous_view.clone(), + None => { + let world_from_view = camera.world_from_view.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); + let view_from_clip = camera.clip_from_view.inverse(); + + PreviousViewData { + view_from_world, + clip_from_world: camera.clip_from_view * view_from_world, + clip_from_view: camera.clip_from_view, + world_from_clip: Mat4::from(world_from_view) * view_from_clip, + view_from_clip, + } + } + }; + + commands.entity(entity).insert(PreviousViewUniformOffset { + offset: writer.write(&prev_view_data), + }); + } +} + +#[derive(Resource)] +pub struct PrepassViewBindGroup { + pub motion_vectors: Option, + pub no_motion_vectors: Option, + pub empty_bind_group: BindGroup, +} + +pub fn init_prepass_view_bind_group( + mut commands: Commands, + render_device: Res, + pipeline_cache: Res, + pipeline: Res, +) { + let empty_bind_group = render_device.create_bind_group( + "prepass_view_empty_bind_group", + &pipeline_cache.get_bind_group_layout(&pipeline.empty_layout), + &[], + ); + commands.insert_resource(PrepassViewBindGroup { + motion_vectors: None, + no_motion_vectors: None, + empty_bind_group, + }); +} + +pub fn prepare_prepass_view_bind_group( + render_device: Res, + pipeline_cache: Res, + prepass_pipeline: Res, + view_uniforms: Res, + globals_buffer: Res, + previous_view_uniforms: Res, + visibility_ranges: Res, + mut prepass_view_bind_group: ResMut, +) { + if let (Some(view_binding), Some(globals_binding), Some(visibility_ranges_buffer)) = ( + view_uniforms.uniforms.binding(), + globals_buffer.buffer.binding(), + visibility_ranges.buffer().buffer(), + ) { + prepass_view_bind_group.no_motion_vectors = Some(render_device.create_bind_group( + "prepass_view_no_motion_vectors_bind_group", + &pipeline_cache.get_bind_group_layout(&prepass_pipeline.view_layout_no_motion_vectors), + &BindGroupEntries::with_indices(( + (0, view_binding.clone()), + (1, globals_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + )), + )); + + if let Some(previous_view_uniforms_binding) = previous_view_uniforms.uniforms.binding() { + prepass_view_bind_group.motion_vectors = Some(render_device.create_bind_group( + "prepass_view_motion_vectors_bind_group", + &pipeline_cache.get_bind_group_layout(&prepass_pipeline.view_layout_motion_vectors), + &BindGroupEntries::with_indices(( + (0, view_binding), + (1, globals_binding), + (2, previous_view_uniforms_binding), + (14, visibility_ranges_buffer.as_entire_binding()), + )), + )); + } + } +} + +/// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialPipelineCache { + // view_entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewKeyPrepassCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewPrepassSpecializationTicks(HashMap); + +pub fn check_prepass_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &ExtractedView, + &Msaa, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + Option<&MotionVectorPrepass>, + )>, + ticks: SystemChangeTick, +) { + for (view, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in views.iter_mut() { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + if depth_prepass.is_some() { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if let Some(current_key) = view_key_cache.get_mut(&view.retained_view_entity) { + if *current_key != view_key { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } else { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } +} + +pub fn specialize_prepass_material_meshes( + render_meshes: Res>, + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + render_lightmaps: Res, + render_visibility_ranges: Res, + view_key_cache: Res, + views: Query<( + &ExtractedView, + &RenderVisibleEntities, + &Msaa, + Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, + )>, + ( + opaque_prepass_render_phases, + alpha_mask_prepass_render_phases, + opaque_deferred_render_phases, + alpha_mask_deferred_render_phases, + ): ( + Res>, + Res>, + Res>, + Res>, + ), + ( + mut specialized_material_pipeline_cache, + ticks, + prepass_pipeline, + mut pipelines, + pipeline_cache, + view_specialization_ticks, + entity_specialization_ticks, + ): ( + ResMut, + SystemChangeTick, + Res, + ResMut>, + Res, + Res, + Res, + ), +) { + for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views + { + if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !opaque_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) + { + continue; + } + + let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&extracted_view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks + .get(visible_entity) + .unwrap() + .system_tick; + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + if !material.properties.prepass_enabled { + // If the material was previously specialized for prepass, remove it + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + + let alpha_mode = material.properties.alpha_mode; + match alpha_mode { + AlphaMode::Opaque | AlphaMode::AlphaToCoverage | AlphaMode::Mask(_) => { + mesh_key |= alpha_mode_pipeline_key(alpha_mode, msaa); + } + AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::Multiply => { + // In case this material was previously in a valid alpha_mode, remove it to + // stop the queue system from assuming its retained cache to be valid. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + } + + if material.properties.reads_view_transmission_texture { + // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` + // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + + let forward = match material.properties.render_method { + OpaqueRendererMethod::Forward => true, + OpaqueRendererMethod::Deferred => false, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + let deferred = deferred_prepass.is_some() && !forward; + + if deferred { + mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + // Even though we don't use the lightmap in the forward prepass, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling && deferred { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } + } + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + // If the previous frame has skins or morph targets, note that. + if motion_vector_prepass.is_some() { + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let prepass_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &prepass_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } +} + +pub fn queue_prepass_material_meshes( + render_mesh_instances: Res, + render_materials: Res>, + render_material_instances: Res, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_prepass_render_phases: ResMut>, + mut alpha_mask_prepass_render_phases: ResMut>, + mut opaque_deferred_render_phases: ResMut>, + mut alpha_mask_deferred_render_phases: ResMut>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: Res, +) { + for (extracted_view, visible_entities) in &views { + let ( + mut opaque_phase, + mut alpha_mask_phase, + mut opaque_deferred_phase, + mut alpha_mask_deferred_phase, + ) = ( + opaque_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + opaque_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + ); + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view.retained_view_entity) + else { + continue; + }; + + // Skip if there's no place to put the mesh. + if opaque_phase.is_none() + && alpha_mask_phase.is_none() + && opaque_deferred_phase.is_none() + && alpha_mask_deferred_phase.is_none() + { + continue; + } + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((current_change_tick, pipeline_id)) = + view_specialized_material_pipeline_cache.get(visible_entity) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if opaque_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || alpha_mask_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || opaque_deferred_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || alpha_mask_deferred_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + + let deferred = match material.properties.render_method { + OpaqueRendererMethod::Forward => false, + OpaqueRendererMethod::Deferred => true, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + match material.properties.render_phase_type { + RenderPhaseType::Opaque => { + if deferred { + let Some(draw_function) = material + .properties + .get_draw_function(DeferredOpaqueDrawFunction) + else { + continue; + }; + opaque_deferred_phase.as_mut().unwrap().add( + OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, + OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } else if let Some(opaque_phase) = opaque_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material + .properties + .get_draw_function(PrepassOpaqueDrawFunction) + else { + continue; + }; + opaque_phase.add( + OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, + OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + RenderPhaseType::AlphaMask => { + if deferred { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material + .properties + .get_draw_function(DeferredAlphaMaskDrawFunction) + else { + continue; + }; + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_deferred_phase.as_mut().unwrap().add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material + .properties + .get_draw_function(PrepassAlphaMaskDrawFunction) + else { + continue; + }; + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + _ => {} + } + } + } +} + +pub struct SetPrepassViewBindGroup; +impl RenderCommand

for SetPrepassViewBindGroup { + type Param = SRes; + type ViewQuery = ( + Read, + Has, + Option>, + ); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (view_uniform_offset, has_motion_vector_prepass, previous_view_uniform_offset): ( + &'_ ViewUniformOffset, + bool, + Option<&'_ PreviousViewUniformOffset>, + ), + _entity: Option<()>, + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + + match previous_view_uniform_offset { + Some(previous_view_uniform_offset) if has_motion_vector_prepass => { + pass.set_bind_group( + I, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } + _ => { + pass.set_bind_group( + I, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + } + RenderCommandResult::Success + } +} + +pub struct SetPrepassViewEmptyBindGroup; +impl RenderCommand

for SetPrepassViewEmptyBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + _entity: Option<()>, + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + pass.set_bind_group(I, &prepass_view_bind_group.empty_bind_group, &[]); + RenderCommandResult::Success + } +} + +pub type DrawPrepass = ( + SetItemPipeline, + SetPrepassViewBindGroup<0>, + SetPrepassViewEmptyBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup<3>, + DrawMesh, +); diff --git a/vendor/bevy_pbr/src/prepass/prepass.wgsl b/vendor/bevy_pbr/src/prepass/prepass.wgsl new file mode 100644 index 00000000..52dd9bf2 --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/prepass.wgsl @@ -0,0 +1,219 @@ +#import bevy_pbr::{ + prepass_bindings, + mesh_bindings::mesh, + mesh_functions, + prepass_io::{Vertex, VertexOutput, FragmentOutput}, + skinning, + morph, + mesh_view_bindings::view, + view_transformations::position_world_to_clip, +} + +#ifdef DEFERRED_PREPASS +#import bevy_pbr::rgb9e5 +#endif + +#ifdef MORPH_TARGETS +fn morph_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + + let weight_count = morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = morph::weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph::morph(vertex_index, morph::position_offset, i); +#ifdef VERTEX_NORMALS + vertex.normal += weight * morph::morph(vertex_index, morph::normal_offset, i); +#endif +#ifdef VERTEX_TANGENTS + vertex.tangent += vec4(weight * morph::morph(vertex_index, morph::tangent_offset, i), 0.0); +#endif + } + return vertex; +} + +// Returns the morphed position of the given vertex from the previous frame. +// +// This function is used for motion vector calculation, and, as such, it doesn't +// bother morphing the normals and tangents. +fn morph_prev_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let weight_count = morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = morph::prev_weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph::morph(vertex.index, morph::position_offset, i); + // Don't bother morphing normals and tangents; we don't need them for + // motion vector calculation. + } + return vertex; +} +#endif // MORPH_TARGETS + +@vertex +fn vertex(vertex_no_morph: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef MORPH_TARGETS + var vertex = morph_vertex(vertex_no_morph); +#else + var vertex = vertex_no_morph; +#endif + + let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); + +#ifdef SKINNED + var world_from_local = skinning::skin_model( + vertex.joint_indices, + vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else // SKINNED + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + var world_from_local = mesh_world_from_local; +#endif // SKINNED + + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.position = position_world_to_clip(out.world_position.xyz); +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.unclipped_depth = out.position.z; + out.position.z = min(out.position.z, 1.0); // Clamp depth to avoid clipping +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef VERTEX_UVS_A + out.uv = vertex.uv; +#endif // VERTEX_UVS_A + +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif // VERTEX_UVS_B + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS +#ifdef SKINNED + out.world_normal = skinning::skin_normals(world_from_local, vertex.normal); +#else // SKINNED + out.world_normal = mesh_functions::mesh_normal_local_to_world( + vertex.normal, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif // SKINNED +#endif // VERTEX_NORMALS + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_functions::mesh_tangent_local_to_world( + world_from_local, + vertex.tangent, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef VERTEX_COLORS + out.color = vertex.color; +#endif + + // Compute the motion vector for TAA among other purposes. For this we need + // to know where the vertex was last frame. +#ifdef MOTION_VECTOR_PREPASS + + // Take morph targets into account. +#ifdef MORPH_TARGETS + +#ifdef HAS_PREVIOUS_MORPH + let prev_vertex = morph_prev_vertex(vertex_no_morph); +#else // HAS_PREVIOUS_MORPH + let prev_vertex = vertex_no_morph; +#endif // HAS_PREVIOUS_MORPH + +#else // MORPH_TARGETS + let prev_vertex = vertex_no_morph; +#endif // MORPH_TARGETS + + // Take skinning into account. +#ifdef SKINNED + +#ifdef HAS_PREVIOUS_SKIN + let prev_model = skinning::skin_prev_model( + prev_vertex.joint_indices, + prev_vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else // HAS_PREVIOUS_SKIN + let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index); +#endif // HAS_PREVIOUS_SKIN + +#else // SKINNED + let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index); +#endif // SKINNED + + out.previous_world_position = mesh_functions::mesh_position_local_to_world( + prev_model, + vec4(prev_vertex.position, 1.0) + ); +#endif // MOTION_VECTOR_PREPASS + +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + out.instance_index = vertex_no_morph.instance_index; +#endif + +#ifdef VISIBILITY_RANGE_DITHER + out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level( + vertex_no_morph.instance_index, mesh_world_from_local[3]); +#endif // VISIBILITY_RANGE_DITHER + + return out; +} + +#ifdef PREPASS_FRAGMENT +@fragment +fn fragment(in: VertexOutput) -> FragmentOutput { + var out: FragmentOutput; + +#ifdef NORMAL_PREPASS + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.frag_depth = in.unclipped_depth; +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef MOTION_VECTOR_PREPASS + let clip_position_t = view.unjittered_clip_from_world * in.world_position; + let clip_position = clip_position_t.xy / clip_position_t.w; + let previous_clip_position_t = prepass_bindings::previous_view_uniforms.clip_from_world * in.previous_world_position; + let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; + // These motion vectors are used as offsets to UV positions and are stored + // in the range -1,1 to allow offsetting from the one corner to the + // diagonally-opposite corner in UV coordinates, in either direction. + // A difference between diagonally-opposite corners of clip space is in the + // range -2,2, so this needs to be scaled by 0.5. And the V direction goes + // down where clip space y goes up, so y needs to be flipped. + out.motion_vector = (clip_position - previous_clip_position) * vec2(0.5, -0.5); +#endif // MOTION_VECTOR_PREPASS + +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + + return out; +} +#endif // PREPASS_FRAGMENT diff --git a/vendor/bevy_pbr/src/prepass/prepass_bindings.rs b/vendor/bevy_pbr/src/prepass/prepass_bindings.rs new file mode 100644 index 00000000..3c66625e --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/prepass_bindings.rs @@ -0,0 +1,75 @@ +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_render::render_resource::{ + binding_types::{ + texture_2d, texture_2d_multisampled, texture_depth_2d, texture_depth_2d_multisampled, + }, + BindGroupLayoutEntryBuilder, TextureAspect, TextureSampleType, TextureView, + TextureViewDescriptor, +}; +use bevy_utils::default; + +use crate::MeshPipelineViewLayoutKey; + +pub fn get_bind_group_layout_entries( + layout_key: MeshPipelineViewLayoutKey, +) -> [Option; 4] { + let mut entries: [Option; 4] = [None; 4]; + + let multisampled = layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED); + + if layout_key.contains(MeshPipelineViewLayoutKey::DEPTH_PREPASS) { + // Depth texture + entries[0] = if multisampled { + Some(texture_depth_2d_multisampled()) + } else { + Some(texture_depth_2d()) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::NORMAL_PREPASS) { + // Normal texture + entries[1] = if multisampled { + Some(texture_2d_multisampled(TextureSampleType::Float { + filterable: false, + })) + } else { + Some(texture_2d(TextureSampleType::Float { filterable: false })) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS) { + // Motion Vectors texture + entries[2] = if multisampled { + Some(texture_2d_multisampled(TextureSampleType::Float { + filterable: false, + })) + } else { + Some(texture_2d(TextureSampleType::Float { filterable: false })) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) { + // Deferred texture + entries[3] = Some(texture_2d(TextureSampleType::Uint)); + } + + entries +} + +pub fn get_bindings(prepass_textures: Option<&ViewPrepassTextures>) -> [Option; 4] { + let depth_desc = TextureViewDescriptor { + label: Some("prepass_depth"), + aspect: TextureAspect::DepthOnly, + ..default() + }; + let depth_view = prepass_textures + .and_then(|x| x.depth.as_ref()) + .map(|texture| texture.texture.texture.create_view(&depth_desc)); + + [ + depth_view, + prepass_textures.and_then(|pt| pt.normal_view().cloned()), + prepass_textures.and_then(|pt| pt.motion_vectors_view().cloned()), + prepass_textures.and_then(|pt| pt.deferred_view().cloned()), + ] +} diff --git a/vendor/bevy_pbr/src/prepass/prepass_bindings.wgsl b/vendor/bevy_pbr/src/prepass/prepass_bindings.wgsl new file mode 100644 index 00000000..141f7d7b --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -0,0 +1,13 @@ +#define_import_path bevy_pbr::prepass_bindings + +struct PreviousViewUniforms { + view_from_world: mat4x4, + clip_from_world: mat4x4, + clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, +} + +@group(0) @binding(2) var previous_view_uniforms: PreviousViewUniforms; + +// Material bindings will be in @group(2) diff --git a/vendor/bevy_pbr/src/prepass/prepass_io.wgsl b/vendor/bevy_pbr/src/prepass/prepass_io.wgsl new file mode 100644 index 00000000..c3c0e555 --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/prepass_io.wgsl @@ -0,0 +1,100 @@ +#define_import_path bevy_pbr::prepass_io + +// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can +// pass them to custom prepass shaders like pbr_prepass.wgsl. +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, + +#ifdef VERTEX_UVS_A + @location(1) uv: vec2, +#endif + +#ifdef VERTEX_UVS_B + @location(2) uv_b: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS + @location(3) normal: vec3, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef SKINNED + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, +#endif + +#ifdef VERTEX_COLORS + @location(7) color: vec4, +#endif + +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif // MORPH_TARGETS +} + +struct VertexOutput { + // This is `clip position` when the struct is used as a vertex stage output + // and `frag coord` when used as a fragment stage input + @builtin(position) position: vec4, + +#ifdef VERTEX_UVS_A + @location(0) uv: vec2, +#endif + +#ifdef VERTEX_UVS_B + @location(1) uv_b: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(2) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(3) world_tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + + @location(4) world_position: vec4, +#ifdef MOTION_VECTOR_PREPASS + @location(5) previous_world_position: vec4, +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + @location(6) unclipped_depth: f32, +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(7) instance_index: u32, +#endif + +#ifdef VERTEX_COLORS + @location(8) color: vec4, +#endif + +#ifdef VISIBILITY_RANGE_DITHER + @location(9) @interpolate(flat) visibility_range_dither: i32, +#endif // VISIBILITY_RANGE_DITHER +} + +#ifdef PREPASS_FRAGMENT +struct FragmentOutput { +#ifdef NORMAL_PREPASS + @location(0) normal: vec4, +#endif + +#ifdef MOTION_VECTOR_PREPASS + @location(1) motion_vector: vec2, +#endif + +#ifdef DEFERRED_PREPASS + @location(2) deferred: vec4, + @location(3) deferred_lighting_pass_id: u32, +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + @builtin(frag_depth) frag_depth: f32, +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION +} +#endif //PREPASS_FRAGMENT diff --git a/vendor/bevy_pbr/src/prepass/prepass_utils.wgsl b/vendor/bevy_pbr/src/prepass/prepass_utils.wgsl new file mode 100644 index 00000000..42f403cd --- /dev/null +++ b/vendor/bevy_pbr/src/prepass/prepass_utils.wgsl @@ -0,0 +1,35 @@ +#define_import_path bevy_pbr::prepass_utils + +#import bevy_pbr::mesh_view_bindings as view_bindings + +#ifdef DEPTH_PREPASS +fn prepass_depth(frag_coord: vec4, sample_index: u32) -> f32 { +#ifdef MULTISAMPLED + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else // MULTISAMPLED + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), 0); +#endif // MULTISAMPLED +} +#endif // DEPTH_PREPASS + +#ifdef NORMAL_PREPASS +fn prepass_normal(frag_coord: vec4, sample_index: u32) -> vec3 { +#ifdef MULTISAMPLED + let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2(frag_coord.xy), 0); +#endif // MULTISAMPLED + return normalize(normal_sample.xyz * 2.0 - vec3(1.0)); +} +#endif // NORMAL_PREPASS + +#ifdef MOTION_VECTOR_PREPASS +fn prepass_motion_vector(frag_coord: vec4, sample_index: u32) -> vec2 { +#ifdef MULTISAMPLED + let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return motion_vector_sample.rg; +} +#endif // MOTION_VECTOR_PREPASS diff --git a/vendor/bevy_pbr/src/render/build_indirect_params.wgsl b/vendor/bevy_pbr/src/render/build_indirect_params.wgsl new file mode 100644 index 00000000..5ca6d4c0 --- /dev/null +++ b/vendor/bevy_pbr/src/render/build_indirect_params.wgsl @@ -0,0 +1,142 @@ +// Builds GPU indirect draw parameters from metadata. +// +// This only runs when indirect drawing is enabled. It takes the output of +// `mesh_preprocess.wgsl` and creates indirect parameters for the GPU. +// +// This shader runs separately for indexed and non-indexed meshes. Unlike +// `mesh_preprocess.wgsl`, which runs one instance per mesh *instance*, one +// instance of this shader corresponds to a single *batch* which could contain +// arbitrarily many instances of a single mesh. + +#import bevy_pbr::mesh_preprocess_types::{ + IndirectBatchSet, + IndirectParametersIndexed, + IndirectParametersNonIndexed, + IndirectParametersCpuMetadata, + IndirectParametersGpuMetadata, + MeshInput +} + +// The data for each mesh that the CPU supplied to the GPU. +@group(0) @binding(0) var current_input: array; + +// Data that we use to generate the indirect parameters. +// +// The `mesh_preprocess.wgsl` shader emits these. +@group(0) @binding(1) var indirect_parameters_cpu_metadata: + array; + +@group(0) @binding(2) var indirect_parameters_gpu_metadata: + array; + +// Information about each batch set. +// +// A *batch set* is a set of meshes that might be multi-drawn together. +@group(0) @binding(3) var indirect_batch_sets: array; + +#ifdef INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for indexed meshes. +@group(0) @binding(4) var indirect_parameters: + array; +#else // INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for non-indexed meshes. +@group(0) @binding(4) var indirect_parameters: + array; +#endif // INDEXED + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index (i.e. batch index). If this thread doesn't + // correspond to any index, bail. + let instance_index = global_invocation_id.x; + if (instance_index >= arrayLength(&indirect_parameters_cpu_metadata)) { + return; + } + + // Unpack the metadata for this batch. + let base_output_index = indirect_parameters_cpu_metadata[instance_index].base_output_index; + let batch_set_index = indirect_parameters_cpu_metadata[instance_index].batch_set_index; + let mesh_index = indirect_parameters_gpu_metadata[instance_index].mesh_index; + + // If we aren't using `multi_draw_indirect_count`, we have a 1:1 fixed + // assignment of batches to slots in the indirect parameters buffer, so we + // can just use the instance index as the index of our indirect parameters. + let early_instance_count = + indirect_parameters_gpu_metadata[instance_index].early_instance_count; + let late_instance_count = indirect_parameters_gpu_metadata[instance_index].late_instance_count; + + // If in the early phase, we draw only the early meshes. If in the late + // phase, we draw only the late meshes. If in the main phase, draw all the + // meshes. +#ifdef EARLY_PHASE + let instance_count = early_instance_count; +#else // EARLY_PHASE +#ifdef LATE_PHASE + let instance_count = late_instance_count; +#else // LATE_PHASE + let instance_count = early_instance_count + late_instance_count; +#endif // LATE_PHASE +#endif // EARLY_PHASE + + var indirect_parameters_index = instance_index; + + // If the current hardware and driver support `multi_draw_indirect_count`, + // dynamically reserve an index for the indirect parameters we're to + // generate. +#ifdef MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + // If this batch belongs to a batch set, then allocate space for the + // indirect commands in that batch set. + if (batch_set_index != 0xffffffffu) { + // Bail out now if there are no instances. Note that we can only bail if + // we're in a batch set. That's because only batch sets are drawn using + // `multi_draw_indirect_count`. If we aren't using + // `multi_draw_indirect_count`, then we need to continue in order to + // zero out the instance count; otherwise, it'll have garbage data in + // it. + if (instance_count == 0u) { + return; + } + + let indirect_parameters_base = + indirect_batch_sets[batch_set_index].indirect_parameters_base; + let indirect_parameters_offset = + atomicAdd(&indirect_batch_sets[batch_set_index].indirect_parameters_count, 1u); + + indirect_parameters_index = indirect_parameters_base + indirect_parameters_offset; + } +#endif // MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + + // Build up the indirect parameters. The structures for indexed and + // non-indexed meshes are slightly different. + + indirect_parameters[indirect_parameters_index].instance_count = instance_count; + +#ifdef LATE_PHASE + // The late mesh instances are stored after the early mesh instances, so we + // offset the output index by the number of early mesh instances. + indirect_parameters[indirect_parameters_index].first_instance = + base_output_index + early_instance_count; +#else // LATE_PHASE + indirect_parameters[indirect_parameters_index].first_instance = base_output_index; +#endif // LATE_PHASE + + indirect_parameters[indirect_parameters_index].base_vertex = + current_input[mesh_index].first_vertex_index; + +#ifdef INDEXED + indirect_parameters[indirect_parameters_index].index_count = + current_input[mesh_index].index_count; + indirect_parameters[indirect_parameters_index].first_index = + current_input[mesh_index].first_index_index; +#else // INDEXED + indirect_parameters[indirect_parameters_index].vertex_count = + current_input[mesh_index].index_count; +#endif // INDEXED +} diff --git a/vendor/bevy_pbr/src/render/clustered_forward.wgsl b/vendor/bevy_pbr/src/render/clustered_forward.wgsl new file mode 100644 index 00000000..aa3fb4f1 --- /dev/null +++ b/vendor/bevy_pbr/src/render/clustered_forward.wgsl @@ -0,0 +1,193 @@ +#define_import_path bevy_pbr::clustered_forward + +#import bevy_pbr::{ + mesh_view_bindings as bindings, + utils::rand_f, +} + +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::PI_2, +} + +// Offsets within the `cluster_offsets_and_counts` buffer for a single cluster. +// +// These offsets must be monotonically nondecreasing. That is, indices are +// always sorted into the following order: point lights, spot lights, reflection +// probes, irradiance volumes. +struct ClusterableObjectIndexRanges { + // The offset of the index of the first point light. + first_point_light_index_offset: u32, + // The offset of the index of the first spot light, which also terminates + // the list of point lights. + first_spot_light_index_offset: u32, + // The offset of the index of the first reflection probe, which also + // terminates the list of spot lights. + first_reflection_probe_index_offset: u32, + // The offset of the index of the first irradiance volumes, which also + // terminates the list of reflection probes. + first_irradiance_volume_index_offset: u32, + first_decal_offset: u32, + // One past the offset of the index of the final clusterable object for this + // cluster. + last_clusterable_object_index_offset: u32, +} + +// NOTE: Keep in sync with bevy_pbr/src/light.rs +fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { + var z_slice: u32 = 0u; + if is_orthographic { + // NOTE: view_z is correct in the orthographic case + z_slice = u32(floor((view_z - bindings::lights.cluster_factors.z) * bindings::lights.cluster_factors.w)); + } else { + // NOTE: had to use -view_z to make it positive else log(negative) is nan + z_slice = u32(log(-view_z) * bindings::lights.cluster_factors.z - bindings::lights.cluster_factors.w + 1.0); + } + // NOTE: We use min as we may limit the far z plane used for clustering to be closer than + // the furthest thing being drawn. This means that we need to limit to the maximum cluster. + return min(z_slice, bindings::lights.cluster_dimensions.z - 1u); +} + +fn fragment_cluster_index(frag_coord: vec2, view_z: f32, is_orthographic: bool) -> u32 { + let xy = vec2(floor((frag_coord - bindings::view.viewport.xy) * bindings::lights.cluster_factors.xy)); + let z_slice = view_z_to_z_slice(view_z, is_orthographic); + // NOTE: Restricting cluster index to avoid undefined behavior when accessing uniform buffer + // arrays based on the cluster index. + return min( + (xy.y * bindings::lights.cluster_dimensions.x + xy.x) * bindings::lights.cluster_dimensions.z + z_slice, + bindings::lights.cluster_dimensions.w - 1u + ); +} + +// this must match CLUSTER_COUNT_SIZE in light.rs +const CLUSTER_COUNT_SIZE = 9u; + +// Returns the indices of clusterable objects belonging to the given cluster. +// +// Note that if fewer than 3 SSBO bindings are available (in WebGL 2, +// primarily), light probes aren't clustered, and therefore both light probe +// index ranges will be empty. +fn unpack_clusterable_object_index_ranges(cluster_index: u32) -> ClusterableObjectIndexRanges { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + + let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0]; + let offset_and_counts_b = bindings::cluster_offsets_and_counts.data[cluster_index][1]; + + // Sum up the counts to produce the range brackets. + // + // We could have stored the range brackets in `cluster_offsets_and_counts` + // directly, but doing it this way makes the logic in this path more + // consistent with the WebGL 2 path below. + let point_light_offset = offset_and_counts_a.x; + let spot_light_offset = point_light_offset + offset_and_counts_a.y; + let reflection_probe_offset = spot_light_offset + offset_and_counts_a.z; + let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_a.w; + let decal_offset = irradiance_volume_offset + offset_and_counts_b.x; + let last_clusterable_offset = decal_offset + offset_and_counts_b.y; + return ClusterableObjectIndexRanges( + point_light_offset, + spot_light_offset, + reflection_probe_offset, + irradiance_volume_offset, + decal_offset, + last_clusterable_offset + ); + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + + let raw_offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; + // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] + // [ offset | point light count | spot light count ] + let offset_and_counts = vec3( + (raw_offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), + (raw_offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u), + raw_offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u), + ); + + // We don't cluster reflection probes or irradiance volumes on this + // platform, as there's no room in the UBO. Thus, those offset ranges + // (corresponding to `offset_d` and `offset_e` above) are empty and are + // simply copies of `offset_c`. + + let offset_a = offset_and_counts.x; + let offset_b = offset_a + offset_and_counts.y; + let offset_c = offset_b + offset_and_counts.z; + + return ClusterableObjectIndexRanges(offset_a, offset_b, offset_c, offset_c, offset_c, offset_c); + +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +} + +// Returns the index of the clusterable object at the given offset. +// +// Note that, in the case of a light probe, the index refers to an element in +// one of the two `light_probes` sublists, not the `clusterable_objects` list. +fn get_clusterable_object_id(index: u32) -> u32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + return bindings::clusterable_object_index_lists.data[index]; +#else + // The index is correct but in clusterable_object_index_lists we pack 4 u8s into a u32 + // This means the index into clusterable_object_index_lists is index / 4 + let indices = bindings::clusterable_object_index_lists.data[index >> 4u][(index >> 2u) & + ((1u << 2u) - 1u)]; + // And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index + return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u); +#endif +} + +fn cluster_debug_visualization( + input_color: vec4, + view_z: f32, + is_orthographic: bool, + clusterable_object_index_ranges: ClusterableObjectIndexRanges, + cluster_index: u32, +) -> vec4 { + var output_color = input_color; + + // Cluster allocation debug (using 'over' alpha blending) +#ifdef CLUSTERED_FORWARD_DEBUG_Z_SLICES + // NOTE: This debug mode visualizes the z-slices + let cluster_overlay_alpha = 0.1; + var z_slice: u32 = view_z_to_z_slice(view_z, is_orthographic); + // A hack to make the colors alternate a bit more + if (z_slice & 1u) == 1u { + z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u; + } + let slice_color_hsv = vec3( + f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u) * PI_2, + 1.0, + 0.5 + ); + let slice_color = hsv_to_rgb(slice_color_hsv); + output_color = vec4( + (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color, + output_color.a + ); +#endif // CLUSTERED_FORWARD_DEBUG_Z_SLICES +#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY + // NOTE: This debug mode visualizes the number of clusterable objects within + // the cluster that contains the fragment. It shows a sort of cluster + // complexity measure. + let cluster_overlay_alpha = 0.1; + let max_complexity_per_cluster = 64.0; + let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset - + clusterable_object_index_ranges.first_point_light_index_offset; + output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha * + smoothstep(0.0, max_complexity_per_cluster, f32(object_count)); + output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha * + (1.0 - smoothstep(0.0, max_complexity_per_cluster, f32(object_count))); +#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY +#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY + // NOTE: Visualizes the cluster to which the fragment belongs + let cluster_overlay_alpha = 0.1; + var rng = cluster_index; + let cluster_color_hsv = vec3(rand_f(&rng) * PI_2, 1.0, 0.5); + let cluster_color = hsv_to_rgb(cluster_color_hsv); + output_color = vec4( + (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color, + output_color.a + ); +#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY + + return output_color; +} diff --git a/vendor/bevy_pbr/src/render/fog.rs b/vendor/bevy_pbr/src/render/fog.rs new file mode 100644 index 00000000..b532c346 --- /dev/null +++ b/vendor/bevy_pbr/src/render/fog.rs @@ -0,0 +1,144 @@ +use bevy_app::{App, Plugin}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::prelude::*; +use bevy_math::{Vec3, Vec4}; +use bevy_render::{ + extract_component::ExtractComponentPlugin, + render_resource::{DynamicUniformBuffer, ShaderType}, + renderer::{RenderDevice, RenderQueue}, + view::ExtractedView, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; + +use crate::{DistanceFog, FogFalloff}; + +/// The GPU-side representation of the fog configuration that's sent as a uniform to the shader +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuFog { + /// Fog color + base_color: Vec4, + /// The color used for the fog where the view direction aligns with directional lights + directional_light_color: Vec4, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + be: Vec3, + /// The exponent applied to the directional light alignment calculation + directional_light_exponent: f32, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + bi: Vec3, + /// Unsigned int representation of the active fog falloff mode + mode: u32, +} + +// Important: These must be kept in sync with `mesh_view_types.wgsl` +const GPU_FOG_MODE_OFF: u32 = 0; +const GPU_FOG_MODE_LINEAR: u32 = 1; +const GPU_FOG_MODE_EXPONENTIAL: u32 = 2; +const GPU_FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3; +const GPU_FOG_MODE_ATMOSPHERIC: u32 = 4; + +/// Metadata for fog +#[derive(Default, Resource)] +pub struct FogMeta { + pub gpu_fogs: DynamicUniformBuffer, +} + +/// Prepares fog metadata and writes the fog-related uniform buffers to the GPU +pub fn prepare_fog( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut fog_meta: ResMut, + views: Query<(Entity, Option<&DistanceFog>), With>, +) { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = fog_meta + .gpu_fogs + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + for (entity, fog) in views_iter { + let gpu_fog = if let Some(fog) = fog { + match &fog.falloff { + FogFalloff::Linear { start, end } => GpuFog { + mode: GPU_FOG_MODE_LINEAR, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*start, *end, 0.0), + ..Default::default() + }, + FogFalloff::Exponential { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::ExponentialSquared { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::Atmospheric { + extinction, + inscattering, + } => GpuFog { + mode: GPU_FOG_MODE_ATMOSPHERIC, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: *extinction, + bi: *inscattering, + }, + } + } else { + // If no fog is added to a camera, by default it's off + GpuFog { + mode: GPU_FOG_MODE_OFF, + ..Default::default() + } + }; + + // This is later read by `SetMeshViewBindGroup` + commands.entity(entity).insert(ViewFogUniformOffset { + offset: writer.write(&gpu_fog), + }); + } +} + +/// Inserted on each `Entity` with an `ExtractedView` to keep track of its offset +/// in the `gpu_fogs` `DynamicUniformBuffer` within `FogMeta` +#[derive(Component)] +pub struct ViewFogUniformOffset { + pub offset: u32, +} + +/// A plugin that consolidates fog extraction, preparation and related resources/assets +pub struct FogPlugin; + +impl Plugin for FogPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "fog.wgsl"); + + app.add_plugins(ExtractComponentPlugin::::default()); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .add_systems(Render, prepare_fog.in_set(RenderSystems::PrepareResources)); + } + } +} diff --git a/vendor/bevy_pbr/src/render/fog.wgsl b/vendor/bevy_pbr/src/render/fog.wgsl new file mode 100644 index 00000000..a9e28ae9 --- /dev/null +++ b/vendor/bevy_pbr/src/render/fog.wgsl @@ -0,0 +1,79 @@ +#define_import_path bevy_pbr::fog + +#import bevy_pbr::{ + mesh_view_bindings::fog, + mesh_view_types::Fog, +} + +// Fog formulas adapted from: +// https://learn.microsoft.com/en-us/windows/win32/direct3d9/fog-formulas +// https://catlikecoding.com/unity/tutorials/rendering/part-14/ +// https://iquilezles.org/articles/fog/ (Atmospheric Fog and Scattering) + +fn scattering_adjusted_fog_color( + fog_params: Fog, + scattering: vec3, +) -> vec4 { + if (fog_params.directional_light_color.a > 0.0) { + return vec4( + fog_params.base_color.rgb + + scattering * fog_params.directional_light_color.rgb * fog_params.directional_light_color.a, + fog_params.base_color.a, + ); + } else { + return fog_params.base_color; + } +} + +fn linear_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let start = fog_params.be.x; + let end = fog_params.be.y; + fog_color.a *= 1.0 - clamp((end - distance) / (end - start), 0.0, 1.0); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let density = fog_params.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance * density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_squared_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let distance_times_density = distance * fog_params.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance_times_density * distance_times_density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn atmospheric_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let extinction_factor = 1.0 - 1.0 / exp(distance * fog_params.be); + let inscattering_factor = 1.0 - 1.0 / exp(distance * fog_params.bi); + return vec4( + input_color.rgb * (1.0 - extinction_factor * fog_color.a) + + fog_color.rgb * inscattering_factor * fog_color.a, + input_color.a + ); +} diff --git a/vendor/bevy_pbr/src/render/forward_io.wgsl b/vendor/bevy_pbr/src/render/forward_io.wgsl new file mode 100644 index 00000000..99f2ecce --- /dev/null +++ b/vendor/bevy_pbr/src/render/forward_io.wgsl @@ -0,0 +1,60 @@ +#define_import_path bevy_pbr::forward_io + +struct Vertex { + @builtin(instance_index) instance_index: u32, +#ifdef VERTEX_POSITIONS + @location(0) position: vec3, +#endif +#ifdef VERTEX_NORMALS + @location(1) normal: vec3, +#endif +#ifdef VERTEX_UVS_A + @location(2) uv: vec2, +#endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(5) color: vec4, +#endif +#ifdef SKINNED + @location(6) joint_indices: vec4, + @location(7) joint_weights: vec4, +#endif +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif +}; + +struct VertexOutput { + // This is `clip position` when the struct is used as a vertex stage output + // and `frag coord` when used as a fragment stage input + @builtin(position) position: vec4, + @location(0) world_position: vec4, + @location(1) world_normal: vec3, +#ifdef VERTEX_UVS_A + @location(2) uv: vec2, +#endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(4) world_tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(5) color: vec4, +#endif +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(6) @interpolate(flat) instance_index: u32, +#endif +#ifdef VISIBILITY_RANGE_DITHER + @location(7) @interpolate(flat) visibility_range_dither: i32, +#endif +} + +struct FragmentOutput { + @location(0) color: vec4, +} diff --git a/vendor/bevy_pbr/src/render/gpu_preprocess.rs b/vendor/bevy_pbr/src/render/gpu_preprocess.rs new file mode 100644 index 00000000..9507f615 --- /dev/null +++ b/vendor/bevy_pbr/src/render/gpu_preprocess.rs @@ -0,0 +1,2729 @@ +//! GPU mesh preprocessing. +//! +//! This is an optional pass that uses a compute shader to reduce the amount of +//! data that has to be transferred from the CPU to the GPU. When enabled, +//! instead of transferring [`MeshUniform`]s to the GPU, we transfer the smaller +//! [`MeshInputUniform`]s instead and use the GPU to calculate the remaining +//! derived fields in [`MeshUniform`]. + +use core::num::{NonZero, NonZeroU64}; + +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + experimental::mip_generation::ViewDepthPyramid, + prepass::{DepthPrepass, PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + prelude::resource_exists, + query::{Has, Or, QueryState, With, Without}, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_log::warn_once; +use bevy_render::{ + batching::gpu_preprocessing::{ + BatchedInstanceBuffers, GpuOcclusionCullingWorkItemBuffers, GpuPreprocessingMode, + GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers, + IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, IndirectParametersIndexed, + IndirectParametersNonIndexed, LatePreprocessWorkItemIndirectParameters, PreprocessWorkItem, + PreprocessWorkItemBuffers, UntypedPhaseBatchedInstanceBuffers, + UntypedPhaseIndirectParametersBuffers, + }, + diagnostic::RecordDiagnostics, + experimental::occlusion_culling::OcclusionCulling, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, + render_resource::{ + binding_types::{storage_buffer, storage_buffer_read_only, texture_2d, uniform_buffer}, + BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindingResource, Buffer, + BufferBinding, CachedComputePipelineId, ComputePassDescriptor, ComputePipelineDescriptor, + DynamicBindGroupLayoutEntries, PipelineCache, PushConstantRange, RawBufferVec, + ShaderStages, ShaderType, SpecializedComputePipeline, SpecializedComputePipelines, + TextureSampleType, UninitBufferVec, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + settings::WgpuFeatures, + view::{ExtractedView, NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::Shader; +use bevy_utils::{default, TypeIdMap}; +use bitflags::bitflags; +use smallvec::{smallvec, SmallVec}; +use tracing::warn; + +use crate::{ + graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform, +}; + +use super::{ShadowView, ViewLightEntities}; + +/// The GPU workgroup size. +const WORKGROUP_SIZE: usize = 64; + +/// A plugin that builds mesh uniforms on GPU. +/// +/// This will only be added if the platform supports compute shaders (e.g. not +/// on WebGL 2). +pub struct GpuMeshPreprocessPlugin { + /// Whether we're building [`MeshUniform`]s on GPU. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, +} + +/// The render node that clears out the GPU-side indirect metadata buffers. +/// +/// This is only used when indirect drawing is enabled. +#[derive(Default)] +pub struct ClearIndirectParametersMetadataNode; + +/// The render node for the first mesh preprocessing pass. +/// +/// This pass runs a compute shader to cull meshes outside the view frustum (if +/// that wasn't done by the CPU), cull meshes that weren't visible last frame +/// (if occlusion culling is on), transform them, and, if indirect drawing is +/// on, populate indirect draw parameter metadata for the subsequent +/// [`EarlyPrepassBuildIndirectParametersNode`]. +pub struct EarlyGpuPreprocessNode { + view_query: QueryState< + ( + Read, + Option>, + Option>, + Has, + Has, + ), + Without, + >, + main_view_query: QueryState>, +} + +/// The render node for the second mesh preprocessing pass. +/// +/// This pass runs a compute shader to cull meshes outside the view frustum (if +/// that wasn't done by the CPU), cull meshes that were neither visible last +/// frame nor visible this frame (if occlusion culling is on), transform them, +/// and, if indirect drawing is on, populate the indirect draw parameter +/// metadata for the subsequent [`LatePrepassBuildIndirectParametersNode`]. +pub struct LateGpuPreprocessNode { + view_query: QueryState< + ( + Read, + Read, + Read, + ), + ( + Without, + Without, + With, + With, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws the meshes visible from the previous frame. +/// +/// This node runs a compute shader on the output of the +/// [`EarlyGpuPreprocessNode`] in order to transform the +/// [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct EarlyPrepassBuildIndirectParametersNode { + view_query: QueryState< + Read, + ( + Without, + Without, + Or<(With, With)>, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws the meshes that are potentially visible on this frame but weren't +/// visible on the previous frame. +/// +/// This node runs a compute shader on the output of the +/// [`LateGpuPreprocessNode`] in order to transform the +/// [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct LatePrepassBuildIndirectParametersNode { + view_query: QueryState< + Read, + ( + Without, + Without, + Or<(With, With)>, + With, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws all meshes, both those that are newly-visible on this frame and those +/// that were visible last frame. +/// +/// This node runs a compute shader on the output of the +/// [`EarlyGpuPreprocessNode`] and [`LateGpuPreprocessNode`] in order to +/// transform the [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct MainBuildIndirectParametersNode { + view_query: QueryState< + Read, + (Without, Without), + >, +} + +/// The compute shader pipelines for the GPU mesh preprocessing and indirect +/// parameter building passes. +#[derive(Resource)] +pub struct PreprocessPipelines { + /// The pipeline used for CPU culling. This pipeline doesn't populate + /// indirect parameter metadata. + pub direct_preprocess: PreprocessPipeline, + /// The pipeline used for mesh preprocessing when GPU frustum culling is in + /// use, but occlusion culling isn't. + /// + /// This pipeline populates indirect parameter metadata. + pub gpu_frustum_culling_preprocess: PreprocessPipeline, + /// The pipeline used for the first phase of occlusion culling. + /// + /// This pipeline culls, transforms meshes, and populates indirect parameter + /// metadata. + pub early_gpu_occlusion_culling_preprocess: PreprocessPipeline, + /// The pipeline used for the second phase of occlusion culling. + /// + /// This pipeline culls, transforms meshes, and populates indirect parameter + /// metadata. + pub late_gpu_occlusion_culling_preprocess: PreprocessPipeline, + /// The pipeline that builds indirect draw parameters for indexed meshes, + /// when frustum culling is enabled but occlusion culling *isn't* enabled. + pub gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline, + /// The pipeline that builds indirect draw parameters for non-indexed + /// meshes, when frustum culling is enabled but occlusion culling *isn't* + /// enabled. + pub gpu_frustum_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline, + /// Compute shader pipelines for the early prepass phase that draws meshes + /// visible in the previous frame. + pub early_phase: PreprocessPhasePipelines, + /// Compute shader pipelines for the late prepass phase that draws meshes + /// that weren't visible in the previous frame, but became visible this + /// frame. + pub late_phase: PreprocessPhasePipelines, + /// Compute shader pipelines for the main color phase. + pub main_phase: PreprocessPhasePipelines, +} + +/// Compute shader pipelines for a specific phase: early, late, or main. +/// +/// The distinction between these phases is relevant for occlusion culling. +#[derive(Clone)] +pub struct PreprocessPhasePipelines { + /// The pipeline that resets the indirect draw counts used in + /// `multi_draw_indirect_count` to 0 in preparation for a new pass. + pub reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline, + /// The pipeline used for indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into indexed indirect + /// parameters. + pub gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline, + /// The pipeline used for non-indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into non-indexed + /// indirect parameters. + pub gpu_occlusion_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline, +} + +/// The pipeline for the GPU mesh preprocessing shader. +pub struct PreprocessPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayoutDescriptor, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +/// The pipeline for the batch set count reset shader. +/// +/// This shader resets the indirect batch set count to 0 for each view. It runs +/// in between every phase (early, late, and main). +#[derive(Clone)] +pub struct ResetIndirectBatchSetsPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayoutDescriptor, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +/// The pipeline for the indirect parameter building shader. +#[derive(Clone)] +pub struct BuildIndirectParametersPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayoutDescriptor, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +bitflags! { + /// Specifies variants of the mesh preprocessing shader. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct PreprocessPipelineKey: u8 { + /// Whether GPU frustum culling is in use. + /// + /// This `#define`'s `FRUSTUM_CULLING` in the shader. + const FRUSTUM_CULLING = 1; + /// Whether GPU two-phase occlusion culling is in use. + /// + /// This `#define`'s `OCCLUSION_CULLING` in the shader. + const OCCLUSION_CULLING = 2; + /// Whether this is the early phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `EARLY_PHASE` in the shader. + const EARLY_PHASE = 4; + } + + /// Specifies variants of the indirect parameter building shader. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct BuildIndirectParametersPipelineKey: u8 { + /// Whether the indirect parameter building shader is processing indexed + /// meshes (those that have index buffers). + /// + /// This defines `INDEXED` in the shader. + const INDEXED = 1; + /// Whether the GPU and driver supports `multi_draw_indirect_count`. + /// + /// This defines `MULTI_DRAW_INDIRECT_COUNT_SUPPORTED` in the shader. + const MULTI_DRAW_INDIRECT_COUNT_SUPPORTED = 2; + /// Whether GPU two-phase occlusion culling is in use. + /// + /// This `#define`'s `OCCLUSION_CULLING` in the shader. + const OCCLUSION_CULLING = 4; + /// Whether this is the early phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `EARLY_PHASE` in the shader. + const EARLY_PHASE = 8; + /// Whether this is the late phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `LATE_PHASE` in the shader. + const LATE_PHASE = 16; + /// Whether this is the phase that runs after the early and late phases, + /// and right before the main drawing logic, when GPU two-phase + /// occlusion culling is in use. + /// + /// This `#define`'s `MAIN_PHASE` in the shader. + const MAIN_PHASE = 32; + } +} + +/// The compute shader bind group for the mesh preprocessing pass for each +/// render phase. +/// +/// This goes on the view. It maps the [`core::any::TypeId`] of a render phase +/// (e.g. [`bevy_core_pipeline::core_3d::Opaque3d`]) to the +/// [`PhasePreprocessBindGroups`] for that phase. +#[derive(Component, Clone, Deref, DerefMut)] +pub struct PreprocessBindGroups(pub TypeIdMap); + +/// The compute shader bind group for the mesh preprocessing step for a single +/// render phase on a single view. +#[derive(Clone)] +pub enum PhasePreprocessBindGroups { + /// The bind group used for the single invocation of the compute shader when + /// indirect drawing is *not* being used. + /// + /// Because direct drawing doesn't require splitting the meshes into indexed + /// and non-indexed meshes, there's only one bind group in this case. + Direct(BindGroup), + + /// The bind groups used for the compute shader when indirect drawing is + /// being used, but occlusion culling isn't being used. + /// + /// Because indirect drawing requires splitting the meshes into indexed and + /// non-indexed meshes, there are two bind groups here. + IndirectFrustumCulling { + /// The bind group for indexed meshes. + indexed: Option, + /// The bind group for non-indexed meshes. + non_indexed: Option, + }, + + /// The bind groups used for the compute shader when indirect drawing is + /// being used, but occlusion culling isn't being used. + /// + /// Because indirect drawing requires splitting the meshes into indexed and + /// non-indexed meshes, and because occlusion culling requires splitting + /// this phase into early and late versions, there are four bind groups + /// here. + IndirectOcclusionCulling { + /// The bind group for indexed meshes during the early mesh + /// preprocessing phase. + early_indexed: Option, + /// The bind group for non-indexed meshes during the early mesh + /// preprocessing phase. + early_non_indexed: Option, + /// The bind group for indexed meshes during the late mesh preprocessing + /// phase. + late_indexed: Option, + /// The bind group for non-indexed meshes during the late mesh + /// preprocessing phase. + late_non_indexed: Option, + }, +} + +/// The bind groups for the compute shaders that reset indirect draw counts and +/// build indirect parameters. +/// +/// There's one set of bind group for each phase. Phases are keyed off their +/// [`core::any::TypeId`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct BuildIndirectParametersBindGroups(pub TypeIdMap); + +impl BuildIndirectParametersBindGroups { + /// Creates a new, empty [`BuildIndirectParametersBindGroups`] table. + pub fn new() -> BuildIndirectParametersBindGroups { + Self::default() + } +} + +/// The per-phase set of bind groups for the compute shaders that reset indirect +/// draw counts and build indirect parameters. +pub struct PhaseBuildIndirectParametersBindGroups { + /// The bind group for the `reset_indirect_batch_sets.wgsl` shader, for + /// indexed meshes. + reset_indexed_indirect_batch_sets: Option, + /// The bind group for the `reset_indirect_batch_sets.wgsl` shader, for + /// non-indexed meshes. + reset_non_indexed_indirect_batch_sets: Option, + /// The bind group for the `build_indirect_params.wgsl` shader, for indexed + /// meshes. + build_indexed_indirect: Option, + /// The bind group for the `build_indirect_params.wgsl` shader, for + /// non-indexed meshes. + build_non_indexed_indirect: Option, +} + +/// Stops the `GpuPreprocessNode` attempting to generate the buffer for this view +/// useful to avoid duplicating effort if the bind group is shared between views +#[derive(Component, Default)] +pub struct SkipGpuPreprocess; + +impl Plugin for GpuMeshPreprocessPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "mesh_preprocess.wgsl"); + embedded_asset!(app, "reset_indirect_batch_sets.wgsl"); + embedded_asset!(app, "build_indirect_params.wgsl"); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // This plugin does nothing if GPU instance buffer building isn't in + // use. + let gpu_preprocessing_support = render_app.world().resource::(); + if !self.use_gpu_instance_buffer_builder || !gpu_preprocessing_support.is_available() { + return; + } + + render_app + .init_resource::() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + Render, + ( + prepare_preprocess_pipelines.in_set(RenderSystems::Prepare), + prepare_preprocess_bind_groups + .run_if(resource_exists::>) + .in_set(RenderSystems::PrepareBindGroups), + write_mesh_culling_data_buffer.in_set(RenderSystems::PrepareResourcesFlush), + ), + ) + .add_render_graph_node::( + Core3d, + NodePbr::ClearIndirectParametersMetadata + ) + .add_render_graph_node::(Core3d, NodePbr::EarlyGpuPreprocess) + .add_render_graph_node::(Core3d, NodePbr::LateGpuPreprocess) + .add_render_graph_node::( + Core3d, + NodePbr::EarlyPrepassBuildIndirectParameters, + ) + .add_render_graph_node::( + Core3d, + NodePbr::LatePrepassBuildIndirectParameters, + ) + .add_render_graph_node::( + Core3d, + NodePbr::MainBuildIndirectParameters, + ) + .add_render_graph_edges( + Core3d, + ( + NodePbr::ClearIndirectParametersMetadata, + NodePbr::EarlyGpuPreprocess, + NodePbr::EarlyPrepassBuildIndirectParameters, + Node3d::EarlyPrepass, + Node3d::EarlyDeferredPrepass, + Node3d::EarlyDownsampleDepth, + NodePbr::LateGpuPreprocess, + NodePbr::LatePrepassBuildIndirectParameters, + Node3d::LatePrepass, + Node3d::LateDeferredPrepass, + NodePbr::MainBuildIndirectParameters, + Node3d::StartMainPass, + ), + ).add_render_graph_edges( + Core3d, + ( + NodePbr::EarlyPrepassBuildIndirectParameters, + NodePbr::EarlyShadowPass, + Node3d::EarlyDownsampleDepth, + ) + ).add_render_graph_edges( + Core3d, + ( + NodePbr::LatePrepassBuildIndirectParameters, + NodePbr::LateShadowPass, + NodePbr::MainBuildIndirectParameters, + ) + ); + } +} + +impl Node for ClearIndirectParametersMetadataNode { + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(indirect_parameters_buffers) = world.get_resource::() + else { + return Ok(()); + }; + + // Clear out each indexed and non-indexed GPU-side buffer. + for phase_indirect_parameters_buffers in indirect_parameters_buffers.values() { + if let Some(indexed_gpu_metadata_buffer) = phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer() + { + render_context.command_encoder().clear_buffer( + indexed_gpu_metadata_buffer, + 0, + Some( + phase_indirect_parameters_buffers.indexed.batch_count() as u64 + * size_of::() as u64, + ), + ); + } + + if let Some(non_indexed_gpu_metadata_buffer) = phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer() + { + render_context.command_encoder().clear_buffer( + non_indexed_gpu_metadata_buffer, + 0, + Some( + phase_indirect_parameters_buffers.non_indexed.batch_count() as u64 + * size_of::() as u64, + ), + ); + } + } + + Ok(()) + } +} + +impl FromWorld for EarlyGpuPreprocessNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + main_view_query: QueryState::new(world), + } + } +} + +impl Node for EarlyGpuPreprocessNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + self.main_view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + + // Grab the [`BatchedInstanceBuffers`]. + let batched_instance_buffers = + world.resource::>(); + + let pipeline_cache = world.resource::(); + let preprocess_pipelines = world.resource::(); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("early_mesh_preprocessing"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut compute_pass, "early_mesh_preprocessing"); + + let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); + all_views.push(graph.view_entity()); + if let Ok(shadow_cascade_views) = + self.main_view_query.get_manual(world, graph.view_entity()) + { + all_views.extend(shadow_cascade_views.lights.iter().copied()); + } + + // Run the compute passes. + + for view_entity in all_views { + let Ok(( + view, + bind_groups, + view_uniform_offset, + no_indirect_drawing, + occlusion_culling, + )) = self.view_query.get_manual(world, view_entity) + else { + continue; + }; + + let Some(bind_groups) = bind_groups else { + continue; + }; + let Some(view_uniform_offset) = view_uniform_offset else { + continue; + }; + + // Select the right pipeline, depending on whether GPU culling is in + // use. + let maybe_pipeline_id = if no_indirect_drawing { + preprocess_pipelines.direct_preprocess.pipeline_id + } else if occlusion_culling { + preprocess_pipelines + .early_gpu_occlusion_culling_preprocess + .pipeline_id + } else { + preprocess_pipelines + .gpu_frustum_culling_preprocess + .pipeline_id + }; + + // Fetch the pipeline. + let Some(preprocess_pipeline_id) = maybe_pipeline_id else { + warn!("The build mesh uniforms pipeline wasn't ready"); + continue; + }; + + let Some(preprocess_pipeline) = + pipeline_cache.get_compute_pipeline(preprocess_pipeline_id) + else { + // This will happen while the pipeline is being compiled and is fine. + continue; + }; + + compute_pass.set_pipeline(preprocess_pipeline); + + // Loop over each render phase. + for (phase_type_id, batched_phase_instance_buffers) in + &batched_instance_buffers.phase_instance_buffers + { + // Grab the work item buffers for this view. + let Some(work_item_buffers) = batched_phase_instance_buffers + .work_item_buffers + .get(&view.retained_view_entity) + else { + continue; + }; + + // Fetch the bind group for the render phase. + let Some(phase_bind_groups) = bind_groups.get(phase_type_id) else { + continue; + }; + + // Make sure the mesh preprocessing shader has access to the + // view info it needs to do culling and motion vector + // computation. + let dynamic_offsets = [view_uniform_offset.offset]; + + // Are we drawing directly or indirectly? + match *phase_bind_groups { + PhasePreprocessBindGroups::Direct(ref bind_group) => { + // Invoke the mesh preprocessing shader to transform + // meshes only, but not cull. + let PreprocessWorkItemBuffers::Direct(work_item_buffer) = work_item_buffers + else { + continue; + }; + compute_pass.set_bind_group(0, bind_group, &dynamic_offsets); + let workgroup_count = work_item_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + PhasePreprocessBindGroups::IndirectFrustumCulling { + indexed: ref maybe_indexed_bind_group, + non_indexed: ref maybe_non_indexed_bind_group, + } + | PhasePreprocessBindGroups::IndirectOcclusionCulling { + early_indexed: ref maybe_indexed_bind_group, + early_non_indexed: ref maybe_non_indexed_bind_group, + .. + } => { + // Invoke the mesh preprocessing shader to transform and + // cull the meshes. + let PreprocessWorkItemBuffers::Indirect { + indexed: indexed_buffer, + non_indexed: non_indexed_buffer, + .. + } = work_item_buffers + else { + continue; + }; + + // Transform and cull indexed meshes if there are any. + if let Some(indexed_bind_group) = maybe_indexed_bind_group { + if let PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_indexed_offset, + .. + }), + .. + } = *work_item_buffers + { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(&late_indirect_parameters_indexed_offset), + ); + } + + compute_pass.set_bind_group(0, indexed_bind_group, &dynamic_offsets); + let workgroup_count = indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Transform and cull non-indexed meshes if there are any. + if let Some(non_indexed_bind_group) = maybe_non_indexed_bind_group { + if let PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_non_indexed_offset, + .. + }), + .. + } = *work_item_buffers + { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of( + &late_indirect_parameters_non_indexed_offset, + ), + ); + } + + compute_pass.set_bind_group( + 0, + non_indexed_bind_group, + &dynamic_offsets, + ); + let workgroup_count = non_indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + } + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) + } +} + +impl FromWorld for EarlyPrepassBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for LatePrepassBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for MainBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for LateGpuPreprocessNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for LateGpuPreprocessNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + + // Grab the [`BatchedInstanceBuffers`]. + let batched_instance_buffers = + world.resource::>(); + + let pipeline_cache = world.resource::(); + let preprocess_pipelines = world.resource::(); + + let maybe_pipeline_id = preprocess_pipelines + .late_gpu_occlusion_culling_preprocess + .pipeline_id; + + // Fetch the pipeline. + let Some(preprocess_pipeline_id) = maybe_pipeline_id else { + warn_once!("The build mesh uniforms pipeline wasn't ready"); + return Ok(()); + }; + + let Some(preprocess_pipeline) = pipeline_cache.get_compute_pipeline(preprocess_pipeline_id) + else { + // This will happen while the pipeline is being compiled and is fine. + return Ok(()); + }; + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("late_mesh_preprocessing"), + timestamp_writes: None, + }); + + let pass_span = diagnostics.pass_span(&mut compute_pass, "late_mesh_preprocessing"); + + // Run the compute passes. + for (view, bind_groups, view_uniform_offset) in self.view_query.iter_manual(world) { + compute_pass.set_pipeline(preprocess_pipeline); + + // Loop over each phase. Because we built the phases in parallel, + // each phase has a separate set of instance buffers. + for (phase_type_id, batched_phase_instance_buffers) in + &batched_instance_buffers.phase_instance_buffers + { + let UntypedPhaseBatchedInstanceBuffers { + ref work_item_buffers, + ref late_indexed_indirect_parameters_buffer, + ref late_non_indexed_indirect_parameters_buffer, + .. + } = *batched_phase_instance_buffers; + + // Grab the work item buffers for this view. + let Some(phase_work_item_buffers) = + work_item_buffers.get(&view.retained_view_entity) + else { + continue; + }; + + let ( + PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_indexed_offset, + late_indirect_parameters_non_indexed_offset, + .. + }), + .. + }, + Some(PhasePreprocessBindGroups::IndirectOcclusionCulling { + late_indexed: maybe_late_indexed_bind_group, + late_non_indexed: maybe_late_non_indexed_bind_group, + .. + }), + Some(late_indexed_indirect_parameters_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) = ( + phase_work_item_buffers, + bind_groups.get(phase_type_id), + late_indexed_indirect_parameters_buffer.buffer(), + late_non_indexed_indirect_parameters_buffer.buffer(), + ) + else { + continue; + }; + + let mut dynamic_offsets: SmallVec<[u32; 1]> = smallvec![]; + dynamic_offsets.push(view_uniform_offset.offset); + + // If there's no space reserved for work items, then don't + // bother doing the dispatch, as there can't possibly be any + // meshes of the given class (indexed or non-indexed) in this + // phase. + + // Transform and cull indexed meshes if there are any. + if let Some(late_indexed_bind_group) = maybe_late_indexed_bind_group { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(late_indirect_parameters_indexed_offset), + ); + + compute_pass.set_bind_group(0, late_indexed_bind_group, &dynamic_offsets); + compute_pass.dispatch_workgroups_indirect( + late_indexed_indirect_parameters_buffer, + (*late_indirect_parameters_indexed_offset as u64) + * (size_of::() as u64), + ); + } + + // Transform and cull non-indexed meshes if there are any. + if let Some(late_non_indexed_bind_group) = maybe_late_non_indexed_bind_group { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(late_indirect_parameters_non_indexed_offset), + ); + + compute_pass.set_bind_group(0, late_non_indexed_bind_group, &dynamic_offsets); + compute_pass.dispatch_workgroups_indirect( + late_non_indexed_indirect_parameters_buffer, + (*late_indirect_parameters_non_indexed_offset as u64) + * (size_of::() as u64), + ); + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) + } +} + +impl Node for EarlyPrepassBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + // If there are no views with a depth prepass enabled, we don't need to + // run this. + if self.view_query.iter_manual(world).next().is_none() { + return Ok(()); + } + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.early_phase, + "early_prepass_indirect_parameters_building", + ) + } +} + +impl Node for LatePrepassBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + // If there are no views with occlusion culling enabled, we don't need + // to run this. + if self.view_query.iter_manual(world).next().is_none() { + return Ok(()); + } + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.late_phase, + "late_prepass_indirect_parameters_building", + ) + } +} + +impl Node for MainBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.main_phase, + "main_indirect_parameters_building", + ) + } +} + +fn run_build_indirect_parameters_node( + render_context: &mut RenderContext, + world: &World, + preprocess_phase_pipelines: &PreprocessPhasePipelines, + label: &'static str, +) -> Result<(), NodeRunError> { + let Some(build_indirect_params_bind_groups) = + world.get_resource::() + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let pipeline_cache = world.resource::(); + let indirect_parameters_buffers = world.resource::(); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut compute_pass, label); + + // Fetch the pipeline. + let ( + Some(reset_indirect_batch_sets_pipeline_id), + Some(build_indexed_indirect_params_pipeline_id), + Some(build_non_indexed_indirect_params_pipeline_id), + ) = ( + preprocess_phase_pipelines + .reset_indirect_batch_sets + .pipeline_id, + preprocess_phase_pipelines + .gpu_occlusion_culling_build_indexed_indirect_params + .pipeline_id, + preprocess_phase_pipelines + .gpu_occlusion_culling_build_non_indexed_indirect_params + .pipeline_id, + ) + else { + warn!("The build indirect parameters pipelines weren't ready"); + pass_span.end(&mut compute_pass); + return Ok(()); + }; + + let ( + Some(reset_indirect_batch_sets_pipeline), + Some(build_indexed_indirect_params_pipeline), + Some(build_non_indexed_indirect_params_pipeline), + ) = ( + pipeline_cache.get_compute_pipeline(reset_indirect_batch_sets_pipeline_id), + pipeline_cache.get_compute_pipeline(build_indexed_indirect_params_pipeline_id), + pipeline_cache.get_compute_pipeline(build_non_indexed_indirect_params_pipeline_id), + ) + else { + // This will happen while the pipeline is being compiled and is fine. + pass_span.end(&mut compute_pass); + return Ok(()); + }; + + // Loop over each phase. As each has as separate set of buffers, we need to + // build indirect parameters individually for each phase. + for (phase_type_id, phase_build_indirect_params_bind_groups) in + build_indirect_params_bind_groups.iter() + { + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffers.get(phase_type_id) + else { + continue; + }; + + // Build indexed indirect parameters. + if let ( + Some(reset_indexed_indirect_batch_sets_bind_group), + Some(build_indirect_indexed_params_bind_group), + ) = ( + &phase_build_indirect_params_bind_groups.reset_indexed_indirect_batch_sets, + &phase_build_indirect_params_bind_groups.build_indexed_indirect, + ) { + compute_pass.set_pipeline(reset_indirect_batch_sets_pipeline); + compute_pass.set_bind_group(0, reset_indexed_indirect_batch_sets_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .batch_set_count(true) + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + + compute_pass.set_pipeline(build_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_indexed_params_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .indexed + .batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Build non-indexed indirect parameters. + if let ( + Some(reset_non_indexed_indirect_batch_sets_bind_group), + Some(build_indirect_non_indexed_params_bind_group), + ) = ( + &phase_build_indirect_params_bind_groups.reset_non_indexed_indirect_batch_sets, + &phase_build_indirect_params_bind_groups.build_non_indexed_indirect, + ) { + compute_pass.set_pipeline(reset_indirect_batch_sets_pipeline); + compute_pass.set_bind_group(0, reset_non_indexed_indirect_batch_sets_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .batch_set_count(false) + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + + compute_pass.set_pipeline(build_non_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_non_indexed_params_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .non_indexed + .batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) +} + +impl PreprocessPipelines { + /// Returns true if the preprocessing and indirect parameters pipelines have + /// been loaded or false otherwise. + pub(crate) fn pipelines_are_loaded( + &self, + pipeline_cache: &PipelineCache, + preprocessing_support: &GpuPreprocessingSupport, + ) -> bool { + match preprocessing_support.max_supported_mode { + GpuPreprocessingMode::None => false, + GpuPreprocessingMode::PreprocessingOnly => { + self.direct_preprocess.is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_preprocess + .is_loaded(pipeline_cache) + } + GpuPreprocessingMode::Culling => { + self.direct_preprocess.is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_preprocess + .is_loaded(pipeline_cache) + && self + .early_gpu_occlusion_culling_preprocess + .is_loaded(pipeline_cache) + && self + .late_gpu_occlusion_culling_preprocess + .is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_build_indexed_indirect_params + .is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_build_non_indexed_indirect_params + .is_loaded(pipeline_cache) + && self.early_phase.is_loaded(pipeline_cache) + && self.late_phase.is_loaded(pipeline_cache) + && self.main_phase.is_loaded(pipeline_cache) + } + } + } +} + +impl PreprocessPhasePipelines { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.reset_indirect_batch_sets.is_loaded(pipeline_cache) + && self + .gpu_occlusion_culling_build_indexed_indirect_params + .is_loaded(pipeline_cache) + && self + .gpu_occlusion_culling_build_non_indexed_indirect_params + .is_loaded(pipeline_cache) + } +} + +impl PreprocessPipeline { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl ResetIndirectBatchSetsPipeline { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl BuildIndirectParametersPipeline { + /// Returns true if this pipeline has been loaded into the pipeline cache or + /// false otherwise. + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl SpecializedComputePipeline for PreprocessPipeline { + type Key = PreprocessPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec!["WRITE_INDIRECT_PARAMETERS_METADATA".into()]; + if key.contains(PreprocessPipelineKey::FRUSTUM_CULLING) { + shader_defs.push("INDIRECT".into()); + shader_defs.push("FRUSTUM_CULLING".into()); + } + if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + shader_defs.push("OCCLUSION_CULLING".into()); + if key.contains(PreprocessPipelineKey::EARLY_PHASE) { + shader_defs.push("EARLY_PHASE".into()); + } else { + shader_defs.push("LATE_PHASE".into()); + } + } + + ComputePipelineDescriptor { + label: Some( + format!( + "mesh preprocessing ({})", + if key.contains( + PreprocessPipelineKey::OCCLUSION_CULLING + | PreprocessPipelineKey::EARLY_PHASE + ) { + "early GPU occlusion culling" + } else if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + "late GPU occlusion culling" + } else if key.contains(PreprocessPipelineKey::FRUSTUM_CULLING) { + "GPU frustum culling" + } else { + "direct" + } + ) + .into(), + ), + layout: vec![self.bind_group_layout.clone()], + push_constant_ranges: if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }] + } else { + vec![] + }, + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +impl FromWorld for PreprocessPipelines { + fn from_world(world: &mut World) -> Self { + // GPU culling bind group parameters are a superset of those in the CPU + // culling (direct) shader. + let direct_bind_group_layout_entries = preprocess_direct_bind_group_layout_entries(); + let gpu_frustum_culling_bind_group_layout_entries = gpu_culling_bind_group_layout_entries(); + let gpu_early_occlusion_culling_bind_group_layout_entries = + gpu_occlusion_culling_bind_group_layout_entries().extend_with_indices((( + 11, + storage_buffer::(/*has_dynamic_offset=*/ false), + ),)); + let gpu_late_occlusion_culling_bind_group_layout_entries = + gpu_occlusion_culling_bind_group_layout_entries(); + + let reset_indirect_batch_sets_bind_group_layout_entries = + DynamicBindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + (storage_buffer::(false),), + ); + + // Indexed and non-indexed bind group parameters share all the bind + // group layout entries except the final one. + let build_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + let build_non_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + + // Create the bind group layouts. + let direct_bind_group_layout = BindGroupLayoutDescriptor::new( + "build mesh uniforms direct bind group layout", + &direct_bind_group_layout_entries, + ); + let gpu_frustum_culling_bind_group_layout = BindGroupLayoutDescriptor::new( + "build mesh uniforms GPU frustum culling bind group layout", + &gpu_frustum_culling_bind_group_layout_entries, + ); + let gpu_early_occlusion_culling_bind_group_layout = BindGroupLayoutDescriptor::new( + "build mesh uniforms GPU early occlusion culling bind group layout", + &gpu_early_occlusion_culling_bind_group_layout_entries, + ); + let gpu_late_occlusion_culling_bind_group_layout = BindGroupLayoutDescriptor::new( + "build mesh uniforms GPU late occlusion culling bind group layout", + &gpu_late_occlusion_culling_bind_group_layout_entries, + ); + let reset_indirect_batch_sets_bind_group_layout = BindGroupLayoutDescriptor::new( + "reset indirect batch sets bind group layout", + &reset_indirect_batch_sets_bind_group_layout_entries, + ); + let build_indexed_indirect_params_bind_group_layout = BindGroupLayoutDescriptor::new( + "build indexed indirect parameters bind group layout", + &build_indexed_indirect_params_bind_group_layout_entries, + ); + let build_non_indexed_indirect_params_bind_group_layout = BindGroupLayoutDescriptor::new( + "build non-indexed indirect parameters bind group layout", + &build_non_indexed_indirect_params_bind_group_layout_entries, + ); + + let preprocess_shader = load_embedded_asset!(world, "mesh_preprocess.wgsl"); + let reset_indirect_batch_sets_shader = + load_embedded_asset!(world, "reset_indirect_batch_sets.wgsl"); + let build_indirect_params_shader = + load_embedded_asset!(world, "build_indirect_params.wgsl"); + + let preprocess_phase_pipelines = PreprocessPhasePipelines { + reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline { + bind_group_layout: reset_indirect_batch_sets_bind_group_layout.clone(), + shader: reset_indirect_batch_sets_shader, + pipeline_id: None, + }, + gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + gpu_occlusion_culling_build_non_indexed_indirect_params: + BuildIndirectParametersPipeline { + bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + }; + + PreprocessPipelines { + direct_preprocess: PreprocessPipeline { + bind_group_layout: direct_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + gpu_frustum_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_frustum_culling_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + early_gpu_occlusion_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_early_occlusion_culling_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + late_gpu_occlusion_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_late_occlusion_culling_bind_group_layout, + shader: preprocess_shader, + pipeline_id: None, + }, + gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + gpu_frustum_culling_build_non_indexed_indirect_params: + BuildIndirectParametersPipeline { + bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader, + pipeline_id: None, + }, + early_phase: preprocess_phase_pipelines.clone(), + late_phase: preprocess_phase_pipelines.clone(), + main_phase: preprocess_phase_pipelines.clone(), + } + } +} + +fn preprocess_direct_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::COMPUTE, + ( + // `view` + ( + 0, + uniform_buffer::(/* has_dynamic_offset= */ true), + ), + // `current_input` + (3, storage_buffer_read_only::(false)), + // `previous_input` + (4, storage_buffer_read_only::(false)), + // `indices` + (5, storage_buffer_read_only::(false)), + // `output` + (6, storage_buffer::(false)), + ), + ) +} + +// Returns the first 4 bind group layout entries shared between all invocations +// of the indirect parameters building shader. +fn build_indirect_params_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::COMPUTE, + ( + (0, storage_buffer_read_only::(false)), + ( + 1, + storage_buffer_read_only::(false), + ), + ( + 2, + storage_buffer_read_only::(false), + ), + (3, storage_buffer::(false)), + ), + ) +} + +/// A system that specializes the `mesh_preprocess.wgsl` and +/// `build_indirect_params.wgsl` pipelines if necessary. +fn gpu_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + // GPU culling bind group parameters are a superset of those in the CPU + // culling (direct) shader. + preprocess_direct_bind_group_layout_entries().extend_with_indices(( + // `indirect_parameters_cpu_metadata` + ( + 7, + storage_buffer_read_only::( + /* has_dynamic_offset= */ false, + ), + ), + // `indirect_parameters_gpu_metadata` + ( + 8, + storage_buffer::(/* has_dynamic_offset= */ false), + ), + // `mesh_culling_data` + ( + 9, + storage_buffer_read_only::(/* has_dynamic_offset= */ false), + ), + )) +} + +fn gpu_occlusion_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + gpu_culling_bind_group_layout_entries().extend_with_indices(( + ( + 2, + uniform_buffer::(/*has_dynamic_offset=*/ false), + ), + ( + 10, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + ( + 12, + storage_buffer::( + /*has_dynamic_offset=*/ false, + ), + ), + )) +} + +/// A system that specializes the `mesh_preprocess.wgsl` pipelines if necessary. +pub fn prepare_preprocess_pipelines( + pipeline_cache: Res, + render_device: Res, + mut specialized_preprocess_pipelines: ResMut>, + mut specialized_reset_indirect_batch_sets_pipelines: ResMut< + SpecializedComputePipelines, + >, + mut specialized_build_indirect_parameters_pipelines: ResMut< + SpecializedComputePipelines, + >, + preprocess_pipelines: ResMut, + gpu_preprocessing_support: Res, +) { + let preprocess_pipelines = preprocess_pipelines.into_inner(); + + preprocess_pipelines.direct_preprocess.prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::empty(), + ); + preprocess_pipelines.gpu_frustum_culling_preprocess.prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING, + ); + + if gpu_preprocessing_support.is_culling_supported() { + preprocess_pipelines + .early_gpu_occlusion_culling_preprocess + .prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING + | PreprocessPipelineKey::OCCLUSION_CULLING + | PreprocessPipelineKey::EARLY_PHASE, + ); + preprocess_pipelines + .late_gpu_occlusion_culling_preprocess + .prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING | PreprocessPipelineKey::OCCLUSION_CULLING, + ); + } + + let mut build_indirect_parameters_pipeline_key = BuildIndirectParametersPipelineKey::empty(); + + // If the GPU and driver support `multi_draw_indirect_count`, tell the + // shader that. + if render_device + .wgpu_device() + .features() + .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT) + { + build_indirect_parameters_pipeline_key + .insert(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED); + } + + preprocess_pipelines + .gpu_frustum_culling_build_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key | BuildIndirectParametersPipelineKey::INDEXED, + ); + preprocess_pipelines + .gpu_frustum_culling_build_non_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key, + ); + + if !gpu_preprocessing_support.is_culling_supported() { + return; + } + + for (preprocess_phase_pipelines, build_indirect_parameters_phase_pipeline_key) in [ + ( + &mut preprocess_pipelines.early_phase, + BuildIndirectParametersPipelineKey::EARLY_PHASE, + ), + ( + &mut preprocess_pipelines.late_phase, + BuildIndirectParametersPipelineKey::LATE_PHASE, + ), + ( + &mut preprocess_pipelines.main_phase, + BuildIndirectParametersPipelineKey::MAIN_PHASE, + ), + ] { + preprocess_phase_pipelines + .reset_indirect_batch_sets + .prepare( + &pipeline_cache, + &mut specialized_reset_indirect_batch_sets_pipelines, + ); + preprocess_phase_pipelines + .gpu_occlusion_culling_build_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key + | build_indirect_parameters_phase_pipeline_key + | BuildIndirectParametersPipelineKey::INDEXED + | BuildIndirectParametersPipelineKey::OCCLUSION_CULLING, + ); + preprocess_phase_pipelines + .gpu_occlusion_culling_build_non_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key + | build_indirect_parameters_phase_pipeline_key + | BuildIndirectParametersPipelineKey::OCCLUSION_CULLING, + ); + } +} + +impl PreprocessPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + key: PreprocessPipelineKey, + ) { + if self.pipeline_id.is_some() { + return; + } + + let preprocess_pipeline_id = pipelines.specialize(pipeline_cache, self, key); + self.pipeline_id = Some(preprocess_pipeline_id); + } +} + +impl SpecializedComputePipeline for ResetIndirectBatchSetsPipeline { + type Key = (); + + fn specialize(&self, _: Self::Key) -> ComputePipelineDescriptor { + ComputePipelineDescriptor { + label: Some("reset indirect batch sets".into()), + layout: vec![self.bind_group_layout.clone()], + shader: self.shader.clone(), + ..default() + } + } +} + +impl SpecializedComputePipeline for BuildIndirectParametersPipeline { + type Key = BuildIndirectParametersPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec![]; + if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + shader_defs.push("INDEXED".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED) { + shader_defs.push("MULTI_DRAW_INDIRECT_COUNT_SUPPORTED".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::OCCLUSION_CULLING) { + shader_defs.push("OCCLUSION_CULLING".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::EARLY_PHASE) { + shader_defs.push("EARLY_PHASE".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::LATE_PHASE) { + shader_defs.push("LATE_PHASE".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::MAIN_PHASE) { + shader_defs.push("MAIN_PHASE".into()); + } + + let label = format!( + "{} build {}indexed indirect parameters", + if !key.contains(BuildIndirectParametersPipelineKey::OCCLUSION_CULLING) { + "frustum culling" + } else if key.contains(BuildIndirectParametersPipelineKey::EARLY_PHASE) { + "early occlusion culling" + } else if key.contains(BuildIndirectParametersPipelineKey::LATE_PHASE) { + "late occlusion culling" + } else { + "main occlusion culling" + }, + if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + "" + } else { + "non-" + } + ); + + ComputePipelineDescriptor { + label: Some(label.into()), + layout: vec![self.bind_group_layout.clone()], + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +impl ResetIndirectBatchSetsPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + ) { + if self.pipeline_id.is_some() { + return; + } + + let reset_indirect_batch_sets_pipeline_id = pipelines.specialize(pipeline_cache, self, ()); + self.pipeline_id = Some(reset_indirect_batch_sets_pipeline_id); + } +} + +impl BuildIndirectParametersPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + key: BuildIndirectParametersPipelineKey, + ) { + if self.pipeline_id.is_some() { + return; + } + + let build_indirect_parameters_pipeline_id = pipelines.specialize(pipeline_cache, self, key); + self.pipeline_id = Some(build_indirect_parameters_pipeline_id); + } +} + +/// A system that attaches the mesh uniform buffers to the bind groups for the +/// variants of the mesh preprocessing compute shader. +#[expect( + clippy::too_many_arguments, + reason = "it's a system that needs a lot of arguments" +)] +pub fn prepare_preprocess_bind_groups( + mut commands: Commands, + views: Query<(Entity, &ExtractedView)>, + view_depth_pyramids: Query<(&ViewDepthPyramid, &PreviousViewUniformOffset)>, + render_device: Res, + pipeline_cache: Res, + batched_instance_buffers: Res>, + indirect_parameters_buffers: Res, + mesh_culling_data_buffer: Res, + view_uniforms: Res, + previous_view_uniforms: Res, + pipelines: Res, +) { + // Grab the `BatchedInstanceBuffers`. + let BatchedInstanceBuffers { + current_input_buffer: current_input_buffer_vec, + previous_input_buffer: previous_input_buffer_vec, + phase_instance_buffers, + } = batched_instance_buffers.into_inner(); + + let (Some(current_input_buffer), Some(previous_input_buffer)) = ( + current_input_buffer_vec.buffer().buffer(), + previous_input_buffer_vec.buffer().buffer(), + ) else { + return; + }; + + // Record whether we have any meshes that are to be drawn indirectly. If we + // don't, then we can skip building indirect parameters. + let mut any_indirect = false; + + // Loop over each view. + for (view_entity, view) in &views { + let mut bind_groups = TypeIdMap::default(); + + // Loop over each phase. + for (phase_type_id, phase_instance_buffers) in phase_instance_buffers { + let UntypedPhaseBatchedInstanceBuffers { + data_buffer: ref data_buffer_vec, + ref work_item_buffers, + ref late_indexed_indirect_parameters_buffer, + ref late_non_indexed_indirect_parameters_buffer, + } = *phase_instance_buffers; + + let Some(data_buffer) = data_buffer_vec.buffer() else { + continue; + }; + + // Grab the indirect parameters buffers for this phase. + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffers.get(phase_type_id) + else { + continue; + }; + + let Some(work_item_buffers) = work_item_buffers.get(&view.retained_view_entity) else { + continue; + }; + + // Create the `PreprocessBindGroupBuilder`. + let preprocess_bind_group_builder = PreprocessBindGroupBuilder { + view: view_entity, + late_indexed_indirect_parameters_buffer, + late_non_indexed_indirect_parameters_buffer, + render_device: &render_device, + pipeline_cache: &pipeline_cache, + phase_indirect_parameters_buffers, + mesh_culling_data_buffer: &mesh_culling_data_buffer, + view_uniforms: &view_uniforms, + previous_view_uniforms: &previous_view_uniforms, + pipelines: &pipelines, + current_input_buffer, + previous_input_buffer, + data_buffer, + }; + + // Depending on the type of work items we have, construct the + // appropriate bind groups. + let (was_indirect, bind_group) = match *work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref work_item_buffer) => ( + false, + preprocess_bind_group_builder + .create_direct_preprocess_bind_groups(work_item_buffer), + ), + + PreprocessWorkItemBuffers::Indirect { + indexed: ref indexed_work_item_buffer, + non_indexed: ref non_indexed_work_item_buffer, + gpu_occlusion_culling: Some(ref gpu_occlusion_culling_work_item_buffers), + } => ( + true, + preprocess_bind_group_builder + .create_indirect_occlusion_culling_preprocess_bind_groups( + &view_depth_pyramids, + indexed_work_item_buffer, + non_indexed_work_item_buffer, + gpu_occlusion_culling_work_item_buffers, + ), + ), + + PreprocessWorkItemBuffers::Indirect { + indexed: ref indexed_work_item_buffer, + non_indexed: ref non_indexed_work_item_buffer, + gpu_occlusion_culling: None, + } => ( + true, + preprocess_bind_group_builder + .create_indirect_frustum_culling_preprocess_bind_groups( + indexed_work_item_buffer, + non_indexed_work_item_buffer, + ), + ), + }; + + // Write that bind group in. + if let Some(bind_group) = bind_group { + any_indirect = any_indirect || was_indirect; + bind_groups.insert(*phase_type_id, bind_group); + } + } + + // Save the bind groups. + commands + .entity(view_entity) + .insert(PreprocessBindGroups(bind_groups)); + } + + // Now, if there were any indirect draw commands, create the bind groups for + // the indirect parameters building shader. + if any_indirect { + create_build_indirect_parameters_bind_groups( + &mut commands, + &render_device, + &pipeline_cache, + &pipelines, + current_input_buffer, + &indirect_parameters_buffers, + ); + } +} + +/// A temporary structure that stores all the information needed to construct +/// bind groups for the mesh preprocessing shader. +struct PreprocessBindGroupBuilder<'a> { + /// The render-world entity corresponding to the current view. + view: Entity, + /// The indirect compute dispatch parameters buffer for indexed meshes in + /// the late prepass. + late_indexed_indirect_parameters_buffer: + &'a RawBufferVec, + /// The indirect compute dispatch parameters buffer for non-indexed meshes + /// in the late prepass. + late_non_indexed_indirect_parameters_buffer: + &'a RawBufferVec, + /// The device. + render_device: &'a RenderDevice, + /// The pipeline cache + pipeline_cache: &'a PipelineCache, + /// The buffers that store indirect draw parameters. + phase_indirect_parameters_buffers: &'a UntypedPhaseIndirectParametersBuffers, + /// The GPU buffer that stores the information needed to cull each mesh. + mesh_culling_data_buffer: &'a MeshCullingDataBuffer, + /// The GPU buffer that stores information about the view. + view_uniforms: &'a ViewUniforms, + /// The GPU buffer that stores information about the view from last frame. + previous_view_uniforms: &'a PreviousViewUniforms, + /// The pipelines for the mesh preprocessing shader. + pipelines: &'a PreprocessPipelines, + /// The GPU buffer containing the list of [`MeshInputUniform`]s for the + /// current frame. + current_input_buffer: &'a Buffer, + /// The GPU buffer containing the list of [`MeshInputUniform`]s for the + /// previous frame. + previous_input_buffer: &'a Buffer, + /// The GPU buffer containing the list of [`MeshUniform`]s for the current + /// frame. + /// + /// This is the buffer containing the mesh's final transforms that the + /// shaders will write to. + data_buffer: &'a Buffer, +} + +impl<'a> PreprocessBindGroupBuilder<'a> { + /// Creates the bind groups for mesh preprocessing when GPU frustum culling + /// and GPU occlusion culling are both disabled. + fn create_direct_preprocess_bind_groups( + &self, + work_item_buffer: &RawBufferVec, + ) -> Option { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let work_item_buffer_size = NonZero::::try_from( + work_item_buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some(PhasePreprocessBindGroups::Direct( + self.render_device.create_bind_group( + "preprocess_direct_bind_group", + &self + .pipeline_cache + .get_bind_group_layout(&self.pipelines.direct_preprocess.bind_group_layout), + &BindGroupEntries::with_indices(( + (0, self.view_uniforms.uniforms.binding()?), + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: work_item_buffer.buffer()?, + offset: 0, + size: work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + )), + ), + )) + } + + /// Creates the bind groups for mesh preprocessing when GPU occlusion + /// culling is enabled. + fn create_indirect_occlusion_culling_preprocess_bind_groups( + &self, + view_depth_pyramids: &Query<(&ViewDepthPyramid, &PreviousViewUniformOffset)>, + indexed_work_item_buffer: &RawBufferVec, + non_indexed_work_item_buffer: &RawBufferVec, + gpu_occlusion_culling_work_item_buffers: &GpuOcclusionCullingWorkItemBuffers, + ) -> Option { + let GpuOcclusionCullingWorkItemBuffers { + late_indexed: ref late_indexed_work_item_buffer, + late_non_indexed: ref late_non_indexed_work_item_buffer, + .. + } = *gpu_occlusion_culling_work_item_buffers; + + let (view_depth_pyramid, previous_view_uniform_offset) = + view_depth_pyramids.get(self.view).ok()?; + + Some(PhasePreprocessBindGroups::IndirectOcclusionCulling { + early_indexed: self.create_indirect_occlusion_culling_early_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + indexed_work_item_buffer, + late_indexed_work_item_buffer, + ), + + early_non_indexed: self.create_indirect_occlusion_culling_early_non_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + non_indexed_work_item_buffer, + late_non_indexed_work_item_buffer, + ), + + late_indexed: self.create_indirect_occlusion_culling_late_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + late_indexed_work_item_buffer, + ), + + late_non_indexed: self.create_indirect_occlusion_culling_late_non_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + late_non_indexed_work_item_buffer, + ), + }) + } + + /// Creates the bind group for the first phase of mesh preprocessing of + /// indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_early_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + indexed_work_item_buffer: &RawBufferVec, + late_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + indexed_work_item_buffer.buffer(), + late_indexed_work_item_buffer.buffer(), + self.late_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(indexed_work_item_gpu_buffer), + Some(late_indexed_work_item_gpu_buffer), + Some(late_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let indexed_work_item_buffer_size = NonZero::::try_from( + indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_early_indexed_gpu_occlusion_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .early_gpu_occlusion_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 11, + BufferBinding { + buffer: late_indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }, + ), + ( + 12, + BufferBinding { + buffer: late_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the first phase of mesh preprocessing of + /// non-indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_early_non_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + non_indexed_work_item_buffer: &RawBufferVec, + late_non_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + non_indexed_work_item_buffer.buffer(), + late_non_indexed_work_item_buffer.buffer(), + self.late_non_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + Some(late_non_indexed_work_item_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_early_non_indexed_gpu_occlusion_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .early_gpu_occlusion_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 11, + BufferBinding { + buffer: late_non_indexed_work_item_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }, + ), + ( + 12, + BufferBinding { + buffer: late_non_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_non_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the second phase of mesh preprocessing of + /// indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_late_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + late_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + late_indexed_work_item_buffer.buffer(), + self.late_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(late_indexed_work_item_gpu_buffer), + Some(late_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let late_indexed_work_item_buffer_size = NonZero::::try_from( + late_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_late_indexed_gpu_occlusion_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .late_gpu_occlusion_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: late_indexed_work_item_gpu_buffer, + offset: 0, + size: late_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 12, + BufferBinding { + buffer: late_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the second phase of mesh preprocessing of + /// non-indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_late_non_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + late_non_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + late_non_indexed_work_item_buffer.buffer(), + self.late_non_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + late_non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_late_non_indexed_gpu_occlusion_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .late_gpu_occlusion_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 12, + BufferBinding { + buffer: late_non_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_non_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind groups for mesh preprocessing when GPU frustum culling + /// is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_preprocess_bind_groups( + &self, + indexed_work_item_buffer: &RawBufferVec, + non_indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + Some(PhasePreprocessBindGroups::IndirectFrustumCulling { + indexed: self + .create_indirect_frustum_culling_indexed_bind_group(indexed_work_item_buffer), + non_indexed: self.create_indirect_frustum_culling_non_indexed_bind_group( + non_indexed_work_item_buffer, + ), + }) + } + + /// Creates the bind group for mesh preprocessing of indexed meshes when GPU + /// frustum culling is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_indexed_bind_group( + &self, + indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + indexed_work_item_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(indexed_work_item_gpu_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let indexed_work_item_buffer_size = NonZero::::try_from( + indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_gpu_indexed_frustum_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .gpu_frustum_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for mesh preprocessing of non-indexed meshes when + /// GPU frustum culling is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_non_indexed_bind_group( + &self, + non_indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + non_indexed_work_item_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_gpu_non_indexed_frustum_culling_bind_group", + &self.pipeline_cache.get_bind_group_layout( + &self + .pipelines + .gpu_frustum_culling_preprocess + .bind_group_layout, + ), + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + )), + ), + ) + } + _ => None, + } + } +} + +/// A system that creates bind groups from the indirect parameters metadata and +/// data buffers for the indirect batch set reset shader and the indirect +/// parameter building shader. +fn create_build_indirect_parameters_bind_groups( + commands: &mut Commands, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + pipelines: &PreprocessPipelines, + current_input_buffer: &Buffer, + indirect_parameters_buffers: &IndirectParametersBuffers, +) { + let mut build_indirect_parameters_bind_groups = BuildIndirectParametersBindGroups::new(); + + for (phase_type_id, phase_indirect_parameters_buffer) in indirect_parameters_buffers.iter() { + build_indirect_parameters_bind_groups.insert( + *phase_type_id, + PhaseBuildIndirectParametersBindGroups { + reset_indexed_indirect_batch_sets: match (phase_indirect_parameters_buffer + .indexed + .batch_sets_buffer(),) + { + (Some(indexed_batch_sets_buffer),) => Some( + render_device.create_bind_group( + "reset_indexed_indirect_batch_sets_bind_group", + // The early bind group is good for the main phase and late + // phase too. They bind the same buffers. + &pipeline_cache.get_bind_group_layout( + &pipelines + .early_phase + .reset_indirect_batch_sets + .bind_group_layout, + ), + &BindGroupEntries::sequential(( + indexed_batch_sets_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + reset_non_indexed_indirect_batch_sets: match (phase_indirect_parameters_buffer + .non_indexed + .batch_sets_buffer(),) + { + (Some(non_indexed_batch_sets_buffer),) => Some( + render_device.create_bind_group( + "reset_non_indexed_indirect_batch_sets_bind_group", + // The early bind group is good for the main phase and late + // phase too. They bind the same buffers. + &pipeline_cache.get_bind_group_layout( + &pipelines + .early_phase + .reset_indirect_batch_sets + .bind_group_layout, + ), + &BindGroupEntries::sequential(( + non_indexed_batch_sets_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + build_indexed_indirect: match ( + phase_indirect_parameters_buffer + .indexed + .cpu_metadata_buffer(), + phase_indirect_parameters_buffer + .indexed + .gpu_metadata_buffer(), + phase_indirect_parameters_buffer.indexed.data_buffer(), + phase_indirect_parameters_buffer.indexed.batch_sets_buffer(), + ) { + ( + Some(indexed_indirect_parameters_cpu_metadata_buffer), + Some(indexed_indirect_parameters_gpu_metadata_buffer), + Some(indexed_indirect_parameters_data_buffer), + Some(indexed_batch_sets_buffer), + ) => Some( + render_device.create_bind_group( + "build_indexed_indirect_parameters_bind_group", + // The frustum culling bind group is good for occlusion culling + // too. They bind the same buffers. + &pipeline_cache.get_bind_group_layout( + &pipelines + .gpu_frustum_culling_build_indexed_indirect_params + .bind_group_layout, + ), + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: indexed_indirect_parameters_cpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + BufferBinding { + buffer: indexed_indirect_parameters_gpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + indexed_batch_sets_buffer.as_entire_binding(), + indexed_indirect_parameters_data_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + build_non_indexed_indirect: match ( + phase_indirect_parameters_buffer + .non_indexed + .cpu_metadata_buffer(), + phase_indirect_parameters_buffer + .non_indexed + .gpu_metadata_buffer(), + phase_indirect_parameters_buffer.non_indexed.data_buffer(), + phase_indirect_parameters_buffer + .non_indexed + .batch_sets_buffer(), + ) { + ( + Some(non_indexed_indirect_parameters_cpu_metadata_buffer), + Some(non_indexed_indirect_parameters_gpu_metadata_buffer), + Some(non_indexed_indirect_parameters_data_buffer), + Some(non_indexed_batch_sets_buffer), + ) => Some( + render_device.create_bind_group( + "build_non_indexed_indirect_parameters_bind_group", + // The frustum culling bind group is good for occlusion culling + // too. They bind the same buffers. + &pipeline_cache.get_bind_group_layout( + &pipelines + .gpu_frustum_culling_build_non_indexed_indirect_params + .bind_group_layout, + ), + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: non_indexed_indirect_parameters_cpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.non_indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + BufferBinding { + buffer: non_indexed_indirect_parameters_gpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.non_indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + non_indexed_batch_sets_buffer.as_entire_binding(), + non_indexed_indirect_parameters_data_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + }, + ); + } + + commands.insert_resource(build_indirect_parameters_bind_groups); +} + +/// Writes the information needed to do GPU mesh culling to the GPU. +pub fn write_mesh_culling_data_buffer( + render_device: Res, + render_queue: Res, + mut mesh_culling_data_buffer: ResMut, +) { + mesh_culling_data_buffer.write_buffer(&render_device, &render_queue); +} diff --git a/vendor/bevy_pbr/src/render/light.rs b/vendor/bevy_pbr/src/render/light.rs new file mode 100644 index 00000000..9c95f071 --- /dev/null +++ b/vendor/bevy_pbr/src/render/light.rs @@ -0,0 +1,2374 @@ +use crate::*; +use bevy_asset::UntypedAssetId; +use bevy_camera::primitives::{ + face_index_to_name, CascadesFrusta, CubeMapFace, CubemapFrusta, Frustum, HalfSpace, + CUBE_MAP_FACES, +}; +use bevy_camera::visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, RenderLayers, ViewVisibility, + VisibleMeshEntities, +}; +use bevy_camera::Camera3d; +use bevy_color::ColorToComponents; +use bevy_core_pipeline::core_3d::CORE_3D_DEPTH_FORMAT; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::change_detection::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{ + entity::{EntityHashMap, EntityHashSet}, + prelude::*, + system::lifetimeless::Read, +}; +use bevy_light::cascade::Cascade; +use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectType}; +use bevy_light::cluster::GlobalVisibleClusterableObjects; +use bevy_light::SunDisk; +use bevy_light::{ + spot_light_clip_from_view, spot_light_world_from_view, AmbientLight, CascadeShadowConfig, + Cascades, DirectionalLight, DirectionalLightShadowMap, GlobalAmbientLight, NotShadowCaster, + PointLight, PointLightShadowMap, ShadowFilteringMethod, SpotLight, VolumetricLight, +}; +use bevy_math::{ops, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::hash::FixedHasher; +use bevy_render::erased_render_asset::ErasedRenderAssets; +use bevy_render::experimental::occlusion_culling::{ + OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, +}; +use bevy_render::sync_world::MainEntityHashMap; +use bevy_render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::SortedCameras, + mesh::allocator::MeshAllocator, + view::{NoIndirectDrawing, RetainedViewEntity}, +}; +use bevy_render::{ + diagnostic::RecordDiagnostics, + mesh::RenderMesh, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_phase::*, + render_resource::*, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::*, + view::ExtractedView, + Extract, +}; +use bevy_render::{ + mesh::allocator::SlabId, + sync_world::{MainEntity, RenderEntity}, +}; +use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_utils::default; +use core::{hash::Hash, ops::Range}; +use decal::clustered::RenderClusteredDecals; +#[cfg(feature = "trace")] +use tracing::info_span; +use tracing::{error, warn}; + +#[derive(Component)] +pub struct ExtractedPointLight { + pub color: LinearRgba, + /// luminous intensity in lumens per steradian + pub intensity: f32, + pub range: f32, + pub radius: f32, + pub transform: GlobalTransform, + pub shadows_enabled: bool, + pub shadow_depth_bias: f32, + pub shadow_normal_bias: f32, + pub shadow_map_near_z: f32, + pub spot_light_angles: Option<(f32, f32)>, + pub volumetric: bool, + pub soft_shadows_enabled: bool, + /// whether this point light contributes diffuse light to lightmapped meshes + pub affects_lightmapped_mesh_diffuse: bool, +} + +#[derive(Component, Debug)] +pub struct ExtractedDirectionalLight { + pub color: LinearRgba, + pub illuminance: f32, + pub transform: GlobalTransform, + pub shadows_enabled: bool, + pub volumetric: bool, + /// whether this directional light contributes diffuse light to lightmapped + /// meshes + pub affects_lightmapped_mesh_diffuse: bool, + pub shadow_depth_bias: f32, + pub shadow_normal_bias: f32, + pub cascade_shadow_config: CascadeShadowConfig, + pub cascades: EntityHashMap>, + pub frusta: EntityHashMap>, + pub render_layers: RenderLayers, + pub soft_shadow_size: Option, + /// True if this light is using two-phase occlusion culling. + pub occlusion_culling: bool, + pub sun_disk_angular_size: f32, + pub sun_disk_intensity: f32, +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + struct PointLightFlags: u32 { + const SHADOWS_ENABLED = 1 << 0; + const SPOT_LIGHT_Y_NEGATIVE = 1 << 1; + const VOLUMETRIC = 1 << 2; + const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 3; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuDirectionalCascade { + clip_from_world: Mat4, + texel_size: f32, + far_bound: f32, +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuDirectionalLight { + cascades: [GpuDirectionalCascade; MAX_CASCADES_PER_LIGHT], + color: Vec4, + dir_to_light: Vec3, + flags: u32, + soft_shadow_size: f32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, + depth_texture_base_index: u32, + decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + struct DirectionalLightFlags: u32 { + const SHADOWS_ENABLED = 1 << 0; + const VOLUMETRIC = 1 << 1; + const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 2; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +#[derive(Copy, Clone, Debug, ShaderType)] +pub struct GpuLights { + directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS], + ambient_color: Vec4, + // xyz are x/y/z cluster dimensions and w is the number of clusters + cluster_dimensions: UVec4, + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + cluster_factors: Vec4, + n_directional_lights: u32, + // offset from spot light's light index to spot light's shadow map index + spot_light_shadowmap_offset: i32, + ambient_light_affects_lightmapped_meshes: u32, +} + +// NOTE: When running bevy on Adreno GPU chipsets in WebGL, any value above 1 will result in a crash +// when loading the wgsl "pbr_functions.wgsl" in the function apply_fog. +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +pub const MAX_DIRECTIONAL_LIGHTS: usize = 1; +#[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" +))] +pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; +#[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" +))] +pub const MAX_CASCADES_PER_LIGHT: usize = 4; +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +pub const MAX_CASCADES_PER_LIGHT: usize = 1; + +#[derive(Resource, Clone)] +pub struct ShadowSamplers { + pub point_light_comparison_sampler: Sampler, + #[cfg(feature = "experimental_pbr_pcss")] + pub point_light_linear_sampler: Sampler, + pub directional_light_comparison_sampler: Sampler, + #[cfg(feature = "experimental_pbr_pcss")] + pub directional_light_linear_sampler: Sampler, +} + +pub fn init_shadow_samplers(mut commands: Commands, render_device: Res) { + let base_sampler_descriptor = SamplerDescriptor { + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..default() + }; + + commands.insert_resource(ShadowSamplers { + point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }), + #[cfg(feature = "experimental_pbr_pcss")] + point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + directional_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }), + #[cfg(feature = "experimental_pbr_pcss")] + directional_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + }); +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type ShadowFilteringMethod +pub fn extract_shadow_filtering_method( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, *query_item)); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractResource on foreign type AmbientLight +pub fn extract_ambient_light_resource( + mut commands: Commands, + main_resource: Extract>>, + target_resource: Option>, +) { + if let Some(main_resource) = main_resource.as_ref() { + if let Some(mut target_resource) = target_resource { + if main_resource.is_changed() { + *target_resource = (*main_resource).clone(); + } + } else { + commands.insert_resource((*main_resource).clone()); + } + } +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type AmbientLight +pub fn extract_ambient_light( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +pub fn extract_lights( + mut commands: Commands, + point_light_shadow_map: Extract>, + directional_light_shadow_map: Extract>, + global_visible_clusterable: Extract>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, + point_lights: Extract< + Query<( + Entity, + RenderEntity, + &PointLight, + &CubemapVisibleEntities, + &GlobalTransform, + &ViewVisibility, + &CubemapFrusta, + Option<&VolumetricLight>, + )>, + >, + spot_lights: Extract< + Query<( + Entity, + RenderEntity, + &SpotLight, + &VisibleMeshEntities, + &GlobalTransform, + &ViewVisibility, + &Frustum, + Option<&VolumetricLight>, + )>, + >, + directional_lights: Extract< + Query< + ( + Entity, + RenderEntity, + &DirectionalLight, + &CascadesVisibleEntities, + &Cascades, + &CascadeShadowConfig, + &CascadesFrusta, + &GlobalTransform, + &ViewVisibility, + Option<&RenderLayers>, + Option<&VolumetricLight>, + Has, + Option<&SunDisk>, + ), + Without, + >, + >, + mapper: Extract>, + mut previous_point_lights_len: Local, + mut previous_spot_lights_len: Local, +) { + // NOTE: These shadow map resources are extracted here as they are used here too so this avoids + // races between scheduling of ExtractResourceSystems and this system. + if point_light_shadow_map.is_changed() { + commands.insert_resource(point_light_shadow_map.clone()); + } + if directional_light_shadow_map.is_changed() { + commands.insert_resource(directional_light_shadow_map.clone()); + } + + // Clear previous visible entities for all point/spot lights as they might not be in the + // `global_visible_clusterable` list anymore. + commands.try_insert_batch( + previous_point_lights + .iter() + .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) + .collect::>(), + ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); + + // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 + // world unit from the light. + // point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels + // PI / 4.0 is half the cube face fov, tan(PI / 4.0) = 1.0, so this simplifies to: + // point_light_texel_size = 2.0 / cube face width in texels + // NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to: + // https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/ + let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; + + let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); + for entity in global_visible_clusterable.iter().copied() { + let Ok(( + main_entity, + render_entity, + point_light, + cubemap_visible_entities, + transform, + view_visibility, + frusta, + volumetric_light, + )) = point_lights.get(entity) + else { + continue; + }; + if !view_visibility.get() { + continue; + } + let render_cubemap_visible_entities = RenderCubemapVisibleEntities { + data: cubemap_visible_entities + .iter() + .map(|v| create_render_visible_mesh_entities(&mapper, v)) + .collect::>() + .try_into() + .unwrap(), + }; + + let extracted_point_light = ExtractedPointLight { + color: point_light.color.into(), + // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian + // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + // for details. + intensity: point_light.intensity / (4.0 * core::f32::consts::PI), + range: point_light.range, + radius: point_light.radius, + transform: *transform, + shadows_enabled: point_light.shadows_enabled, + shadow_depth_bias: point_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: point_light.shadow_normal_bias + * point_light_texel_size + * core::f32::consts::SQRT_2, + shadow_map_near_z: point_light.shadow_map_near_z, + spot_light_angles: None, + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: point_light.affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: point_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, + }; + point_lights_values.push(( + render_entity, + ( + extracted_point_light, + render_cubemap_visible_entities, + (*frusta).clone(), + MainEntity::from(main_entity), + ), + )); + } + *previous_point_lights_len = point_lights_values.len(); + commands.try_insert_batch(point_lights_values); + + let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); + for entity in global_visible_clusterable.iter().copied() { + if let Ok(( + main_entity, + render_entity, + spot_light, + visible_entities, + transform, + view_visibility, + frustum, + volumetric_light, + )) = spot_lights.get(entity) + { + if !view_visibility.get() { + continue; + } + let render_visible_entities = + create_render_visible_mesh_entities(&mapper, visible_entities); + + let texel_size = + 2.0 * ops::tan(spot_light.outer_angle) / directional_light_shadow_map.size as f32; + + spot_lights_values.push(( + render_entity, + ( + ExtractedPointLight { + color: spot_light.color.into(), + // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian + // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + // for details. + // Note: Filament uses a divisor of PI for spot lights. We choose to use the same 4*PI divisor + // in both cases so that toggling between point light and spot light keeps lit areas lit equally, + // which seems least surprising for users + intensity: spot_light.intensity / (4.0 * core::f32::consts::PI), + range: spot_light.range, + radius: spot_light.radius, + transform: *transform, + shadows_enabled: spot_light.shadows_enabled, + shadow_depth_bias: spot_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: spot_light.shadow_normal_bias + * texel_size + * core::f32::consts::SQRT_2, + shadow_map_near_z: spot_light.shadow_map_near_z, + spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: spot_light + .affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: spot_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, + }, + render_visible_entities, + *frustum, + MainEntity::from(main_entity), + ), + )); + } + } + *previous_spot_lights_len = spot_lights_values.len(); + commands.try_insert_batch(spot_lights_values); + + for ( + main_entity, + entity, + directional_light, + visible_entities, + cascades, + cascade_config, + frusta, + transform, + view_visibility, + maybe_layers, + volumetric_light, + occlusion_culling, + sun_disk, + ) in &directional_lights + { + if !view_visibility.get() { + commands + .get_entity(entity) + .expect("Light entity wasn't synced.") + .remove::<(ExtractedDirectionalLight, RenderCascadesVisibleEntities)>(); + continue; + } + + // TODO: update in place instead of reinserting. + let mut extracted_cascades = EntityHashMap::default(); + let mut extracted_frusta = EntityHashMap::default(); + let mut cascade_visible_entities = EntityHashMap::default(); + for (e, v) in cascades.cascades.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_cascades.insert(entity, v.clone()); + } else { + break; + } + } + for (e, v) in frusta.frusta.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_frusta.insert(entity, v.clone()); + } else { + break; + } + } + for (e, v) in visible_entities.entities.iter() { + if let Ok(entity) = mapper.get(*e) { + cascade_visible_entities.insert( + entity, + v.iter() + .map(|v| create_render_visible_mesh_entities(&mapper, v)) + .collect(), + ); + } else { + break; + } + } + + commands + .get_entity(entity) + .expect("Light entity wasn't synced.") + .insert(( + ExtractedDirectionalLight { + color: directional_light.color.into(), + illuminance: directional_light.illuminance, + transform: *transform, + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: directional_light + .affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadow_size: directional_light.soft_shadow_size, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadow_size: None, + shadows_enabled: directional_light.shadows_enabled, + shadow_depth_bias: directional_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: directional_light.shadow_normal_bias + * core::f32::consts::SQRT_2, + cascade_shadow_config: cascade_config.clone(), + cascades: extracted_cascades, + frusta: extracted_frusta, + render_layers: maybe_layers.unwrap_or_default().clone(), + occlusion_culling, + sun_disk_angular_size: sun_disk.unwrap_or_default().angular_size, + sun_disk_intensity: sun_disk.unwrap_or_default().intensity, + }, + RenderCascadesVisibleEntities { + entities: cascade_visible_entities, + }, + MainEntity::from(main_entity), + )); + } +} + +fn create_render_visible_mesh_entities( + mapper: &Extract>, + visible_entities: &VisibleMeshEntities, +) -> RenderVisibleMeshEntities { + RenderVisibleMeshEntities { + entities: visible_entities + .iter() + .map(|e| { + let render_entity = mapper.get(*e).unwrap_or(Entity::PLACEHOLDER); + (render_entity, MainEntity::from(*e)) + }) + .collect(), + } +} + +#[derive(Component, Default, Deref, DerefMut)] +/// Component automatically attached to a light entity to track light-view entities +/// for each view. +pub struct LightViewEntities(EntityHashMap>); + +// TODO: using required component +pub(crate) fn add_light_view_entities( + add: On, + mut commands: Commands, +) { + if let Ok(mut v) = commands.get_entity(add.entity) { + v.insert(LightViewEntities::default()); + } +} + +/// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. +pub(crate) fn extracted_light_removed( + remove: On, + mut commands: Commands, +) { + if let Ok(mut v) = commands.get_entity(remove.entity) { + v.try_remove::(); + } +} + +pub(crate) fn remove_light_view_entities( + remove: On, + query: Query<&LightViewEntities>, + mut commands: Commands, +) { + if let Ok(entities) = query.get(remove.entity) { + for v in entities.0.values() { + for e in v.iter().copied() { + if let Ok(mut v) = commands.get_entity(e) { + v.despawn(); + } + } + } + } +} + +#[derive(Component)] +pub struct ShadowView { + pub depth_attachment: DepthAttachment, + pub pass_name: String, +} + +#[derive(Component)] +pub struct ViewShadowBindings { + pub point_light_depth_texture: Texture, + pub point_light_depth_texture_view: TextureView, + pub directional_light_depth_texture: Texture, + pub directional_light_depth_texture_view: TextureView, +} + +/// A component that holds the shadow cascade views for all shadow cascades +/// associated with a camera. +/// +/// Note: Despite the name, this component actually holds the shadow cascade +/// views, not the lights themselves. +#[derive(Component)] +pub struct ViewLightEntities { + /// The shadow cascade views for all shadow cascades associated with a + /// camera. + /// + /// Note: Despite the name, this component actually holds the shadow cascade + /// views, not the lights themselves. + pub lights: Vec, +} + +#[derive(Component)] +pub struct ViewLightsUniformOffset { + pub offset: u32, +} + +#[derive(Resource, Default)] +pub struct LightMeta { + pub view_gpu_lights: DynamicUniformBuffer, +} + +#[derive(Component)] +pub enum LightEntity { + Directional { + light_entity: Entity, + cascade_index: usize, + }, + Point { + light_entity: Entity, + face_index: usize, + }, + Spot { + light_entity: Entity, + }, +} + +pub fn prepare_lights( + mut commands: Commands, + mut texture_cache: ResMut, + (render_device, render_queue): (Res, Res), + mut global_light_meta: ResMut, + mut light_meta: ResMut, + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + ), + With, + >, + ambient_light: Res, + point_light_shadow_map: Res, + directional_light_shadow_map: Res, + mut shadow_render_phases: ResMut>, + ( + mut max_directional_lights_warning_emitted, + mut max_cascades_per_light_warning_emitted, + mut live_shadow_mapping_lights, + ): (Local, Local, Local>), + point_lights: Query<( + Entity, + &MainEntity, + &ExtractedPointLight, + AnyOf<(&CubemapFrusta, &Frustum)>, + )>, + directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>, + mut light_view_entities: Query<&mut LightViewEntities>, + sorted_cameras: Res, + (gpu_preprocessing_support, decals): ( + Res, + Option>, + ), +) { + let views_iter = views.iter(); + let views_count = views_iter.len(); + let Some(mut view_gpu_lights_writer) = + light_meta + .view_gpu_lights + .get_writer(views_count, &render_device, &render_queue) + else { + return; + }; + + // Pre-calculate for PointLights + let cube_face_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + + global_light_meta.entity_to_index.clear(); + + let mut point_lights: Vec<_> = point_lights.iter().collect::>(); + let mut directional_lights: Vec<_> = directional_lights.iter().collect::>(); + + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize; + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + let max_texture_cubes = max_texture_array_layers / 6; + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + let max_texture_array_layers = 1; + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + let max_texture_cubes = 1; + + if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS + { + warn!( + "The amount of directional lights of {} is exceeding the supported limit of {}.", + directional_lights.len(), + MAX_DIRECTIONAL_LIGHTS + ); + *max_directional_lights_warning_emitted = true; + } + + if !*max_cascades_per_light_warning_emitted + && directional_lights + .iter() + .any(|(_, _, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT) + { + warn!( + "The number of cascades configured for a directional light exceeds the supported limit of {}.", + MAX_CASCADES_PER_LIGHT + ); + *max_cascades_per_light_warning_emitted = true; + } + + let point_light_count = point_lights + .iter() + .filter(|light| light.2.spot_light_angles.is_none()) + .count(); + + let point_light_volumetric_enabled_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_none()) + .count() + .min(max_texture_cubes); + + let point_light_shadow_maps_count = point_lights + .iter() + .filter(|light| light.2.shadows_enabled && light.2.spot_light_angles.is_none()) + .count() + .min(max_texture_cubes); + + let directional_volumetric_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, _, light)| light.volumetric) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + + let directional_shadow_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, _, light)| light.shadows_enabled) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + + let spot_light_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + let spot_light_volumetric_enabled_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + let spot_light_shadow_maps_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.shadows_enabled && light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + // Sort lights by + // - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader, + // - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count` + // point light shadows and `spot_light_shadow_maps_count` spot light shadow maps, + // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. + point_lights.sort_by_cached_key(|(entity, _, light, _)| { + ( + point_or_spot_light_to_clusterable(light).ordering(), + *entity, + ) + }); + + // Sort lights by + // - those with volumetric (and shadows) enabled first, so that the + // volumetric lighting pass can quickly find the volumetric lights; + // - then those with shadows enabled second, so that the index can be used + // to render at most `directional_light_shadow_maps_count` directional light + // shadows + // - then by entity as a stable key to ensure that a consistent set of + // lights are chosen if the light count limit is exceeded. + // - because entities are unique, we can use `sort_unstable_by_key` + // and still end up with a stable order. + directional_lights.sort_unstable_by_key(|(entity, _, light)| { + (light.volumetric, light.shadows_enabled, *entity) + }); + + if global_light_meta.entity_to_index.capacity() < point_lights.len() { + global_light_meta + .entity_to_index + .reserve(point_lights.len()); + } + + let mut gpu_point_lights = Vec::new(); + for (index, &(entity, _, light, _)) in point_lights.iter().enumerate() { + let mut flags = PointLightFlags::NONE; + + // Lights are sorted, shadow enabled lights are first + if light.shadows_enabled + && (index < point_light_shadow_maps_count + || (light.spot_light_angles.is_some() + && index - point_light_count < spot_light_shadow_maps_count)) + { + flags |= PointLightFlags::SHADOWS_ENABLED; + } + + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + if light.shadows_enabled + && light.volumetric + && (index < point_light_volumetric_enabled_count + || (light.spot_light_angles.is_some() + && index - point_light_count < spot_light_volumetric_enabled_count)) + { + flags |= PointLightFlags::VOLUMETRIC; + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= PointLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles { + Some((inner, outer)) => { + let light_direction = light.transform.forward(); + if light_direction.y.is_sign_negative() { + flags |= PointLightFlags::SPOT_LIGHT_Y_NEGATIVE; + } + + let cos_outer = ops::cos(outer); + let spot_scale = 1.0 / f32::max(ops::cos(inner) - cos_outer, 1e-4); + let spot_offset = -cos_outer * spot_scale; + + ( + // For spot lights: the direction (x,z), spot_scale and spot_offset + light_direction.xz().extend(spot_scale).extend(spot_offset), + ops::tan(outer), + ) + } + None => { + ( + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + Vec4::new( + cube_face_projection.z_axis.z, + cube_face_projection.z_axis.w, + cube_face_projection.w_axis.z, + cube_face_projection.w_axis.w, + ), + // unused + 0.0, + ) + } + }; + + gpu_point_lights.push(GpuClusterableObject { + light_custom_data, + // premultiply color by intensity + // we don't use the alpha at all, so no reason to multiply only [0..3] + color_inverse_square_range: (Vec4::from_slice(&light.color.to_f32_array()) + * light.intensity) + .xyz() + .extend(1.0 / (light.range * light.range)), + position_radius: light.transform.translation().extend(light.radius), + flags: flags.bits(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + shadow_map_near_z: light.shadow_map_near_z, + spot_light_tan_angle, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + pad: 0.0, + soft_shadow_size: if light.soft_shadows_enabled { + light.radius + } else { + 0.0 + }, + }); + global_light_meta.entity_to_index.insert(entity, index); + } + + // iterate the views once to find the maximum number of cascade shadowmaps we will need + let mut num_directional_cascades_enabled = 0usize; + for ( + _entity, + _camera_main_entity, + _extracted_view, + _clusters, + maybe_layers, + _no_indirect_drawing, + _maybe_ambient_override, + ) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { + let mut num_directional_cascades_for_this_view = 0usize; + let render_layers = maybe_layers.unwrap_or_default(); + + for (_light_entity, _, light) in directional_lights.iter() { + if light.shadows_enabled && light.render_layers.intersects(render_layers) { + num_directional_cascades_for_this_view += light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + } + } + + num_directional_cascades_enabled = num_directional_cascades_enabled + .max(num_directional_cascades_for_this_view) + .min(max_texture_array_layers); + } + + global_light_meta + .gpu_clusterable_objects + .set(gpu_point_lights); + global_light_meta + .gpu_clusterable_objects + .write_buffer(&render_device, &render_queue); + + live_shadow_mapping_lights.clear(); + + let mut point_light_depth_attachments = HashMap::::default(); + let mut directional_light_depth_attachments = HashMap::::default(); + + let point_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: point_light_shadow_map.size as u32, + height: point_light_shadow_map.size as u32, + depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("point_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let point_light_depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_array_texture_view"), + format: None, + // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. + // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. + #[cfg(all( + not(target_abi = "sim"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + dimension: Some(TextureViewDimension::CubeArray), + #[cfg(any( + target_abi = "sim", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + dimension: Some(TextureViewDimension::Cube), + usage: None, + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let directional_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + height: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + depth_or_array_layers: (num_directional_cascades_enabled + + spot_light_shadow_maps_count) + .max(1) as u32, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("directional_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let directional_light_depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + dimension: Some(TextureViewDimension::D2Array), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let mut live_views = EntityHashSet::with_capacity(views_count); + + // set up light data for each view + for ( + entity, + camera_main_entity, + extracted_view, + clusters, + maybe_layers, + no_indirect_drawing, + maybe_ambient_override, + ) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { + live_views.insert(entity); + + let view_layers = maybe_layers.unwrap_or_default(); + let mut view_lights = Vec::new(); + let mut view_occlusion_culling_lights = Vec::new(); + + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; + let cluster_factors_zw = calculate_cluster_factors( + clusters.near, + clusters.far, + clusters.dimensions.z as f32, + is_orthographic, + ); + + let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z; + let ambient_light = AmbientLight { + color: ambient_light.color, + brightness: ambient_light.brightness, + affects_lightmapped_meshes: ambient_light.affects_lightmapped_meshes, + }; + let ambient_light = maybe_ambient_override.unwrap_or(&ambient_light); + + let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + let mut num_directional_cascades_enabled_for_this_view = 0usize; + let mut num_directional_lights_for_this_view = 0usize; + for (index, (light_entity, _, light)) in directional_lights + .iter() + .filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + num_directional_lights_for_this_view += 1; + + let mut flags = DirectionalLightFlags::NONE; + + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + + // Shadow enabled lights are second + let mut num_cascades = 0; + if light.shadows_enabled { + let cascades = light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + + if num_directional_cascades_enabled_for_this_view + cascades + <= max_texture_array_layers + { + flags |= DirectionalLightFlags::SHADOWS_ENABLED; + num_cascades += cascades; + } + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + gpu_directional_lights[index] = GpuDirectionalLight { + // Filled in later. + cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], + // premultiply color by illuminance + // we don't use the alpha at all, so no reason to multiply only [0..3] + color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, + // direction is negated to be ready for N.L + dir_to_light: light.transform.back().into(), + flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + num_cascades: num_cascades as u32, + cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, + depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + sun_disk_angular_size: light.sun_disk_angular_size, + sun_disk_intensity: light.sun_disk_intensity, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(*light_entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + }; + num_directional_cascades_enabled_for_this_view += num_cascades; + } + + let mut gpu_lights = GpuLights { + directional_lights: gpu_directional_lights, + ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array()) + * ambient_light.brightness, + cluster_factors: Vec4::new( + clusters.dimensions.x as f32 / extracted_view.viewport.z as f32, + clusters.dimensions.y as f32 / extracted_view.viewport.w as f32, + cluster_factors_zw.x, + cluster_factors_zw.y, + ), + cluster_dimensions: clusters.dimensions.extend(n_clusters), + n_directional_lights: num_directional_lights_for_this_view as u32, + // spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled. + // the spot lights themselves start in the light array at point_light_count. so to go from light + // index to shadow map index, we need to subtract point light count and add directional shadowmap count. + spot_light_shadowmap_offset: num_directional_cascades_enabled as i32 + - point_light_count as i32, + ambient_light_affects_lightmapped_meshes: ambient_light.affects_lightmapped_meshes + as u32, + }; + + // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query + for &(light_entity, light_main_entity, light, (point_light_frusta, _)) in point_lights + .iter() + // Lights are sorted, shadow enabled lights are first + .take(point_light_count.min(max_texture_cubes)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let light_index = *global_light_meta + .entity_to_index + .get(&light_entity) + .unwrap(); + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = GlobalTransform::from_translation(light.transform.translation()); + + // for each face of a cube and each view we spawn a light entity + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| (0..6).map(|_| commands.spawn_empty().id()).collect()); + + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + + for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations + .iter() + .zip(&point_light_frusta.unwrap().frusta) + .zip(light_view_entities.iter().copied()) + .enumerate() + { + let mut first = false; + let base_array_layer = (light_index * 6 + face_index) as u32; + + let depth_attachment = point_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + face_index as u32, + ); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_point_light_{}_{}", + light_index, + face_index_to_name(face_index) + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + hdr: false, + color_grading: Default::default(), + invert_culling: false, + }, + *frustum, + LightEntity::Point { + light_entity, + face_index, + }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + } + + // spot lights + for (light_index, &(light_entity, light_main_entity, light, (_, spot_light_frustum))) in + point_lights + .iter() + .skip(point_light_count) + .take(spot_light_count) + .enumerate() + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let spot_world_from_view = spot_light_world_from_view(&light.transform); + let spot_world_from_view = spot_world_from_view.into(); + + let angle = light.spot_light_angles.expect("lights should be sorted so that \ + [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; + let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); + + let mut first = false; + let base_array_layer = (num_directional_cascades_enabled + light_index) as u32; + + let depth_attachment = directional_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = directional_light_depth_texture.texture.create_view( + &TextureViewDescriptor { + label: Some("spot_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }, + ); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| vec![commands.spawn_empty().id()]); + + let view_light_entity = light_view_entities[0]; + + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, Some(camera_main_entity.into()), 0); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!("shadow_spot_light_{light_index}"), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + hdr: false, + color_grading: Default::default(), + invert_culling: false, + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { light_entity }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + + // directional lights + // clear entities for lights that don't intersect the layer + for &(light_entity, _, _) in directional_lights + .iter() + .filter(|(_, _, light)| !light.render_layers.intersects(view_layers)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + } + + let mut directional_depth_texture_array_index = 0u32; + for (light_index, &(light_entity, light_main_entity, light)) in directional_lights + .iter() + .filter(|(_, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + let gpu_light = &mut gpu_lights.directional_lights[light_index]; + + // Only deal with cascades when shadows are enabled. + if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let cascades = light + .cascades + .get(&entity) + .unwrap() + .iter() + .take(MAX_CASCADES_PER_LIGHT); + let frusta = light + .frusta + .get(&entity) + .unwrap() + .iter() + .take(MAX_CASCADES_PER_LIGHT); + + let iter = cascades + .zip(frusta) + .zip(&light.cascade_shadow_config.bounds); + + let light_view_entities = light_view_entities.entry(entity).or_insert_with(|| { + (0..iter.len()) + .map(|_| commands.spawn_empty().id()) + .collect() + }); + if light_view_entities.len() != iter.len() { + let entities = core::mem::take(light_view_entities); + despawn_entities(&mut commands, entities); + light_view_entities.extend((0..iter.len()).map(|_| commands.spawn_empty().id())); + } + + for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in + iter.zip(light_view_entities.iter().copied()).enumerate() + { + gpu_lights.directional_lights[light_index].cascades[cascade_index] = + GpuDirectionalCascade { + clip_from_world: cascade.clip_from_world, + texel_size: cascade.texel_size, + far_bound: *bound, + }; + + let depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: directional_depth_texture_array_index, + array_layer_count: Some(1u32), + }); + + // NOTE: For point and spotlights, we reuse the same depth attachment for all views. + // However, for directional lights, we want a new depth attachment for each view, + // so that the view is cleared for each view. + let depth_attachment = DepthAttachment::new(depth_texture_view.clone(), Some(0.0)); + + directional_depth_texture_array_index += 1; + + let mut frustum = *frustum; + // Push the near clip plane out to infinity for directional lights + frustum.half_spaces[Frustum::NEAR_PLANE_IDX] = HalfSpace::new( + frustum.half_spaces[Frustum::NEAR_PLANE_IDX] + .normal() + .extend(f32::INFINITY), + ); + + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + cascade_index as u32, + ); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_directional_light_{light_index}_cascade_{cascade_index}" + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: GlobalTransform::from(cascade.world_from_cascade), + clip_from_view: cascade.clip_from_cascade, + clip_from_world: Some(cascade.clip_from_world), + hdr: false, + color_grading: Default::default(), + invert_culling: false, + }, + frustum, + LightEntity::Directional { + light_entity, + cascade_index, + }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + // If this light is using occlusion culling, add the appropriate components. + if light.occlusion_culling { + commands.entity(view_light_entity).insert(( + OcclusionCulling, + OcclusionCullingSubview { + depth_texture_view, + depth_texture_size: directional_light_shadow_map.size as u32, + }, + )); + view_occlusion_culling_lights.push(view_light_entity); + } + + // Subsequent views with the same light entity will **NOT** reuse the same shadow map + // (Because the cascades are unique to each view) + // TODO: Implement GPU culling for shadow passes. + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + + commands.entity(entity).insert(( + ViewShadowBindings { + point_light_depth_texture: point_light_depth_texture.texture.clone(), + point_light_depth_texture_view: point_light_depth_texture_view.clone(), + directional_light_depth_texture: directional_light_depth_texture.texture.clone(), + directional_light_depth_texture_view: directional_light_depth_texture_view.clone(), + }, + ViewLightEntities { + lights: view_lights, + }, + ViewLightsUniformOffset { + offset: view_gpu_lights_writer.write(&gpu_lights), + }, + )); + + // Make a link from the camera to all shadow cascades with occlusion + // culling enabled. + if !view_occlusion_culling_lights.is_empty() { + commands + .entity(entity) + .insert(OcclusionCullingSubviewEntities( + view_occlusion_culling_lights, + )); + } + } + + // Despawn light-view entities for views that no longer exist + for mut entities in &mut light_view_entities { + for (_, light_view_entities) in + entities.extract_if(|entity, _| !live_views.contains(entity)) + { + despawn_entities(&mut commands, light_view_entities); + } + } + + shadow_render_phases.retain(|entity, _| live_shadow_mapping_lights.contains(entity)); +} + +fn despawn_entities(commands: &mut Commands, entities: Vec) { + if entities.is_empty() { + return; + } + commands.queue(move |world: &mut World| { + for entity in entities { + world.despawn(entity); + } + }); +} + +// These will be extracted in the material extraction, which will also clear the needs_specialization +// collection. +pub fn check_light_entities_needing_specialization( + needs_specialization: Query>, Changed)>, + mesh_materials: Query>>, + mut entities_needing_specialization: ResMut>, + mut removed_components: RemovedComponents, +) { + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } + + for removed in removed_components.read() { + // Only require specialization if the entity still exists. + if mesh_materials.contains(removed) { + entities_needing_specialization.entities.push(removed); + } + } +} + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightKeyCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightSpecializationTicks(HashMap); + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialPipelineCache { + // view light entity -> view pipeline cache + #[deref] + map: HashMap, +} + +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialViewPipelineCache { + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +pub fn check_views_lights_need_specialization( + view_lights: Query<&ViewLightEntities, With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + shadow_render_phases: Res>, + mut light_key_cache: ResMut, + mut light_specialization_ticks: ResMut, + ticks: SystemChangeTick, +) { + for view_lights in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + + let is_directional_light = matches!(light_entity, LightEntity::Directional { .. }); + let mut light_key = MeshPipelineKey::DEPTH_PREPASS; + light_key.set(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO, is_directional_light); + if let Some(current_key) = + light_key_cache.get_mut(&extracted_view_light.retained_view_entity) + { + if *current_key != light_key { + light_key_cache.insert(extracted_view_light.retained_view_entity, light_key); + light_specialization_ticks + .insert(extracted_view_light.retained_view_entity, ticks.this_run()); + } + } else { + light_key_cache.insert(extracted_view_light.retained_view_entity, light_key); + light_specialization_ticks + .insert(extracted_view_light.retained_view_entity, ticks.this_run()); + } + } + } +} + +pub fn specialize_shadows( + prepass_pipeline: Res, + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( + Res>, + Res, + Res>, + Res, + ), + shadow_render_phases: Res>, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_lightmaps: Res, + view_lights: Query<(Entity, &ViewLightEntities), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + directional_light_entities: Query< + &RenderCascadesVisibleEntities, + With, + >, + spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + light_key_cache: Res, + mut specialized_material_pipeline_cache: ResMut, + light_specialization_ticks: Res, + entity_specialization_ticks: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_shadow_views: HashSet = HashSet::default(); + + for (entity, view_lights) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + + all_shadow_views.insert(extracted_view_light.retained_view_entity); + + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + let Some(light_key) = light_key_cache.get(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let visible_entities = match light_entity { + LightEntity::Directional { + light_entity, + cascade_index, + } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities") + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect("Failed to get directional light visible entities for cascade"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + LightEntity::Spot { light_entity } => spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"), + }; + + // NOTE: Lights with shadow mapping disabled will have no visible entities + // so no meshes will be queued + + let view_tick = light_specialization_ticks + .get(&extracted_view_light.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view_light.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter().copied() { + let Some(material_instance) = + render_material_instances.instances.get(&visible_entity) + else { + continue; + }; + + let Some(mesh_instance) = + render_mesh_instances.render_mesh_queue_data(visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(&visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick + .system_tick + .is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + if !material.properties.shadows_enabled { + // If the material is not a shadow caster, we don't need to specialize it. + continue; + } + if !mesh_instance + .flags + .contains(RenderMeshInstanceFlags::SHADOW_CASTER) + { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = + *light_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + + // Even though we don't use the lightmap in the shadow map, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + if render_lightmaps + .render_lightmaps + .contains_key(&visible_entity) + { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + } + + mesh_key |= match material.properties.alpha_mode { + AlphaMode::Mask(_) + | AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, + _ => MeshPipelineKey::NONE, + }; + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(visible_entity, (ticks.this_run(), pipeline_id)); + } + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache.retain(|view, _| all_shadow_views.contains(view)); +} + +/// For each shadow cascade, iterates over all the meshes "visible" from it and +/// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as +/// appropriate. +pub fn queue_shadows( + render_mesh_instances: Res, + render_materials: Res>, + render_material_instances: Res, + mut shadow_render_phases: ResMut>, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + view_lights: Query<(Entity, &ViewLightEntities, Option<&RenderLayers>), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + directional_light_entities: Query< + &RenderCascadesVisibleEntities, + With, + >, + spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + specialized_material_pipeline_cache: Res, +) { + for (entity, view_lights, camera_layers) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + let Some(shadow_phase) = + shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let visible_entities = match light_entity { + LightEntity::Directional { + light_entity, + cascade_index, + } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities") + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect("Failed to get directional light visible entities for cascade"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + LightEntity::Spot { light_entity } => spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"), + }; + + for (entity, main_entity) in visible_entities.iter().copied() { + let Some((current_change_tick, pipeline_id)) = + view_specialized_material_pipeline_cache.get(&main_entity) + else { + continue; + }; + + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity) + else { + continue; + }; + if !mesh_instance + .flags + .contains(RenderMeshInstanceFlags::SHADOW_CASTER) + { + continue; + } + + let mesh_layers = mesh_instance + .shared + .render_layers + .as_ref() + .unwrap_or_default(); + + let camera_layers = camera_layers.unwrap_or_default(); + + if !camera_layers.intersects(mesh_layers) { + continue; + } + + // Skip the entity if it's cached in a bin and up to date. + if shadow_phase.validate_cached_entity(main_entity, *current_change_tick) { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(&main_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + let Some(draw_function) = + material.properties.get_draw_function(ShadowsDrawFunction) + else { + continue; + }; + + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + + let batch_set_key = ShadowBatchSetKey { + pipeline: *pipeline_id, + draw_function, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + + shadow_phase.add( + batch_set_key, + ShadowBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (entity, main_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + } +} + +pub struct Shadow { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: ShadowBatchSetKey, + /// Information that separates items into bins. + pub bin_key: ShadowBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +/// Information that must be identical in order to place opaque meshes in the +/// same *batch set*. +/// +/// A batch set is a set of batches that can be multi-drawn together, if +/// multi-draw is in use. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShadowBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for ShadowBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data used to bin each object in the shadow map phase. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShadowBinKey { + /// The object. + pub asset_id: UntypedAssetId, +} + +impl PhaseItem for Shadow { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Shadow { + type BatchSetKey = ShadowBatchSetKey; + type BinKey = ShadowBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Shadow { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Shadow { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +/// The rendering node that renders meshes that were "visible" (so to speak) +/// from a light last frame. +/// +/// If occlusion culling for a light is disabled, then this node simply renders +/// all meshes in range of the light. +#[derive(Deref, DerefMut)] +pub struct EarlyShadowPassNode(ShadowPassNode); + +/// The rendering node that renders meshes that became newly "visible" (so to +/// speak) from a light this frame. +/// +/// If occlusion culling for a light is disabled, then this node does nothing. +#[derive(Deref, DerefMut)] +pub struct LateShadowPassNode(ShadowPassNode); + +/// Encapsulates rendering logic shared between the early and late shadow pass +/// nodes. +pub struct ShadowPassNode { + /// The query that finds cameras in which shadows are visible. + main_view_query: QueryState>, + /// The query that finds shadow cascades. + view_light_query: QueryState<(Read, Read, Has)>, +} + +impl FromWorld for EarlyShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self(ShadowPassNode::from_world(world)) + } +} + +impl FromWorld for LateShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self(ShadowPassNode::from_world(world)) + } +} + +impl FromWorld for ShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + view_light_query: QueryState::new(world), + } + } +} + +impl Node for EarlyShadowPassNode { + fn update(&mut self, world: &mut World) { + self.0.update(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + self.0.run(graph, render_context, world, false) + } +} + +impl Node for LateShadowPassNode { + fn update(&mut self, world: &mut World) { + self.0.update(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + self.0.run(graph, render_context, world, true) + } +} + +impl ShadowPassNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.view_light_query.update_archetypes(world); + } + + /// Runs the node logic. + /// + /// `is_late` is true if this is the late shadow pass or false if this is + /// the early shadow pass. + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + is_late: bool, + ) -> Result<(), NodeRunError> { + let Some(shadow_render_phases) = world.get_resource::>() + else { + return Ok(()); + }; + + if let Ok(view_lights) = self.main_view_query.get_manual(world, graph.view_entity()) { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((view_light, extracted_light_view, occlusion_culling)) = + self.view_light_query.get_manual(world, view_light_entity) + else { + continue; + }; + + // There's no need for a late shadow pass if the light isn't + // using occlusion culling. + if is_late && !occlusion_culling { + continue; + } + + let Some(shadow_phase) = + shadow_render_phases.get(&extracted_light_view.retained_view_entity) + else { + continue; + }; + + let depth_stencil_attachment = + Some(view_light.depth_attachment.get_attachment(StoreOp::Store)); + + let diagnostics = render_context.diagnostic_recorder(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _shadow_pass_span = info_span!("", "{}", view_light.pass_name).entered(); + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("shadow_pass_command_encoder"), + }); + + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some(&view_light.pass_name), + color_attachments: &[], + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = + diagnostics.pass_span(&mut render_pass, view_light.pass_name.clone()); + + if let Err(err) = + shadow_phase.render(&mut render_pass, world, view_light_entity) + { + error!("Error encountered while rendering the shadow phase {err:?}"); + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + } + } + + Ok(()) + } +} + +/// Creates the [`ClusterableObjectType`] data for a point or spot light. +fn point_or_spot_light_to_clusterable(point_light: &ExtractedPointLight) -> ClusterableObjectType { + match point_light.spot_light_angles { + Some((_, outer_angle)) => ClusterableObjectType::SpotLight { + outer_angle, + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + None => ClusterableObjectType::PointLight { + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + } +} diff --git a/vendor/bevy_pbr/src/render/mesh.rs b/vendor/bevy_pbr/src/render/mesh.rs new file mode 100644 index 00000000..e0243899 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh.rs @@ -0,0 +1,3431 @@ +use crate::{ + material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}, + resources::write_atmosphere_buffer, +}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId}; +use bevy_camera::{ + primitives::Aabb, + visibility::{NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange}, + Camera, Camera3d, Projection, +}; +use bevy_core_pipeline::{ + core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, + deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, + oit::{prepare_oit_buffers, OrderIndependentTransparencySettingsOffset}, + prepass::MotionVectorPrepass, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + entity::EntityHashSet, + prelude::*, + query::{QueryData, ROQueryItem}, + relationship::RelationshipSourceCollection, + system::{lifetimeless::*, SystemParamItem, SystemState}, +}; +use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo}; +use bevy_light::{ + EnvironmentMapLight, IrradianceVolume, NotShadowCaster, NotShadowReceiver, + ShadowFilteringMethod, TransmittedShadowReceiver, +}; +use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; +use bevy_mesh::{ + skinning::SkinnedMesh, BaseMeshPipelineKey, Mesh, Mesh3d, MeshTag, MeshVertexBufferLayoutRef, + VertexAttributeDescriptor, +}; +use bevy_platform::collections::{hash_map::Entry, HashMap}; +use bevy_render::{ + batching::{ + gpu_preprocessing::{ + self, GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers, + IndirectParametersCpuMetadata, IndirectParametersIndexed, IndirectParametersNonIndexed, + InstanceInputUniformBuffer, UntypedPhaseIndirectParametersBuffers, + }, + no_gpu_preprocessing, GetBatchData, GetFullBatchData, NoAutomaticBatching, + }, + mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo}, + render_asset::RenderAssets, + render_phase::{ + BinnedRenderPhasePlugin, InputUniformIndex, PhaseItem, PhaseItemExtraIndex, RenderCommand, + RenderCommandResult, SortedRenderPhasePlugin, TrackedRenderPass, + }, + render_resource::*, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::MainEntityHashSet, + texture::{DefaultImageSampler, GpuImage}, + view::{ + self, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity, ViewTarget, + ViewUniformOffset, + }, + Extract, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal, ShaderSettings}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::{default, Parallel, TypeIdMap}; +use core::any::TypeId; +use core::mem::size_of; +use material_bind_groups::MaterialBindingId; +use tracing::{error, warn}; + +use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; +use crate::{ + render::{ + morph::{ + extract_morphs, no_automatic_morph_batching, prepare_morphs, MorphIndices, + MorphUniforms, + }, + skin::no_automatic_skin_batching, + }, + *, +}; +use bevy_core_pipeline::oit::OrderIndependentTransparencySettings; +use bevy_core_pipeline::prepass::{DeferredPrepass, DepthPrepass, NormalPrepass}; +use bevy_core_pipeline::tonemapping::{DebandDither, Tonemapping}; +use bevy_ecs::change_detection::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_render::camera::TemporalJitter; +use bevy_render::prelude::Msaa; +use bevy_render::sync_world::{MainEntity, MainEntityHashMap}; +use bevy_render::view::ExtractedView; +use bevy_render::RenderSystems::PrepareAssets; + +use bytemuck::{Pod, Zeroable}; +use nonmax::{NonMaxU16, NonMaxU32}; +use smallvec::{smallvec, SmallVec}; +use static_assertions::const_assert_eq; + +/// Provides support for rendering 3D meshes. +pub struct MeshRenderPlugin { + /// Whether we're building [`MeshUniform`]s on GPU. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl MeshRenderPlugin { + /// Creates a new [`MeshRenderPlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> MeshRenderPlugin { + MeshRenderPlugin { + use_gpu_instance_buffer_builder: false, + debug_flags, + } + } +} + +/// How many textures are allowed in the view bind group layout (`@group(0)`) before +/// broader compatibility with WebGL and WebGPU is at risk, due to the minimum guaranteed +/// values for `MAX_TEXTURE_IMAGE_UNITS` (in WebGL) and `maxSampledTexturesPerShaderStage` (in WebGPU), +/// currently both at 16. +/// +/// We use 10 here because it still leaves us, in a worst case scenario, with 6 textures for the other bind groups. +/// +/// See: +#[cfg(debug_assertions)] +pub const MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES: usize = 10; + +impl Plugin for MeshRenderPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "forward_io.wgsl"); + load_shader_library!(app, "mesh_view_types.wgsl", |settings| *settings = + ShaderSettings { + shader_defs: vec![ + ShaderDefVal::UInt( + "MAX_DIRECTIONAL_LIGHTS".into(), + MAX_DIRECTIONAL_LIGHTS as u32 + ), + ShaderDefVal::UInt( + "MAX_CASCADES_PER_LIGHT".into(), + MAX_CASCADES_PER_LIGHT as u32, + ) + ] + }); + load_shader_library!(app, "mesh_view_bindings.wgsl"); + load_shader_library!(app, "mesh_types.wgsl"); + load_shader_library!(app, "mesh_functions.wgsl"); + load_shader_library!(app, "skinning.wgsl"); + load_shader_library!(app, "morph.wgsl"); + load_shader_library!(app, "occlusion_culling.wgsl"); + + embedded_asset!(app, "mesh.wgsl"); + + if app.get_sub_app(RenderApp).is_none() { + return; + } + + app.add_systems( + PostUpdate, + (no_automatic_skin_batching, no_automatic_morph_batching), + ) + .add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + SortedRenderPhasePlugin::::new(self.debug_flags), + SortedRenderPhasePlugin::::new(self.debug_flags), + )); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .configure_sets( + ExtractSchedule, + MeshExtractionSystems + .after(view::extract_visibility_ranges) + .after(late_sweep_material_instances), + ) + .add_systems( + ExtractSchedule, + ( + extract_skins, + extract_morphs, + gpu_preprocessing::clear_batched_gpu_instance_buffers:: + .before(MeshExtractionSystems), + ), + ) + .add_systems( + Render, + ( + set_mesh_motion_vector_flags.in_set(RenderSystems::PrepareMeshes), + prepare_skins.in_set(RenderSystems::PrepareResources), + prepare_morphs.in_set(RenderSystems::PrepareResources), + prepare_mesh_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_mesh_view_bind_groups + .in_set(RenderSystems::PrepareBindGroups) + .after(prepare_oit_buffers) + .after(write_atmosphere_buffer), + no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: + .in_set(RenderSystems::Cleanup) + .after(RenderSystems::Render), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + let mut mesh_bindings_shader_defs = Vec::with_capacity(1); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + check_views_need_specialization.in_set(PrepareAssets), + ); + + let gpu_preprocessing_support = + render_app.world().resource::(); + let use_gpu_instance_buffer_builder = + self.use_gpu_instance_buffer_builder && gpu_preprocessing_support.is_available(); + + let render_mesh_instances = RenderMeshInstances::new(use_gpu_instance_buffer_builder); + render_app.insert_resource(render_mesh_instances); + + if use_gpu_instance_buffer_builder { + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .add_systems( + ExtractSchedule, + extract_meshes_for_gpu_building.in_set(MeshExtractionSystems), + ) + .add_systems( + Render, + ( + gpu_preprocessing::write_batched_instance_buffers:: + .in_set(RenderSystems::PrepareResourcesFlush), + gpu_preprocessing::delete_old_work_item_buffers:: + .in_set(RenderSystems::PrepareResources), + collect_meshes_for_gpu_building + .in_set(RenderSystems::PrepareMeshes) + // This must be before + // `set_mesh_motion_vector_flags` so it doesn't + // overwrite those flags. + .before(set_mesh_motion_vector_flags), + ), + ); + } else { + let render_device = render_app.world().resource::(); + let cpu_batched_instance_buffer = no_gpu_preprocessing::BatchedInstanceBuffer::< + MeshUniform, + >::new(&render_device.limits()); + render_app + .insert_resource(cpu_batched_instance_buffer) + .add_systems( + ExtractSchedule, + extract_meshes_for_cpu_building.in_set(MeshExtractionSystems), + ) + .add_systems( + Render, + no_gpu_preprocessing::write_batched_instance_buffer:: + .in_set(RenderSystems::PrepareResourcesFlush), + ); + }; + + let render_device = render_app.world().resource::(); + if let Some(per_object_buffer_batch_size) = + GpuArrayBuffer::::batch_size(&render_device.limits()) + { + mesh_bindings_shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + render_app + .init_resource::() + .init_resource::(); + } + + // Load the mesh_bindings shader module here as it depends on runtime information about + // whether storage buffers are supported, or the maximum uniform buffer binding size. + load_shader_library!(app, "mesh_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone(), + }); + } +} + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewKeyCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewSpecializationTicks(HashMap); + +pub fn check_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &ExtractedView, + &Msaa, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + Has, + ( + Has, + Has, + Has, + Has, + ), + Option<&Camera3d>, + Has, + Option<&Projection>, + Has, + ( + Has>, + Has>, + ), + Has, + Has, + )>, + ticks: SystemChangeTick, +) { + for ( + view, + msaa, + tonemapping, + dither, + shadow_filter_method, + ssao, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + camera_3d, + temporal_jitter, + projection, + distance_fog, + (has_environment_maps, has_irradiance_volumes), + has_oit, + has_atmosphere, + ) in views.iter_mut() + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if has_oit { + view_key |= MeshPipelineKey::OIT_ENABLED; + } + + if has_atmosphere { + view_key |= MeshPipelineKey::ATMOSPHERE; + } + + if view.invert_culling { + view_key |= MeshPipelineKey::INVERT_CULLING; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + if let Some(camera_3d) = camera_3d { + view_key |= screen_space_specular_transmission_pipeline_key( + camera_3d.screen_space_specular_transmission_quality, + ); + } + if !view_key_cache + .get_mut(&view.retained_view_entity) + .is_some_and(|current_key| *current_key == view_key) + { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } +} + +#[derive(Component)] +pub struct MeshTransforms { + pub world_from_local: Affine3, + pub previous_world_from_local: Affine3, + pub flags: u32, +} + +#[derive(ShaderType, Clone)] +pub struct MeshUniform { + // Affine 4x3 matrices transposed to 3x4 + pub world_from_local: [Vec4; 3], + pub previous_world_from_local: [Vec4; 3], + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + pub local_from_world_transpose_a: [Vec4; 2], + pub local_from_world_transpose_b: f32, + pub flags: u32, + // Four 16-bit unsigned normalized UV values packed into a `UVec2`: + // + // <--- MSB LSB ---> + // +---- min v ----+ +---- min u ----+ + // lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + // +---- max v ----+ +---- max u ----+ + // lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + // + // (MSB: most significant bit; LSB: least significant bit.) + pub lightmap_uv_rect: UVec2, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// The current skin index, or `u32::MAX` if there's no skin. + pub current_skin_index: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Padding. + pub pad: u32, +} + +/// Information that has to be transferred from CPU to GPU in order to produce +/// the full [`MeshUniform`]. +/// +/// This is essentially a subset of the fields in [`MeshUniform`] above. +#[derive(ShaderType, Pod, Zeroable, Clone, Copy, Default, Debug)] +#[repr(C)] +pub struct MeshInputUniform { + /// Affine 4x3 matrix transposed to 3x4. + pub world_from_local: [Vec4; 3], + /// Four 16-bit unsigned normalized UV values packed into a `UVec2`: + /// + /// ```text + /// <--- MSB LSB ---> + /// +---- min v ----+ +---- min u ----+ + /// lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + /// +---- max v ----+ +---- max u ----+ + /// lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + /// + /// (MSB: most significant bit; LSB: least significant bit.) + /// ``` + pub lightmap_uv_rect: UVec2, + /// Various [`MeshFlags`]. + pub flags: u32, + /// The index of this mesh's [`MeshInputUniform`] in the previous frame's + /// buffer, if applicable. + /// + /// This is used for TAA. If not present, this will be `u32::MAX`. + pub previous_input_index: u32, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// The index of this mesh's first index in the index buffer, if any. + /// + /// Multiple meshes can be packed into a single index buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first index in + /// this mesh in that buffer. + /// + /// If this mesh isn't indexed, this value is ignored. + pub first_index_index: u32, + /// For an indexed mesh, the number of indices that make it up; for a + /// non-indexed mesh, the number of vertices in it. + pub index_count: u32, + /// The current skin index, or `u32::MAX` if there's no skin. + pub current_skin_index: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, + /// The number of the frame on which this [`MeshInputUniform`] was built. + /// + /// This is used to validate the previous transform and skin. If this + /// [`MeshInputUniform`] wasn't updated on this frame, then we know that + /// neither this mesh's transform nor that of its joints have been updated + /// on this frame, and therefore the transforms of both this mesh and its + /// joints must be identical to those for the previous frame. + pub timestamp: u32, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Padding. + pub pad: u32, +} + +/// Information about each mesh instance needed to cull it on GPU. +/// +/// This consists of its axis-aligned bounding box (AABB). +#[derive(ShaderType, Pod, Zeroable, Clone, Copy, Default)] +#[repr(C)] +pub struct MeshCullingData { + /// The 3D center of the AABB in model space, padded with an extra unused + /// float value. + pub aabb_center: Vec4, + /// The 3D extents of the AABB in model space, divided by two, padded with + /// an extra unused float value. + pub aabb_half_extents: Vec4, +} + +/// A GPU buffer that holds the information needed to cull meshes on GPU. +/// +/// At the moment, this simply holds each mesh's AABB. +/// +/// To avoid wasting CPU time in the CPU culling case, this buffer will be empty +/// if GPU culling isn't in use. +#[derive(Resource, Deref, DerefMut)] +pub struct MeshCullingDataBuffer(RawBufferVec); + +impl MeshUniform { + pub fn new( + mesh_transforms: &MeshTransforms, + first_vertex_index: u32, + material_bind_group_slot: MaterialBindGroupSlot, + maybe_lightmap: Option<(LightmapSlotIndex, Rect)>, + current_skin_index: Option, + tag: Option, + ) -> Self { + let (local_from_world_transpose_a, local_from_world_transpose_b) = + mesh_transforms.world_from_local.inverse_transpose_3x3(); + let lightmap_bind_group_slot = match maybe_lightmap { + None => u16::MAX, + Some((slot_index, _)) => slot_index.into(), + }; + + Self { + world_from_local: mesh_transforms.world_from_local.to_transpose(), + previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(), + lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap.map(|(_, uv_rect)| uv_rect)), + local_from_world_transpose_a, + local_from_world_transpose_b, + flags: mesh_transforms.flags, + first_vertex_index, + current_skin_index: current_skin_index.unwrap_or(u32::MAX), + material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot) + | ((lightmap_bind_group_slot as u32) << 16), + tag: tag.unwrap_or(0), + pad: 0, + } + } +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_types.wgsl! +bitflags::bitflags! { + /// Various flags and tightly-packed values on a mesh. + /// + /// Flags grow from the top bit down; other values grow from the bottom bit + /// up. + #[repr(transparent)] + pub struct MeshFlags: u32 { + /// Bitmask for the 16-bit index into the LOD array. + /// + /// This will be `u16::MAX` if this mesh has no LOD. + const LOD_INDEX_MASK = (1 << 16) - 1; + /// Disables frustum culling for this mesh. + /// + /// This corresponds to the + /// [`bevy_render::view::visibility::NoFrustumCulling`] component. + const NO_FRUSTUM_CULLING = 1 << 28; + const SHADOW_RECEIVER = 1 << 29; + const TRANSMITTED_SHADOW_RECEIVER = 1 << 30; + // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, + // then the flag should be set, else it should not be set. + const SIGN_DETERMINANT_MODEL_3X3 = 1 << 31; + const NONE = 0; + const UNINITIALIZED = 0xFFFFFFFF; + } +} + +impl MeshFlags { + fn from_components( + transform: &GlobalTransform, + lod_index: Option, + no_frustum_culling: bool, + not_shadow_receiver: bool, + transmitted_receiver: bool, + ) -> MeshFlags { + let mut mesh_flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if no_frustum_culling { + mesh_flags |= MeshFlags::NO_FRUSTUM_CULLING; + } + if transmitted_receiver { + mesh_flags |= MeshFlags::TRANSMITTED_SHADOW_RECEIVER; + } + if transform.affine().matrix3.determinant().is_sign_positive() { + mesh_flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + + let lod_index_bits = match lod_index { + None => u16::MAX, + Some(lod_index) => u16::from(lod_index), + }; + mesh_flags |= + MeshFlags::from_bits_retain((lod_index_bits as u32) << MeshFlags::LOD_INDEX_SHIFT); + + mesh_flags + } + + /// The first bit of the LOD index. + pub const LOD_INDEX_SHIFT: u32 = 0; +} + +bitflags::bitflags! { + /// Various useful flags for [`RenderMeshInstance`]s. + #[derive(Clone, Copy)] + pub struct RenderMeshInstanceFlags: u8 { + /// The mesh casts shadows. + const SHADOW_CASTER = 1 << 0; + /// The mesh can participate in automatic batching. + const AUTOMATIC_BATCHING = 1 << 1; + /// The mesh had a transform last frame and so is eligible for motion + /// vector computation. + const HAS_PREVIOUS_TRANSFORM = 1 << 2; + /// The mesh had a skin last frame and so that skin should be taken into + /// account for motion vector computation. + const HAS_PREVIOUS_SKIN = 1 << 3; + /// The mesh had morph targets last frame and so they should be taken + /// into account for motion vector computation. + const HAS_PREVIOUS_MORPH = 1 << 4; + } +} + +/// CPU data that the render world keeps for each entity, when *not* using GPU +/// mesh uniform building. +#[derive(Deref, DerefMut)] +pub struct RenderMeshInstanceCpu { + /// Data shared between both the CPU mesh uniform building and the GPU mesh + /// uniform building paths. + #[deref] + pub shared: RenderMeshInstanceShared, + /// The transform of the mesh. + /// + /// This will be written into the [`MeshUniform`] at the appropriate time. + pub transforms: MeshTransforms, +} + +/// CPU data that the render world needs to keep for each entity that contains a +/// mesh when using GPU mesh uniform building. +#[derive(Deref, DerefMut)] +pub struct RenderMeshInstanceGpu { + /// Data shared between both the CPU mesh uniform building and the GPU mesh + /// uniform building paths. + #[deref] + pub shared: RenderMeshInstanceShared, + /// The representative position of the mesh instance in world-space. + /// + /// This world-space center is used as a spatial proxy for view-dependent + /// operations such as distance computation and render-order sorting. + pub center: Vec3, + /// The index of the [`MeshInputUniform`] in the buffer. + pub current_uniform_index: NonMaxU32, +} + +/// CPU data that the render world needs to keep about each entity that contains +/// a mesh. +pub struct RenderMeshInstanceShared { + /// The [`AssetId`] of the mesh. + pub mesh_asset_id: AssetId, + /// A slot for the material bind group index. + pub material_bindings_index: MaterialBindingId, + /// Various flags. + pub flags: RenderMeshInstanceFlags, + /// Index of the slab that the lightmap resides in, if a lightmap is + /// present. + pub lightmap_slab_index: Option, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Render layers that this mesh instance belongs to. + pub render_layers: Option, + /// A representative position of the mesh instance in local space, + /// derived from its axis-aligned bounding box. + /// + /// This value is typically used as a spatial proxy for operations such as + /// view-dependent sorting (e.g., transparent object ordering). + pub center: Vec3, +} + +/// Information that is gathered during the parallel portion of mesh extraction +/// when GPU mesh uniform building is enabled. +/// +/// From this, the [`MeshInputUniform`] and [`RenderMeshInstanceGpu`] are +/// prepared. +pub struct RenderMeshInstanceGpuBuilder { + /// Data that will be placed on the [`RenderMeshInstanceGpu`]. + pub shared: RenderMeshInstanceShared, + /// The current transform. + pub world_from_local: Affine3, + /// Four 16-bit unsigned normalized UV values packed into a [`UVec2`]: + /// + /// ```text + /// <--- MSB LSB ---> + /// +---- min v ----+ +---- min u ----+ + /// lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + /// +---- max v ----+ +---- max u ----+ + /// lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + /// + /// (MSB: most significant bit; LSB: least significant bit.) + /// ``` + pub lightmap_uv_rect: UVec2, + /// The index of the previous mesh input. + pub previous_input_index: Option, + /// Various flags. + pub mesh_flags: MeshFlags, +} + +/// The per-thread queues used during [`extract_meshes_for_gpu_building`]. +/// +/// There are two varieties of these: one for when culling happens on CPU and +/// one for when culling happens on GPU. Having the two varieties avoids wasting +/// space if GPU culling is disabled. +#[derive(Default)] +pub enum RenderMeshInstanceGpuQueue { + /// The default value. + /// + /// This becomes [`RenderMeshInstanceGpuQueue::CpuCulling`] or + /// [`RenderMeshInstanceGpuQueue::GpuCulling`] once extraction starts. + #[default] + None, + /// The version of [`RenderMeshInstanceGpuQueue`] that omits the + /// [`MeshCullingData`], so that we don't waste space when GPU + /// culling is disabled. + CpuCulling { + /// Stores GPU data for each entity that became visible or changed in + /// such a way that necessitates updating the [`MeshInputUniform`] (e.g. + /// changed transform). + changed: Vec<(MainEntity, RenderMeshInstanceGpuBuilder)>, + /// Stores the IDs of entities that became invisible this frame. + removed: Vec, + }, + /// The version of [`RenderMeshInstanceGpuQueue`] that contains the + /// [`MeshCullingData`], used when any view has GPU culling + /// enabled. + GpuCulling { + /// Stores GPU data for each entity that became visible or changed in + /// such a way that necessitates updating the [`MeshInputUniform`] (e.g. + /// changed transform). + changed: Vec<(MainEntity, RenderMeshInstanceGpuBuilder, MeshCullingData)>, + /// Stores the IDs of entities that became invisible this frame. + removed: Vec, + }, +} + +/// The per-thread queues containing mesh instances, populated during the +/// extract phase. +/// +/// These are filled in [`extract_meshes_for_gpu_building`] and consumed in +/// [`collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMeshInstanceGpuQueues(Parallel); + +/// Holds a list of meshes that couldn't be extracted this frame because their +/// materials weren't prepared yet. +/// +/// On subsequent frames, we try to reextract those meshes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct MeshesToReextractNextFrame(MainEntityHashSet); + +impl RenderMeshInstanceShared { + /// A gpu builder will provide the mesh instance id + /// during [`RenderMeshInstanceGpuBuilder::update`]. + fn for_gpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + not_shadow_caster: bool, + no_automatic_batching: bool, + render_layers: Option<&RenderLayers>, + aabb: Option<&Aabb>, + ) -> Self { + Self::for_cpu_building( + previous_transform, + mesh, + tag, + default(), + not_shadow_caster, + no_automatic_batching, + render_layers, + aabb, + ) + } + + /// The cpu builder does not have an equivalent [`RenderMeshInstanceGpuBuilder::update`]. + fn for_cpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + material_bindings_index: MaterialBindingId, + not_shadow_caster: bool, + no_automatic_batching: bool, + render_layers: Option<&RenderLayers>, + aabb: Option<&Aabb>, + ) -> Self { + let mut mesh_instance_flags = RenderMeshInstanceFlags::empty(); + mesh_instance_flags.set(RenderMeshInstanceFlags::SHADOW_CASTER, !not_shadow_caster); + mesh_instance_flags.set( + RenderMeshInstanceFlags::AUTOMATIC_BATCHING, + !no_automatic_batching, + ); + mesh_instance_flags.set( + RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM, + previous_transform.is_some(), + ); + + RenderMeshInstanceShared { + mesh_asset_id: mesh.id(), + flags: mesh_instance_flags, + material_bindings_index, + lightmap_slab_index: None, + tag: tag.map_or(0, |i| **i), + render_layers: render_layers.cloned(), + center: aabb.map_or(Vec3::ZERO, |aabb| aabb.center.into()), + } + } + + /// Returns true if this entity is eligible to participate in automatic + /// batching. + #[inline] + pub fn should_batch(&self) -> bool { + self.flags + .contains(RenderMeshInstanceFlags::AUTOMATIC_BATCHING) + } +} + +/// Information that the render world keeps about each entity that contains a +/// mesh. +/// +/// The set of information needed is different depending on whether CPU or GPU +/// [`MeshUniform`] building is in use. +#[derive(Resource)] +pub enum RenderMeshInstances { + /// Information needed when using CPU mesh instance data building. + CpuBuilding(RenderMeshInstancesCpu), + /// Information needed when using GPU mesh instance data building. + GpuBuilding(RenderMeshInstancesGpu), +} + +/// Information that the render world keeps about each entity that contains a +/// mesh, when using CPU mesh instance data building. +#[derive(Default, Deref, DerefMut)] +pub struct RenderMeshInstancesCpu(MainEntityHashMap); + +/// Information that the render world keeps about each entity that contains a +/// mesh, when using GPU mesh instance data building. +#[derive(Default, Deref, DerefMut)] +pub struct RenderMeshInstancesGpu(MainEntityHashMap); + +impl RenderMeshInstances { + /// Creates a new [`RenderMeshInstances`] instance. + fn new(use_gpu_instance_buffer_builder: bool) -> RenderMeshInstances { + if use_gpu_instance_buffer_builder { + RenderMeshInstances::GpuBuilding(RenderMeshInstancesGpu::default()) + } else { + RenderMeshInstances::CpuBuilding(RenderMeshInstancesCpu::default()) + } + } + + /// Returns the ID of the mesh asset attached to the given entity, if any. + pub fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + match *self { + RenderMeshInstances::CpuBuilding(ref instances) => instances.mesh_asset_id(entity), + RenderMeshInstances::GpuBuilding(ref instances) => instances.mesh_asset_id(entity), + } + } + + /// Constructs [`RenderMeshQueueData`] for the given entity, if it has a + /// mesh attached. + pub fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + match *self { + RenderMeshInstances::CpuBuilding(ref instances) => { + instances.render_mesh_queue_data(entity) + } + RenderMeshInstances::GpuBuilding(ref instances) => { + instances.render_mesh_queue_data(entity) + } + } + } + + /// Inserts the given flags into the CPU or GPU render mesh instance data + /// for the given mesh as appropriate. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + match *self { + RenderMeshInstances::CpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + RenderMeshInstances::GpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + } + } +} + +impl RenderMeshInstancesCpu { + fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| render_mesh_instance.mesh_asset_id) + } + + fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + self.get(&entity).map(|render_mesh_instance| { + let world_from_local = &render_mesh_instance.transforms.world_from_local; + let center = world_from_local + .matrix3 + .mul_vec3(render_mesh_instance.shared.center) + + world_from_local.translation; + + RenderMeshQueueData { + shared: &render_mesh_instance.shared, + center, + current_uniform_index: InputUniformIndex::default(), + } + }) + } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } +} + +impl RenderMeshInstancesGpu { + fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| render_mesh_instance.mesh_asset_id) + } + + fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| RenderMeshQueueData { + shared: &render_mesh_instance.shared, + center: render_mesh_instance.center, + current_uniform_index: InputUniformIndex( + render_mesh_instance.current_uniform_index.into(), + ), + }) + } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } +} + +impl RenderMeshInstanceGpuQueue { + /// Clears out a [`RenderMeshInstanceGpuQueue`], creating or recreating it + /// as necessary. + /// + /// `any_gpu_culling` should be set to true if any view has GPU culling + /// enabled. + fn init(&mut self, any_gpu_culling: bool) { + match (any_gpu_culling, &mut *self) { + (true, RenderMeshInstanceGpuQueue::GpuCulling { changed, removed }) => { + changed.clear(); + removed.clear(); + } + (true, _) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![], + removed: vec![], + } + } + (false, RenderMeshInstanceGpuQueue::CpuCulling { changed, removed }) => { + changed.clear(); + removed.clear(); + } + (false, _) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![], + removed: vec![], + } + } + } + } + + /// Adds a new mesh to this queue. + fn push( + &mut self, + entity: MainEntity, + instance_builder: RenderMeshInstanceGpuBuilder, + culling_data_builder: Option, + ) { + match (&mut *self, culling_data_builder) { + ( + &mut RenderMeshInstanceGpuQueue::CpuCulling { + changed: ref mut queue, + .. + }, + None, + ) => { + queue.push((entity, instance_builder)); + } + ( + &mut RenderMeshInstanceGpuQueue::GpuCulling { + changed: ref mut queue, + .. + }, + Some(culling_data_builder), + ) => { + queue.push((entity, instance_builder, culling_data_builder)); + } + (_, None) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![(entity, instance_builder)], + removed: vec![], + }; + } + (_, Some(culling_data_builder)) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![(entity, instance_builder, culling_data_builder)], + removed: vec![], + }; + } + } + } + + /// Adds the given entity to the `removed` list, queuing it for removal. + /// + /// The `gpu_culling` parameter specifies whether GPU culling is enabled. + fn remove(&mut self, entity: MainEntity, gpu_culling: bool) { + match (&mut *self, gpu_culling) { + (RenderMeshInstanceGpuQueue::None, false) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![], + removed: vec![entity], + } + } + (RenderMeshInstanceGpuQueue::None, true) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![], + removed: vec![entity], + } + } + (RenderMeshInstanceGpuQueue::CpuCulling { removed, .. }, _) + | (RenderMeshInstanceGpuQueue::GpuCulling { removed, .. }, _) => { + removed.push(entity); + } + } + } +} + +impl RenderMeshInstanceGpuBuilder { + /// Flushes this mesh instance to the [`RenderMeshInstanceGpu`] and + /// [`MeshInputUniform`] tables, replacing the existing entry if applicable. + fn update( + mut self, + entity: MainEntity, + render_mesh_instances: &mut MainEntityHashMap, + current_input_buffer: &mut InstanceInputUniformBuffer, + previous_input_buffer: &mut InstanceInputUniformBuffer, + mesh_allocator: &MeshAllocator, + mesh_material_ids: &RenderMaterialInstances, + render_material_bindings: &RenderMaterialBindings, + render_lightmaps: &RenderLightmaps, + skin_uniforms: &SkinUniforms, + timestamp: FrameCount, + meshes_to_reextract_next_frame: &mut MeshesToReextractNextFrame, + ) -> Option { + let (first_vertex_index, vertex_count) = + match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) { + Some(mesh_vertex_slice) => ( + mesh_vertex_slice.range.start, + mesh_vertex_slice.range.end - mesh_vertex_slice.range.start, + ), + None => (0, 0), + }; + let (mesh_is_indexed, first_index_index, index_count) = + match mesh_allocator.mesh_index_slice(&self.shared.mesh_asset_id) { + Some(mesh_index_slice) => ( + true, + mesh_index_slice.range.start, + mesh_index_slice.range.end - mesh_index_slice.range.start, + ), + None => (false, 0, 0), + }; + let current_skin_index = match skin_uniforms.skin_byte_offset(entity) { + Some(skin_index) => skin_index.index(), + None => u32::MAX, + }; + + // Look up the material index. If we couldn't fetch the material index, + // then the material hasn't been prepared yet, perhaps because it hasn't + // yet loaded. In that case, add the mesh to + // `meshes_to_reextract_next_frame` and bail. + let mesh_material = mesh_material_ids.mesh_material(entity); + let mesh_material_binding_id = if mesh_material != DUMMY_MESH_MATERIAL.untyped() { + match render_material_bindings.get(&mesh_material) { + Some(binding_id) => *binding_id, + None => { + meshes_to_reextract_next_frame.insert(entity); + return None; + } + } + } else { + // Use a dummy material binding ID. + MaterialBindingId::default() + }; + self.shared.material_bindings_index = mesh_material_binding_id; + + let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) { + Some(render_lightmap) => u16::from(*render_lightmap.slot_index), + None => u16::MAX, + }; + let lightmap_slab_index = render_lightmaps + .render_lightmaps + .get(&entity) + .map(|lightmap| lightmap.slab_index); + self.shared.lightmap_slab_index = lightmap_slab_index; + + // Create the mesh input uniform. + let mut mesh_input_uniform = MeshInputUniform { + world_from_local: self.world_from_local.to_transpose(), + lightmap_uv_rect: self.lightmap_uv_rect, + flags: self.mesh_flags.bits(), + previous_input_index: u32::MAX, + timestamp: timestamp.0, + first_vertex_index, + first_index_index, + index_count: if mesh_is_indexed { + index_count + } else { + vertex_count + }, + current_skin_index, + material_and_lightmap_bind_group_slot: u32::from( + self.shared.material_bindings_index.slot, + ) | ((lightmap_slot as u32) << 16), + tag: self.shared.tag, + pad: 0, + }; + + // Did the last frame contain this entity as well? + let current_uniform_index; + let world_from_local = &self.world_from_local; + let center = + world_from_local.matrix3.mul_vec3(self.shared.center) + world_from_local.translation; + + match render_mesh_instances.entry(entity) { + Entry::Occupied(mut occupied_entry) => { + // Yes, it did. Replace its entry with the new one. + + // Reserve a slot. + current_uniform_index = u32::from(occupied_entry.get_mut().current_uniform_index); + + // Save the old mesh input uniform. The mesh preprocessing + // shader will need it to compute motion vectors. + let previous_mesh_input_uniform = + current_input_buffer.get_unchecked(current_uniform_index); + let previous_input_index = previous_input_buffer.add(previous_mesh_input_uniform); + mesh_input_uniform.previous_input_index = previous_input_index; + + // Write in the new mesh input uniform. + current_input_buffer.set(current_uniform_index, mesh_input_uniform); + + occupied_entry.replace_entry_with(|_, _| { + Some(RenderMeshInstanceGpu { + shared: self.shared, + center, + current_uniform_index: NonMaxU32::new(current_uniform_index) + .unwrap_or_default(), + }) + }); + } + + Entry::Vacant(vacant_entry) => { + // No, this is a new entity. Push its data on to the buffer. + current_uniform_index = current_input_buffer.add(mesh_input_uniform); + + vacant_entry.insert(RenderMeshInstanceGpu { + shared: self.shared, + center, + current_uniform_index: NonMaxU32::new(current_uniform_index) + .unwrap_or_default(), + }); + } + } + + Some(current_uniform_index) + } +} + +/// Removes a [`MeshInputUniform`] corresponding to an entity that became +/// invisible from the buffer. +fn remove_mesh_input_uniform( + entity: MainEntity, + render_mesh_instances: &mut MainEntityHashMap, + current_input_buffer: &mut InstanceInputUniformBuffer, +) -> Option { + // Remove the uniform data. + let removed_render_mesh_instance = render_mesh_instances.remove(&entity)?; + + let removed_uniform_index = removed_render_mesh_instance.current_uniform_index.get(); + current_input_buffer.remove(removed_uniform_index); + Some(removed_uniform_index) +} + +impl MeshCullingData { + /// Returns a new [`MeshCullingData`] initialized with the given AABB. + /// + /// If no AABB is provided, an infinitely-large one is conservatively + /// chosen. + fn new(aabb: Option<&Aabb>) -> Self { + match aabb { + Some(aabb) => MeshCullingData { + aabb_center: aabb.center.extend(0.0), + aabb_half_extents: aabb.half_extents.extend(0.0), + }, + None => MeshCullingData { + aabb_center: Vec3::ZERO.extend(0.0), + aabb_half_extents: Vec3::INFINITY.extend(0.0), + }, + } + } + + /// Flushes this mesh instance culling data to the + /// [`MeshCullingDataBuffer`], replacing the existing entry if applicable. + fn update( + &self, + mesh_culling_data_buffer: &mut MeshCullingDataBuffer, + instance_data_index: usize, + ) { + while mesh_culling_data_buffer.len() < instance_data_index + 1 { + mesh_culling_data_buffer.push(MeshCullingData::default()); + } + mesh_culling_data_buffer.values_mut()[instance_data_index] = *self; + } +} + +impl Default for MeshCullingDataBuffer { + #[inline] + fn default() -> Self { + Self(RawBufferVec::new(BufferUsages::STORAGE)) + } +} + +/// Data that [`crate::material::queue_material_meshes`] and similar systems +/// need in order to place entities that contain meshes in the right batch. +#[derive(Deref)] +pub struct RenderMeshQueueData<'a> { + /// General information about the mesh instance. + #[deref] + pub shared: &'a RenderMeshInstanceShared, + /// The representative position of the mesh instance in world-space. + /// + /// This world-space center is used as a spatial proxy for view-dependent + /// operations such as distance computation and render-order sorting. + pub center: Vec3, + /// The index of the [`MeshInputUniform`] in the GPU buffer for this mesh + /// instance. + pub current_uniform_index: InputUniformIndex, +} + +/// A [`SystemSet`] that encompasses both [`extract_meshes_for_cpu_building`] +/// and [`extract_meshes_for_gpu_building`]. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct MeshExtractionSystems; + +/// Extracts meshes from the main world into the render world, populating the +/// [`RenderMeshInstances`]. +/// +/// This is the variant of the system that runs when we're *not* using GPU +/// [`MeshUniform`] building. +pub fn extract_meshes_for_cpu_building( + mut render_mesh_instances: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, + render_visibility_ranges: Res, + mut render_mesh_instance_queues: Local>>, + meshes_query: Extract< + Query<( + Entity, + &ViewVisibility, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + &Mesh3d, + Option<&MeshTag>, + Has, + Has, + Has, + Has, + Has, + Has, + Option<&RenderLayers>, + Option<&Aabb>, + )>, + >, +) { + meshes_query.par_iter().for_each_init( + || render_mesh_instance_queues.borrow_local_mut(), + |queue, + ( + entity, + view_visibility, + transform, + previous_transform, + mesh, + tag, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + not_shadow_caster, + no_automatic_batching, + visibility_range, + render_layers, + aabb, + )| { + if !view_visibility.get() { + return; + } + + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + ); + + let mesh_material = mesh_material_ids.mesh_material(MainEntity::from(entity)); + + let material_bindings_index = render_material_bindings + .get(&mesh_material) + .copied() + .unwrap_or_default(); + + let shared = RenderMeshInstanceShared::for_cpu_building( + previous_transform, + mesh, + tag, + material_bindings_index, + not_shadow_caster, + no_automatic_batching, + render_layers, + aabb, + ); + + let world_from_local = transform.affine(); + queue.push(( + entity, + RenderMeshInstanceCpu { + transforms: MeshTransforms { + world_from_local: (&world_from_local).into(), + previous_world_from_local: (&previous_transform + .map(|t| t.0) + .unwrap_or(world_from_local)) + .into(), + flags: mesh_flags.bits(), + }, + shared, + }, + )); + }, + ); + + // Collect the render mesh instances. + let RenderMeshInstances::CpuBuilding(ref mut render_mesh_instances) = *render_mesh_instances + else { + panic!( + "`extract_meshes_for_cpu_building` should only be called if we're using CPU \ + `MeshUniform` building" + ); + }; + + render_mesh_instances.clear(); + for queue in render_mesh_instance_queues.iter_mut() { + for (entity, render_mesh_instance) in queue.drain(..) { + render_mesh_instances.insert(entity.into(), render_mesh_instance); + } + } +} + +/// All the data that we need from a mesh in the main world. +type GpuMeshExtractionQuery = ( + Entity, + Read, + Read, + Option>, + Option>, + Option>, + Read, + Option>, + Has, + Has, + Has, + Has, + Has, + Has, + Option>, +); + +/// Extracts meshes from the main world into the render world and queues +/// [`MeshInputUniform`]s to be uploaded to the GPU. +/// +/// This is optimized to only look at entities that have changed since the last +/// frame. +/// +/// This is the variant of the system that runs when we're using GPU +/// [`MeshUniform`] building. +pub fn extract_meshes_for_gpu_building( + mut render_mesh_instances: ResMut, + render_visibility_ranges: Res, + mut render_mesh_instance_queues: ResMut, + changed_meshes_query: Extract< + Query< + GpuMeshExtractionQuery, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + >, + >, + ( + mut removed_previous_global_transform_query, + mut removed_lightmap_query, + mut removed_aabb_query, + mut removed_mesh_tag_query, + mut removed_no_frustum_culling_query, + mut removed_not_shadow_receiver_query, + mut removed_transmitted_receiver_query, + mut removed_not_shadow_caster_query, + mut removed_no_automatic_batching_query, + mut removed_visibility_range_query, + mut removed_skinned_mesh_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + all_meshes_query: Extract>, + mut removed_meshes_query: Extract>, + gpu_culling_query: Extract, Without)>>, + meshes_to_reextract_next_frame: ResMut, + mut reextract_entities: Local, +) { + reextract_entities.clear(); + + let any_gpu_culling = !gpu_culling_query.is_empty(); + + for render_mesh_instance_queue in render_mesh_instance_queues.iter_mut() { + render_mesh_instance_queue.init(any_gpu_culling); + } + + // Collect render mesh instances. Build up the uniform buffer. + + let RenderMeshInstances::GpuBuilding(ref mut render_mesh_instances) = *render_mesh_instances + else { + panic!( + "`extract_meshes_for_gpu_building` should only be called if we're \ + using GPU `MeshUniform` building" + ); + }; + + // Find all meshes that have changed, and record information needed to + // construct the `MeshInputUniform` for them. + changed_meshes_query.par_iter().for_each_init( + || render_mesh_instance_queues.borrow_local_mut(), + |queue, query_row| { + extract_mesh_for_gpu_building( + query_row, + &render_visibility_ranges, + render_mesh_instances, + queue, + any_gpu_culling, + ); + }, + ); + + // Process materials that `collect_meshes_for_gpu_building` marked as + // needing to be reextracted. This will happen when we extracted a mesh on + // some previous frame, but its material hadn't been prepared yet, perhaps + // because the material hadn't yet been loaded. We reextract such materials + // on subsequent frames so that `collect_meshes_for_gpu_building` will check + // to see if their materials have been prepared. + let iters = meshes_to_reextract_next_frame + .iter() + .map(|&e| *e) + .chain(removed_previous_global_transform_query.read()) + .chain(removed_lightmap_query.read()) + .chain(removed_aabb_query.read()) + .chain(removed_mesh_tag_query.read()) + .chain(removed_no_frustum_culling_query.read()) + .chain(removed_not_shadow_receiver_query.read()) + .chain(removed_transmitted_receiver_query.read()) + .chain(removed_not_shadow_caster_query.read()) + .chain(removed_no_automatic_batching_query.read()) + .chain(removed_visibility_range_query.read()) + .chain(removed_skinned_mesh_query.read()); + + reextract_entities.extend_from_iter(iters); + + let mut queue = render_mesh_instance_queues.borrow_local_mut(); + for entity in &reextract_entities { + if let Ok(query_row) = all_meshes_query.get(*entity) { + extract_mesh_for_gpu_building( + query_row, + &render_visibility_ranges, + render_mesh_instances, + &mut queue, + any_gpu_culling, + ); + } + } + + // Also record info about each mesh that became invisible. + for entity in removed_meshes_query.read() { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + let entity = MainEntity::from(entity); + if !changed_meshes_query.contains(*entity) + && !meshes_to_reextract_next_frame.contains(&entity) + { + queue.remove(entity, any_gpu_culling); + } + } +} + +fn extract_mesh_for_gpu_building( + ( + entity, + view_visibility, + transform, + previous_transform, + lightmap, + aabb, + mesh, + tag, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + not_shadow_caster, + no_automatic_batching, + visibility_range, + render_layers, + ): ::Item<'_, '_>, + render_visibility_ranges: &RenderVisibilityRanges, + render_mesh_instances: &RenderMeshInstancesGpu, + queue: &mut RenderMeshInstanceGpuQueue, + any_gpu_culling: bool, +) { + if !view_visibility.get() { + queue.remove(entity.into(), any_gpu_culling); + return; + } + + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + ); + + let shared = RenderMeshInstanceShared::for_gpu_building( + previous_transform, + mesh, + tag, + not_shadow_caster, + no_automatic_batching, + render_layers, + aabb, + ); + + let lightmap_uv_rect = pack_lightmap_uv_rect(lightmap.map(|lightmap| lightmap.uv_rect)); + + let gpu_mesh_culling_data = any_gpu_culling.then(|| MeshCullingData::new(aabb)); + + let previous_input_index = if shared + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM) + { + render_mesh_instances + .get(&MainEntity::from(entity)) + .map(|render_mesh_instance| render_mesh_instance.current_uniform_index) + } else { + None + }; + + let gpu_mesh_instance_builder = RenderMeshInstanceGpuBuilder { + shared, + world_from_local: (&transform.affine()).into(), + lightmap_uv_rect, + mesh_flags, + previous_input_index, + }; + + queue.push( + entity.into(), + gpu_mesh_instance_builder, + gpu_mesh_culling_data, + ); +} + +/// A system that sets the [`RenderMeshInstanceFlags`] for each mesh based on +/// whether the previous frame had skins and/or morph targets. +/// +/// Ordinarily, [`RenderMeshInstanceFlags`] are set during the extraction phase. +/// However, we can't do that for the flags related to skins and morph targets +/// because the previous frame's skin and morph targets are the responsibility +/// of [`extract_skins`] and [`extract_morphs`] respectively. We want to run +/// those systems in parallel with mesh extraction for performance, so we need +/// to defer setting of these mesh instance flags to after extraction, which +/// this system does. An alternative to having skin- and morph-target-related +/// data in [`RenderMeshInstanceFlags`] would be to have +/// [`crate::material::queue_material_meshes`] check the skin and morph target +/// tables for each mesh, but that would be too slow in the hot mesh queuing +/// loop. +pub fn set_mesh_motion_vector_flags( + mut render_mesh_instances: ResMut, + skin_uniforms: Res, + morph_indices: Res, +) { + for &entity in skin_uniforms.all_skins() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN); + } + for &entity in morph_indices.prev.keys() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH); + } +} + +/// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU +/// mesh uniforms are built. +pub fn collect_meshes_for_gpu_building( + render_mesh_instances: ResMut, + batched_instance_buffers: ResMut< + gpu_preprocessing::BatchedInstanceBuffers, + >, + mut mesh_culling_data_buffer: ResMut, + mut render_mesh_instance_queues: ResMut, + mesh_allocator: Res, + mesh_material_ids: Res, + render_material_bindings: Res, + render_lightmaps: Res, + skin_uniforms: Res, + frame_count: Res, + mut meshes_to_reextract_next_frame: ResMut, +) { + let RenderMeshInstances::GpuBuilding(render_mesh_instances) = + render_mesh_instances.into_inner() + else { + return; + }; + + // We're going to rebuild `meshes_to_reextract_next_frame`. + meshes_to_reextract_next_frame.clear(); + + // Collect render mesh instances. Build up the uniform buffer. + let gpu_preprocessing::BatchedInstanceBuffers { + current_input_buffer, + previous_input_buffer, + .. + } = batched_instance_buffers.into_inner(); + + previous_input_buffer.clear(); + + // Build the [`RenderMeshInstance`]s and [`MeshInputUniform`]s. + + for queue in render_mesh_instance_queues.iter_mut() { + match *queue { + RenderMeshInstanceGpuQueue::None => { + // This can only happen if the queue is empty. + } + + RenderMeshInstanceGpuQueue::CpuCulling { + ref mut changed, + ref mut removed, + } => { + for (entity, mesh_instance_builder) in changed.drain(..) { + mesh_instance_builder.update( + entity, + &mut *render_mesh_instances, + current_input_buffer, + previous_input_buffer, + &mesh_allocator, + &mesh_material_ids, + &render_material_bindings, + &render_lightmaps, + &skin_uniforms, + *frame_count, + &mut meshes_to_reextract_next_frame, + ); + } + + for entity in removed.drain(..) { + remove_mesh_input_uniform( + entity, + &mut *render_mesh_instances, + current_input_buffer, + ); + } + } + + RenderMeshInstanceGpuQueue::GpuCulling { + ref mut changed, + ref mut removed, + } => { + for (entity, mesh_instance_builder, mesh_culling_builder) in changed.drain(..) { + let Some(instance_data_index) = mesh_instance_builder.update( + entity, + &mut *render_mesh_instances, + current_input_buffer, + previous_input_buffer, + &mesh_allocator, + &mesh_material_ids, + &render_material_bindings, + &render_lightmaps, + &skin_uniforms, + *frame_count, + &mut meshes_to_reextract_next_frame, + ) else { + continue; + }; + mesh_culling_builder + .update(&mut mesh_culling_data_buffer, instance_data_index as usize); + } + + for entity in removed.drain(..) { + remove_mesh_input_uniform( + entity, + &mut *render_mesh_instances, + current_input_buffer, + ); + } + } + } + } + + // Buffers can't be empty. Make sure there's something in the previous input buffer. + previous_input_buffer.ensure_nonempty(); +} + +/// All data needed to construct a pipeline for rendering 3D meshes. +#[derive(Resource, Clone)] +pub struct MeshPipeline { + /// A reference to all the mesh pipeline view layouts. + pub view_layouts: MeshPipelineViewLayouts, + pub clustered_forward_buffer_binding_type: BufferBindingType, + pub mesh_layouts: MeshLayouts, + /// The shader asset handle. + pub shader: Handle, + /// `MeshUniform`s are stored in arrays in buffers. If storage buffers are available, they + /// are used and this will be `None`, otherwise uniform buffers will be used with batches + /// of this many `MeshUniform`s, stored at dynamic offsets within the uniform buffer. + /// Use code like this in custom shaders: + /// ```wgsl + /// ##ifdef PER_OBJECT_BUFFER_BATCH_SIZE + /// @group(1) @binding(0) var mesh: array; + /// ##else + /// @group(1) @binding(0) var mesh: array; + /// ##endif // PER_OBJECT_BUFFER_BATCH_SIZE + /// ``` + pub per_object_buffer_batch_size: Option, + + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + /// + /// This affects whether reflection probes can be used. + pub binding_arrays_are_usable: bool, + + /// Whether clustered decals are usable on the current render device. + pub clustered_decals_are_usable: bool, + + /// Whether skins will use uniform buffers on account of storage buffers + /// being unavailable on this platform. + pub skins_use_uniform_buffers: bool, +} + +impl FromWorld for MeshPipeline { + fn from_world(world: &mut World) -> Self { + let shader = load_embedded_asset!(world, "mesh.wgsl"); + let mut system_state: SystemState<( + Res, + Res, + Res, + )> = SystemState::new(world); + let (render_device, render_adapter, view_layouts) = system_state.get_mut(world); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + + MeshPipeline { + view_layouts: view_layouts.clone(), + clustered_forward_buffer_binding_type, + mesh_layouts: MeshLayouts::new(&render_device, &render_adapter), + shader, + per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( + &render_device.limits(), + ), + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + clustered_decals_are_usable: decal::clustered::clustered_decals_are_usable( + &render_device, + &render_adapter, + ), + skins_use_uniform_buffers: skins_use_uniform_buffers(&render_device.limits()), + } + } +} + +impl MeshPipeline { + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { + self.view_layouts.get_view_layout(layout_key) + } +} + +/// A 1x1x1 'all 1.0' texture to use as a dummy texture in place of optional [`crate::pbr_material::StandardMaterial`] textures +pub fn build_dummy_white_gpu_image( + render_device: Res, + default_sampler: Res, + render_queue: Res, +) -> GpuImage { + let image = Image::default(); + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = match image.sampler { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(ref descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + + if let Ok(format_size) = image.texture_descriptor.format.pixel_size() { + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image was created without data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + } + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + texture_view_format: image.texture_view_descriptor.and_then(|v| v.format), + sampler, + size: image.texture_descriptor.size, + mip_level_count: image.texture_descriptor.mip_level_count, + had_data: true, + } +} + +pub fn get_image_texture<'a>( + dummy_white_gpu_image: &'a GpuImage, + gpu_images: &'a RenderAssets, + handle_option: &Option>, +) -> Option<(&'a TextureView, &'a Sampler)> { + if let Some(handle) = handle_option { + let gpu_image = gpu_images.get(handle)?; + Some((&gpu_image.texture_view, &gpu_image.sampler)) + } else { + Some(( + &dummy_white_gpu_image.texture_view, + &dummy_white_gpu_image.sampler, + )) + } +} + +impl GetBatchData for MeshPipeline { + type Param = ( + SRes, + SRes, + SRes>, + SRes, + SRes, + ); + // The material bind group ID, the mesh ID, and the lightmap ID, + // respectively. + type CompareData = ( + MaterialBindGroupIndex, + AssetId, + Option, + ); + + type BufferData = MeshUniform; + + fn get_batch_data( + (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + Self::Param, + >, + (_entity, main_entity): (Entity, MainEntity), + ) -> Option<(Self::BufferData, Option)> { + let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_batch_data` should never be called in GPU mesh uniform \ + building mode" + ); + return None; + }; + let mesh_instance = mesh_instances.get(&main_entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + let current_skin_index = skin_uniforms.skin_index(main_entity); + let material_bind_group_index = mesh_instance.material_bindings_index; + + Some(( + MeshUniform::new( + &mesh_instance.transforms, + first_vertex_index, + material_bind_group_index.slot, + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), + current_skin_index, + Some(mesh_instance.tag), + ), + mesh_instance.should_batch().then_some(( + material_bind_group_index.group, + mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.slab_index), + )), + )) + } +} + +impl GetFullBatchData for MeshPipeline { + type BufferInputData = MeshInputUniform; + + fn get_index_and_compare_data( + (mesh_instances, lightmaps, _, _, _): &SystemParamItem, + main_entity: MainEntity, + ) -> Option<(NonMaxU32, Option)> { + // This should only be called during GPU building. + let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_index_and_compare_data` should never be called in CPU mesh uniform building \ + mode" + ); + return None; + }; + + let mesh_instance = mesh_instances.get(&main_entity)?; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + Some(( + mesh_instance.current_uniform_index, + mesh_instance.should_batch().then_some(( + mesh_instance.material_bindings_index.group, + mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.slab_index), + )), + )) + } + + fn get_binned_batch_data( + (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + Self::Param, + >, + main_entity: MainEntity, + ) -> Option { + let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_binned_batch_data` should never be called in GPU mesh uniform building mode" + ); + return None; + }; + let mesh_instance = mesh_instances.get(&main_entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + let current_skin_index = skin_uniforms.skin_index(main_entity); + + Some(MeshUniform::new( + &mesh_instance.transforms, + first_vertex_index, + mesh_instance.material_bindings_index.slot, + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), + current_skin_index, + Some(mesh_instance.tag), + )) + } + + fn get_binned_index( + (mesh_instances, _, _, _, _): &SystemParamItem, + main_entity: MainEntity, + ) -> Option { + // This should only be called during GPU building. + let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_binned_index` should never be called in CPU mesh uniform \ + building mode" + ); + return None; + }; + + mesh_instances + .get(&main_entity) + .map(|entity| entity.current_uniform_index) + } + + fn write_batch_indirect_parameters_metadata( + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + phase_indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers, + indirect_parameters_offset: u32, + ) { + let indirect_parameters = IndirectParametersCpuMetadata { + base_output_index, + batch_set_index: match batch_set_index { + Some(batch_set_index) => u32::from(batch_set_index), + None => !0, + }, + }; + + if indexed { + phase_indirect_parameters_buffers + .indexed + .set(indirect_parameters_offset, indirect_parameters); + } else { + phase_indirect_parameters_buffers + .non_indexed + .set(indirect_parameters_offset, indirect_parameters); + } + } +} + +bitflags::bitflags! { + #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[repr(transparent)] + // NOTE: Apparently quadro drivers support up to 64x MSAA. + /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. + pub struct MeshPipelineKey: u64 { + // Nothing + const NONE = 0; + + // Inherited bits + const MORPH_TARGETS = BaseMeshPipelineKey::MORPH_TARGETS.bits(); + + // Flag bits + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; + const DEPTH_PREPASS = 1 << 3; + const NORMAL_PREPASS = 1 << 4; + const DEFERRED_PREPASS = 1 << 5; + const MOTION_VECTOR_PREPASS = 1 << 6; + const MAY_DISCARD = 1 << 7; // Guards shader codepaths that may discard, allowing early depth tests in most cases + // See: https://www.khronos.org/opengl/wiki/Early_Fragment_Test + const ENVIRONMENT_MAP = 1 << 8; + const SCREEN_SPACE_AMBIENT_OCCLUSION = 1 << 9; + const UNCLIPPED_DEPTH_ORTHO = 1 << 10; // Disables depth clipping for use with directional light shadow views + // Emulated via fragment shader depth on hardware that doesn't support it natively + // See: https://www.w3.org/TR/webgpu/#depth-clipping and https://therealmjp.github.io/posts/shadow-maps/#disabling-z-clipping + const TEMPORAL_JITTER = 1 << 11; + const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12; + const LIGHTMAPPED = 1 << 13; + const LIGHTMAP_BICUBIC_SAMPLING = 1 << 14; + const IRRADIANCE_VOLUME = 1 << 15; + const VISIBILITY_RANGE_DITHER = 1 << 16; + const SCREEN_SPACE_REFLECTIONS = 1 << 17; + const HAS_PREVIOUS_SKIN = 1 << 18; + const HAS_PREVIOUS_MORPH = 1 << 19; + const OIT_ENABLED = 1 << 20; + const DISTANCE_FOG = 1 << 21; + const ATMOSPHERE = 1 << 22; + const INVERT_CULLING = 1 << 23; + const LAST_FLAG = Self::INVERT_CULLING.bits(); + + // Bitfields + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state + const BLEND_OPAQUE = 0 << Self::BLEND_SHIFT_BITS; // ← Values are just sequential within the mask + const BLEND_PREMULTIPLIED_ALPHA = 1 << Self::BLEND_SHIFT_BITS; // ← As blend states is on 3 bits, it can range from 0 to 7 + const BLEND_MULTIPLY = 2 << Self::BLEND_SHIFT_BITS; // ← See `BLEND_MASK_BITS` for the number of bits available + const BLEND_ALPHA = 3 << Self::BLEND_SHIFT_BITS; // + const BLEND_ALPHA_TO_COVERAGE = 4 << Self::BLEND_SHIFT_BITS; // ← We still have room for three more values without adding more bits + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_RESERVED_BITS = Self::SHADOW_FILTER_METHOD_MASK_BITS << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_HARDWARE_2X2 = 0 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_GAUSSIAN = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_TEMPORAL = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const VIEW_PROJECTION_RESERVED_BITS = Self::VIEW_PROJECTION_MASK_BITS << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_NONSTANDARD = 0 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_PERSPECTIVE = 1 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_ORTHOGRAPHIC = 2 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_RESERVED = 3 << Self::VIEW_PROJECTION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS = Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM = 1 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH = 2 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA = 3 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const ALL_RESERVED_BITS = + Self::BLEND_RESERVED_BITS.bits() | + Self::MSAA_RESERVED_BITS.bits() | + Self::TONEMAP_METHOD_RESERVED_BITS.bits() | + Self::SHADOW_FILTER_METHOD_RESERVED_BITS.bits() | + Self::VIEW_PROJECTION_RESERVED_BITS.bits() | + Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS.bits(); + } +} + +impl MeshPipelineKey { + const MSAA_MASK_BITS: u64 = 0b111; + const MSAA_SHIFT_BITS: u64 = Self::LAST_FLAG.bits().trailing_zeros() as u64 + 1; + + const BLEND_MASK_BITS: u64 = 0b111; + const BLEND_SHIFT_BITS: u64 = Self::MSAA_MASK_BITS.count_ones() as u64 + Self::MSAA_SHIFT_BITS; + + const TONEMAP_METHOD_MASK_BITS: u64 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u64 = + Self::BLEND_MASK_BITS.count_ones() as u64 + Self::BLEND_SHIFT_BITS; + + const SHADOW_FILTER_METHOD_MASK_BITS: u64 = 0b11; + const SHADOW_FILTER_METHOD_SHIFT_BITS: u64 = + Self::TONEMAP_METHOD_MASK_BITS.count_ones() as u64 + Self::TONEMAP_METHOD_SHIFT_BITS; + + const VIEW_PROJECTION_MASK_BITS: u64 = 0b11; + const VIEW_PROJECTION_SHIFT_BITS: u64 = Self::SHADOW_FILTER_METHOD_MASK_BITS.count_ones() + as u64 + + Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS: u64 = 0b11; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS: u64 = + Self::VIEW_PROJECTION_MASK_BITS.count_ones() as u64 + Self::VIEW_PROJECTION_SHIFT_BITS; + + pub fn from_msaa_samples(msaa_samples: u32) -> Self { + let msaa_bits = + (msaa_samples.trailing_zeros() as u64 & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; + Self::from_bits_retain(msaa_bits) + } + + pub fn from_hdr(hdr: bool) -> Self { + if hdr { + MeshPipelineKey::HDR + } else { + MeshPipelineKey::NONE + } + } + + pub fn msaa_samples(&self) -> u32 { + 1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + } + + pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self { + let primitive_topology_bits = ((primitive_topology as u64) + & BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS) + << BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + Self::from_bits_retain(primitive_topology_bits) + } + + pub fn primitive_topology(&self) -> PrimitiveTopology { + let primitive_topology_bits = (self.bits() + >> BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS) + & BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS; + match primitive_topology_bits { + x if x == PrimitiveTopology::PointList as u64 => PrimitiveTopology::PointList, + x if x == PrimitiveTopology::LineList as u64 => PrimitiveTopology::LineList, + x if x == PrimitiveTopology::LineStrip as u64 => PrimitiveTopology::LineStrip, + x if x == PrimitiveTopology::TriangleList as u64 => PrimitiveTopology::TriangleList, + x if x == PrimitiveTopology::TriangleStrip as u64 => PrimitiveTopology::TriangleStrip, + _ => PrimitiveTopology::default(), + } + } +} + +// Ensure that we didn't overflow the number of bits available in `MeshPipelineKey`. +const_assert_eq!( + (((MeshPipelineKey::LAST_FLAG.bits() << 1) - 1) | MeshPipelineKey::ALL_RESERVED_BITS.bits()) + & BaseMeshPipelineKey::all().bits(), + 0 +); + +// Ensure that the reserved bits don't overlap with the topology bits +const_assert_eq!( + (BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS + << BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS) + & MeshPipelineKey::ALL_RESERVED_BITS.bits(), + 0 +); + +fn is_skinned(layout: &MeshVertexBufferLayoutRef) -> bool { + layout.0.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.0.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) +} +pub fn setup_morph_and_skinning_defs( + mesh_layouts: &MeshLayouts, + layout: &MeshVertexBufferLayoutRef, + offset: u32, + key: &MeshPipelineKey, + shader_defs: &mut Vec, + vertex_attributes: &mut Vec, + skins_use_uniform_buffers: bool, +) -> BindGroupLayoutDescriptor { + let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS); + let is_lightmapped = key.intersects(MeshPipelineKey::LIGHTMAPPED); + let motion_vector_prepass = key.intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS); + + if skins_use_uniform_buffers { + shader_defs.push("SKINS_USE_UNIFORM_BUFFERS".into()); + } + + let mut add_skin_data = || { + shader_defs.push("SKINNED".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(offset)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(offset + 1)); + }; + + match ( + is_skinned(layout), + is_morphed, + is_lightmapped, + motion_vector_prepass, + ) { + (true, false, _, true) => { + add_skin_data(); + mesh_layouts.skinned_motion.clone() + } + (true, false, _, false) => { + add_skin_data(); + mesh_layouts.skinned.clone() + } + (true, true, _, true) => { + add_skin_data(); + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_skinned_motion.clone() + } + (true, true, _, false) => { + add_skin_data(); + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_skinned.clone() + } + (false, true, _, true) => { + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_motion.clone() + } + (false, true, _, false) => { + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed.clone() + } + (false, false, true, _) => mesh_layouts.lightmapped.clone(), + (false, false, false, _) => mesh_layouts.model_only.clone(), + } +} + +impl SpecializedMeshPipeline for MeshPipeline { + type Key = MeshPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut shader_defs = Vec::new(); + let mut vertex_attributes = Vec::new(); + + // Let the shader code know that it's running in a mesh pipeline. + shader_defs.push("MESH_PIPELINE".into()); + + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); + + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { + shader_defs.push("VERTEX_NORMALS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_B".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(3)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(5)); + } + + if cfg!(feature = "pbr_transmission_textures") { + shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); + } + if cfg!(feature = "pbr_multi_layer_material_textures") { + shader_defs.push("PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED".into()); + } + if cfg!(feature = "pbr_anisotropy_texture") { + shader_defs.push("PBR_ANISOTROPY_TEXTURE_SUPPORTED".into()); + } + if cfg!(feature = "pbr_specular_textures") { + shader_defs.push("PBR_SPECULAR_TEXTURES_SUPPORTED".into()); + } + + let bind_group_layout = self.get_view_layout(key.into()); + let mut bind_group_layout = vec![ + bind_group_layout.main_layout.clone(), + bind_group_layout.binding_array_layout.clone(), + ]; + + if key.msaa_samples() > 1 { + shader_defs.push("MULTISAMPLED".into()); + }; + + bind_group_layout.push(setup_morph_and_skinning_defs( + &self.mesh_layouts, + layout, + 6, + &key, + &mut shader_defs, + &mut vertex_attributes, + self.skins_use_uniform_buffers, + )); + + if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) { + shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into()); + } + + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + + let (label, blend, depth_write_enabled); + let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); + let (mut is_opaque, mut alpha_to_coverage_enabled) = (false, false); + if key.contains(MeshPipelineKey::OIT_ENABLED) && pass == MeshPipelineKey::BLEND_ALPHA { + label = "oit_mesh_pipeline".into(); + // TODO tail blending would need alpha blending + blend = None; + shader_defs.push("OIT_ENABLED".into()); + // TODO it should be possible to use this to combine MSAA and OIT + // alpha_to_coverage_enabled = true; + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_ALPHA { + label = "alpha_blend_mesh_pipeline".into(); + blend = Some(BlendState::ALPHA_BLENDING); + // For the transparent pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA { + label = "premultiplied_alpha_mesh_pipeline".into(); + blend = Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into()); + // For the transparent pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_MULTIPLY { + label = "multiply_mesh_pipeline".into(); + blend = Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::Dst, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent::OVER, + }); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_MULTIPLY".into()); + // For the multiply pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE { + label = "alpha_to_coverage_mesh_pipeline".into(); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + alpha_to_coverage_enabled = true; + shader_defs.push("ALPHA_TO_COVERAGE".into()); + } else { + label = "opaque_mesh_pipeline".into(); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + + if key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + shader_defs.push("DEFERRED_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) && key.msaa_samples() == 1 && is_opaque { + shader_defs.push("LOAD_PREPASS_NORMALS".into()); + } + + let view_projection = key.intersection(MeshPipelineKey::VIEW_PROJECTION_RESERVED_BITS); + if view_projection == MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD { + shader_defs.push("VIEW_PROJECTION_NONSTANDARD".into()); + } else if view_projection == MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE { + shader_defs.push("VIEW_PROJECTION_PERSPECTIVE".into()); + } else if view_projection == MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC { + shader_defs.push("VIEW_PROJECTION_ORTHOGRAPHIC".into()); + } + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + + #[cfg(feature = "experimental_pbr_pcss")] + shader_defs.push("PCSS_SAMPLERS_AVAILABLE".into()); + + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, + )); + + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(MeshPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + + if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if key.contains(MeshPipelineKey::IRRADIANCE_VOLUME) && IRRADIANCE_VOLUMES_ARE_USABLE { + shader_defs.push("IRRADIANCE_VOLUME".into()); + } + + if key.contains(MeshPipelineKey::LIGHTMAPPED) { + shader_defs.push("LIGHTMAP".into()); + } + if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } + + if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { + shader_defs.push("TEMPORAL_JITTER".into()); + } + + let shadow_filter_method = + key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); + if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { + shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); + } + + let blur_quality = + key.intersection(MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS); + + shader_defs.push(ShaderDefVal::Int( + "SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS".into(), + match blur_quality { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW => 4, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM => 8, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH => 16, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA => 32, + _ => unreachable!(), // Not possible, since the mask is 2 bits, and we've covered all 4 cases + }, + )); + + if key.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) { + shader_defs.push("VISIBILITY_RANGE_DITHER".into()); + } + + if key.contains(MeshPipelineKey::DISTANCE_FOG) { + shader_defs.push("DISTANCE_FOG".into()); + } + + if key.contains(MeshPipelineKey::ATMOSPHERE) { + shader_defs.push("ATMOSPHERE".into()); + } + + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + + if IRRADIANCE_VOLUMES_ARE_USABLE { + shader_defs.push("IRRADIANCE_VOLUMES_ARE_USABLE".into()); + } + + if self.clustered_decals_are_usable { + shader_defs.push("CLUSTERED_DECALS_ARE_USABLE".into()); + if cfg!(feature = "pbr_light_textures") { + shader_defs.push("LIGHT_TEXTURES".into()); + } + } + + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + // This is defined here so that custom shaders that use something other than + // the mesh binding from bevy_pbr::mesh_bindings can easily make use of this + // in their own shaders. + if let Some(per_object_buffer_batch_size) = self.per_object_buffer_batch_size { + shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + ..default() + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format, + blend, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + layout: bind_group_layout, + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + unclipped_depth: false, + topology: key.primitive_topology(), + ..default() + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled, + }, + label: Some(label), + ..default() + }) + } +} + +/// The bind groups for meshes currently loaded. +/// +/// If GPU mesh preprocessing isn't in use, these are global to the scene. If +/// GPU mesh preprocessing is in use, these are specific to a single phase. +#[derive(Default)] +pub struct MeshPhaseBindGroups { + model_only: Option, + skinned: Option, + morph_targets: HashMap, MeshBindGroupPair>, + lightmaps: HashMap, +} + +pub struct MeshBindGroupPair { + motion_vectors: BindGroup, + no_motion_vectors: BindGroup, +} + +/// All bind groups for meshes currently loaded. +#[derive(Resource)] +pub enum MeshBindGroups { + /// The bind groups for the meshes for the entire scene, if GPU mesh + /// preprocessing isn't in use. + CpuPreprocessing(MeshPhaseBindGroups), + /// A mapping from the type ID of a phase (e.g. [`Opaque3d`]) to the mesh + /// bind groups for that phase. + GpuPreprocessing(TypeIdMap), +} + +impl MeshPhaseBindGroups { + pub fn reset(&mut self) { + self.model_only = None; + self.skinned = None; + self.morph_targets.clear(); + self.lightmaps.clear(); + } + /// Get the `BindGroup` for `RenderMesh` with given `handle_id` and lightmap + /// key `lightmap`. + pub fn get( + &self, + asset_id: AssetId, + lightmap: Option, + is_skinned: bool, + morph: bool, + motion_vectors: bool, + ) -> Option<&BindGroup> { + match (is_skinned, morph, lightmap) { + (_, true, _) => self + .morph_targets + .get(&asset_id) + .map(|bind_group_pair| bind_group_pair.get(motion_vectors)), + (true, false, _) => self + .skinned + .as_ref() + .map(|bind_group_pair| bind_group_pair.get(motion_vectors)), + (false, false, Some(lightmap_slab)) => self.lightmaps.get(&lightmap_slab), + (false, false, None) => self.model_only.as_ref(), + } + } +} + +impl MeshBindGroupPair { + fn get(&self, motion_vectors: bool) -> &BindGroup { + if motion_vectors { + &self.motion_vectors + } else { + &self.no_motion_vectors + } + } +} + +/// Creates the per-mesh bind groups for each type of mesh and each phase. +pub fn prepare_mesh_bind_groups( + mut commands: Commands, + meshes: Res>, + mesh_pipeline: Res, + render_device: Res, + pipeline_cache: Res, + cpu_batched_instance_buffer: Option< + Res>, + >, + gpu_batched_instance_buffers: Option< + Res>, + >, + skins_uniform: Res, + weights_uniform: Res, + mut render_lightmaps: ResMut, +) { + // CPU mesh preprocessing path. + if let Some(cpu_batched_instance_buffer) = cpu_batched_instance_buffer + && let Some(instance_data_binding) = cpu_batched_instance_buffer + .into_inner() + .instance_data_binding() + { + // In this path, we only have a single set of bind groups for all phases. + let cpu_preprocessing_mesh_bind_groups = prepare_mesh_bind_groups_for_phase( + instance_data_binding, + &meshes, + &mesh_pipeline, + &render_device, + &pipeline_cache, + &skins_uniform, + &weights_uniform, + &mut render_lightmaps, + ); + + commands.insert_resource(MeshBindGroups::CpuPreprocessing( + cpu_preprocessing_mesh_bind_groups, + )); + return; + } + + // GPU mesh preprocessing path. + if let Some(gpu_batched_instance_buffers) = gpu_batched_instance_buffers { + let mut gpu_preprocessing_mesh_bind_groups = TypeIdMap::default(); + + // Loop over each phase. + for (phase_type_id, batched_phase_instance_buffers) in + &gpu_batched_instance_buffers.phase_instance_buffers + { + let Some(instance_data_binding) = + batched_phase_instance_buffers.instance_data_binding() + else { + continue; + }; + + let mesh_phase_bind_groups = prepare_mesh_bind_groups_for_phase( + instance_data_binding, + &meshes, + &mesh_pipeline, + &render_device, + &pipeline_cache, + &skins_uniform, + &weights_uniform, + &mut render_lightmaps, + ); + + gpu_preprocessing_mesh_bind_groups.insert(*phase_type_id, mesh_phase_bind_groups); + } + + commands.insert_resource(MeshBindGroups::GpuPreprocessing( + gpu_preprocessing_mesh_bind_groups, + )); + } +} + +/// Creates the per-mesh bind groups for each type of mesh, for a single phase. +fn prepare_mesh_bind_groups_for_phase( + model: BindingResource, + meshes: &RenderAssets, + mesh_pipeline: &MeshPipeline, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + skins_uniform: &SkinUniforms, + weights_uniform: &MorphUniforms, + render_lightmaps: &mut RenderLightmaps, +) -> MeshPhaseBindGroups { + let layouts = &mesh_pipeline.mesh_layouts; + + // TODO: Reuse allocations. + let mut groups = MeshPhaseBindGroups { + model_only: Some(layouts.model_only(render_device, pipeline_cache, &model)), + ..default() + }; + + // Create the skinned mesh bind group with the current and previous buffers + // (the latter being for motion vector computation). + let (skin, prev_skin) = (&skins_uniform.current_buffer, &skins_uniform.prev_buffer); + groups.skinned = Some(MeshBindGroupPair { + motion_vectors: layouts.skinned_motion( + render_device, + pipeline_cache, + &model, + skin, + prev_skin, + ), + no_motion_vectors: layouts.skinned(render_device, pipeline_cache, &model, skin), + }); + + // Create the morphed bind groups just like we did for the skinned bind + // group. + if let Some(weights) = weights_uniform.current_buffer.buffer() { + let prev_weights = weights_uniform.prev_buffer.buffer().unwrap_or(weights); + for (id, gpu_mesh) in meshes.iter() { + if let Some(targets) = gpu_mesh.morph_targets.as_ref() { + let bind_group_pair = if is_skinned(&gpu_mesh.layout) { + let prev_skin = &skins_uniform.prev_buffer; + MeshBindGroupPair { + motion_vectors: layouts.morphed_skinned_motion( + render_device, + pipeline_cache, + &model, + skin, + weights, + targets, + prev_skin, + prev_weights, + ), + no_motion_vectors: layouts.morphed_skinned( + render_device, + pipeline_cache, + &model, + skin, + weights, + targets, + ), + } + } else { + MeshBindGroupPair { + motion_vectors: layouts.morphed_motion( + render_device, + pipeline_cache, + &model, + weights, + targets, + prev_weights, + ), + no_motion_vectors: layouts.morphed( + render_device, + pipeline_cache, + &model, + weights, + targets, + ), + } + }; + groups.morph_targets.insert(id, bind_group_pair); + } + } + } + + // Create lightmap bindgroups. There will be one bindgroup for each slab. + let bindless_supported = render_lightmaps.bindless_supported; + for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() { + groups.lightmaps.insert( + LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()), + layouts.lightmapped( + render_device, + pipeline_cache, + &model, + lightmap_slab, + bindless_supported, + ), + ); + } + + groups +} + +pub struct SetMeshViewBindGroup; +impl RenderCommand

for SetMeshViewBindGroup { + type Param = (); + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Option>, + ); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + ( + view_uniform, + view_lights, + view_fog, + view_light_probes, + view_ssr, + view_environment_map, + mesh_view_bind_group, + maybe_oit_layers_count_offset, + ): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let mut offsets: SmallVec<[u32; 8]> = smallvec![ + view_uniform.offset, + view_lights.offset, + view_fog.offset, + **view_light_probes, + **view_ssr, + **view_environment_map, + ]; + if let Some(layers_count_offset) = maybe_oit_layers_count_offset { + offsets.push(layers_count_offset.offset); + } + pass.set_bind_group(I, &mesh_view_bind_group.main, &offsets); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewBindingArrayBindGroup; +impl RenderCommand

for SetMeshViewBindingArrayBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.binding_array, &[]); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewEmptyBindGroup; +impl RenderCommand

for SetMeshViewEmptyBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.empty, &[]); + + RenderCommandResult::Success + } +} + +pub struct SetMeshBindGroup; +impl RenderCommand

for SetMeshBindGroup { + type Param = ( + SRes, + SRes, + SRes, + SRes, + SRes, + SRes, + ); + type ViewQuery = Has; + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + has_motion_vector_prepass: bool, + _item_query: Option<()>, + ( + render_device, + bind_groups, + mesh_instances, + skin_uniforms, + morph_indices, + lightmaps, + ): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let bind_groups = bind_groups.into_inner(); + let mesh_instances = mesh_instances.into_inner(); + let skin_uniforms = skin_uniforms.into_inner(); + let morph_indices = morph_indices.into_inner(); + + let entity = &item.main_entity(); + + let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(*entity) else { + return RenderCommandResult::Success; + }; + + let current_skin_byte_offset = skin_uniforms.skin_byte_offset(*entity); + let current_morph_index = morph_indices.current.get(entity); + let prev_morph_index = morph_indices.prev.get(entity); + + let is_skinned = current_skin_byte_offset.is_some(); + let is_morphed = current_morph_index.is_some(); + + let lightmap_slab_index = lightmaps + .render_lightmaps + .get(entity) + .map(|render_lightmap| render_lightmap.slab_index); + + let Some(mesh_phase_bind_groups) = (match *bind_groups { + MeshBindGroups::CpuPreprocessing(ref mesh_phase_bind_groups) => { + Some(mesh_phase_bind_groups) + } + MeshBindGroups::GpuPreprocessing(ref mesh_phase_bind_groups) => { + mesh_phase_bind_groups.get(&TypeId::of::

()) + } + }) else { + // This is harmless if e.g. we're rendering the `Shadow` phase and + // there weren't any shadows. + return RenderCommandResult::Success; + }; + + let Some(bind_group) = mesh_phase_bind_groups.get( + mesh_asset_id, + lightmap_slab_index, + is_skinned, + is_morphed, + has_motion_vector_prepass, + ) else { + return RenderCommandResult::Failure( + "The MeshBindGroups resource wasn't set in the render phase. \ + It should be set by the prepare_mesh_bind_group system.\n\ + This is a bevy bug! Please open an issue.", + ); + }; + + let mut dynamic_offsets: [u32; 5] = Default::default(); + let mut offset_count = 0; + if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() { + dynamic_offsets[offset_count] = dynamic_offset; + offset_count += 1; + } + if let Some(current_skin_index) = current_skin_byte_offset + && skins_use_uniform_buffers(&render_device.limits()) + { + dynamic_offsets[offset_count] = current_skin_index.byte_offset; + offset_count += 1; + } + if let Some(current_morph_index) = current_morph_index { + dynamic_offsets[offset_count] = current_morph_index.index; + offset_count += 1; + } + + // Attach motion vectors if needed. + if has_motion_vector_prepass { + // Attach the previous skin index for motion vector computation. + if skins_use_uniform_buffers(&render_device.limits()) + && let Some(current_skin_byte_offset) = current_skin_byte_offset + { + dynamic_offsets[offset_count] = current_skin_byte_offset.byte_offset; + offset_count += 1; + } + + // Attach the previous morph index for motion vector computation. If + // there isn't one, just use zero as the shader will ignore it. + if current_morph_index.is_some() { + match prev_morph_index { + Some(prev_morph_index) => { + dynamic_offsets[offset_count] = prev_morph_index.index; + } + None => dynamic_offsets[offset_count] = 0, + } + offset_count += 1; + } + } + + pass.set_bind_group(I, bind_group, &dynamic_offsets[0..offset_count]); + + RenderCommandResult::Success + } +} + +pub struct DrawMesh; +impl RenderCommand

for DrawMesh { + type Param = ( + SRes>, + SRes, + SRes, + SRes, + SRes, + Option>, + SRes, + ); + type ViewQuery = Has; + type ItemQuery = (); + #[inline] + fn render<'w>( + item: &P, + has_preprocess_bind_group: ROQueryItem, + _item_query: Option<()>, + ( + meshes, + mesh_instances, + indirect_parameters_buffer, + pipeline_cache, + mesh_allocator, + preprocess_pipelines, + preprocessing_support, + ): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + // If we're using GPU preprocessing, then we're dependent on that + // compute shader having been run, which of course can only happen if + // it's compiled. Otherwise, our mesh instance data won't be present. + if let Some(preprocess_pipelines) = preprocess_pipelines + && (!has_preprocess_bind_group + || !preprocess_pipelines + .pipelines_are_loaded(&pipeline_cache, &preprocessing_support)) + { + return RenderCommandResult::Skip; + } + + let meshes = meshes.into_inner(); + let mesh_instances = mesh_instances.into_inner(); + let indirect_parameters_buffer = indirect_parameters_buffer.into_inner(); + let mesh_allocator = mesh_allocator.into_inner(); + + let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(item.main_entity()) else { + return RenderCommandResult::Skip; + }; + let Some(gpu_mesh) = meshes.get(mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); + + let batch_range = item.batch_range(); + + // Draw either directly or indirectly, as appropriate. If we're in + // indirect mode, we can additionally multi-draw. (We can't multi-draw + // in direct mode because `wgpu` doesn't expose that functionality.) + match &gpu_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), *index_format); + + match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { + pass.draw_indexed( + index_buffer_slice.range.start + ..(index_buffer_slice.range.start + *count), + vertex_buffer_slice.range.start as i32, + batch_range.clone(), + ); + } + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as + // the buffer we're going to use for + // `multi_draw_indexed_indirect_count` (if available). + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffer.get(&TypeId::of::

()) + else { + warn!( + "Not rendering mesh because indexed indirect parameters buffer \ + wasn't present for this phase", + ); + return RenderCommandResult::Skip; + }; + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + phase_indirect_parameters_buffers.indexed.data_buffer(), + phase_indirect_parameters_buffers + .indexed + .batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because indexed indirect parameters buffer \ + wasn't present", + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters + // within the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in + // the batch sets buffer. Otherwise, supply the size of + // the batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = u32::from(batch_set_index) + * (size_of::() as u32); + pass.multi_draw_indexed_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indexed_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } + } + } + } + + RenderMeshBufferInfo::NonIndexed => match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { + pass.draw(vertex_buffer_slice.range, batch_range.clone()); + } + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as the + // buffer we're going to use for + // `multi_draw_indirect_count` (if available). + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffer.get(&TypeId::of::

()) + else { + warn!( + "Not rendering mesh because non-indexed indirect parameters buffer \ + wasn't present for this phase", + ); + return RenderCommandResult::Skip; + }; + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + phase_indirect_parameters_buffers.non_indexed.data_buffer(), + phase_indirect_parameters_buffers + .non_indexed + .batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because non-indexed indirect parameters buffer \ + wasn't present" + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters within + // the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in the + // batch sets buffer. Otherwise, supply the size of the + // batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = + u32::from(batch_set_index) * (size_of::() as u32); + pass.multi_draw_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } + } + }, + } + RenderCommandResult::Success + } +} + +#[cfg(test)] +mod tests { + use super::MeshPipelineKey; + #[test] + fn mesh_key_msaa_samples() { + for i in [1, 2, 4, 8, 16, 32, 64, 128] { + assert_eq!(MeshPipelineKey::from_msaa_samples(i).msaa_samples(), i); + } + } +} diff --git a/vendor/bevy_pbr/src/render/mesh.wgsl b/vendor/bevy_pbr/src/render/mesh.wgsl new file mode 100644 index 00000000..95684684 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh.wgsl @@ -0,0 +1,120 @@ +#import bevy_pbr::{ + mesh_bindings::mesh, + mesh_functions, + skinning, + morph::morph, + forward_io::{Vertex, VertexOutput}, + view_transformations::position_world_to_clip, +} + +#ifdef MORPH_TARGETS +fn morph_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + + let weight_count = bevy_pbr::morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = bevy_pbr::morph::weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph(vertex_index, bevy_pbr::morph::position_offset, i); +#ifdef VERTEX_NORMALS + vertex.normal += weight * morph(vertex_index, bevy_pbr::morph::normal_offset, i); +#endif +#ifdef VERTEX_TANGENTS + vertex.tangent += vec4(weight * morph(vertex_index, bevy_pbr::morph::tangent_offset, i), 0.0); +#endif + } + return vertex; +} +#endif + +@vertex +fn vertex(vertex_no_morph: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef MORPH_TARGETS + var vertex = morph_vertex(vertex_no_morph); +#else + var vertex = vertex_no_morph; +#endif + + let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); + +#ifdef SKINNED + var world_from_local = skinning::skin_model( + vertex.joint_indices, + vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 . + var world_from_local = mesh_world_from_local; +#endif + +#ifdef VERTEX_NORMALS +#ifdef SKINNED + out.world_normal = skinning::skin_normals(world_from_local, vertex.normal); +#else + out.world_normal = mesh_functions::mesh_normal_local_to_world( + vertex.normal, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif +#endif + +#ifdef VERTEX_POSITIONS + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.position = position_world_to_clip(out.world_position.xyz); +#endif + +#ifdef VERTEX_UVS_A + out.uv = vertex.uv; +#endif +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_functions::mesh_tangent_local_to_world( + world_from_local, + vertex.tangent, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif + +#ifdef VERTEX_COLORS + out.color = vertex.color; +#endif + +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + out.instance_index = vertex_no_morph.instance_index; +#endif + +#ifdef VISIBILITY_RANGE_DITHER + out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level( + vertex_no_morph.instance_index, mesh_world_from_local[3]); +#endif + + return out; +} + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { +#ifdef VERTEX_COLORS + return mesh.color; +#else + return vec4(1.0, 0.0, 1.0, 1.0); +#endif +} diff --git a/vendor/bevy_pbr/src/render/mesh_bindings.rs b/vendor/bevy_pbr/src/render/mesh_bindings.rs new file mode 100644 index 00000000..350b27ab --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_bindings.rs @@ -0,0 +1,569 @@ +//! Bind group layout related definitions for the mesh pipeline. + +use bevy_math::Mat4; +use bevy_mesh::morph::MAX_MORPH_WEIGHTS; +use bevy_render::{ + render_resource::*, + renderer::{RenderAdapter, RenderDevice}, +}; + +use crate::{LightmapSlab, binding_arrays_are_usable, render::skin::MAX_JOINTS}; + +const MORPH_WEIGHT_SIZE: usize = size_of::(); + +/// This is used to allocate buffers. +/// The correctness of the value depends on the GPU/platform. +/// The current value is chosen because it is guaranteed to work everywhere. +/// To allow for bigger values, a check must be made for the limits +/// of the GPU at runtime, which would mean not using consts anymore. +pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE; + +const JOINT_SIZE: usize = size_of::(); +pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; + +/// Individual layout entries. +mod layout_entry { + use core::num::NonZeroU32; + + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; + use crate::{LIGHTMAPS_PER_SLAB, MeshUniform, render::skin}; + use bevy_render::{ + render_resource::{ + BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, SamplerBindingType, + ShaderStages, TextureSampleType, + binding_types::{ + sampler, storage_buffer_read_only_sized, texture_2d, texture_3d, + uniform_buffer_sized, + }, + }, + settings::WgpuLimits, + }; + + pub(super) fn model(limits: &WgpuLimits) -> BindGroupLayoutEntryBuilder { + GpuArrayBuffer::::binding_layout(limits) + .visibility(ShaderStages::VERTEX_FRAGMENT) + } + pub(super) fn skinning(limits: &WgpuLimits) -> BindGroupLayoutEntryBuilder { + // If we can use storage buffers, do so. Otherwise, fall back to uniform + // buffers. + let size = BufferSize::new(JOINT_BUFFER_SIZE as u64); + if skin::skins_use_uniform_buffers(limits) { + uniform_buffer_sized(true, size) + } else { + storage_buffer_read_only_sized(false, size) + } + } + pub(super) fn weights() -> BindGroupLayoutEntryBuilder { + uniform_buffer_sized(true, BufferSize::new(MORPH_BUFFER_SIZE as u64)) + } + pub(super) fn targets() -> BindGroupLayoutEntryBuilder { + texture_3d(TextureSampleType::Float { filterable: false }) + } + pub(super) fn lightmaps_texture_view() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }).visibility(ShaderStages::FRAGMENT) + } + pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT) + } + pub(super) fn lightmaps_texture_view_array() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } + pub(super) fn lightmaps_sampler_array() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } +} + +/// Individual [`BindGroupEntry`] +/// for bind groups. +mod entry { + use crate::render::skin; + + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; + use bevy_render::{ + render_resource::{ + BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler, + TextureView, WgpuSampler, WgpuTextureView, + }, + renderer::RenderDevice, + }; + + fn entry(binding: u32, size: Option, buffer: &Buffer) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::Buffer(BufferBinding { + buffer, + offset: 0, + size: size.map(|size| BufferSize::new(size).unwrap()), + }), + } + } + pub(super) fn model(binding: u32, resource: BindingResource) -> BindGroupEntry { + BindGroupEntry { binding, resource } + } + pub(super) fn skinning<'a>( + render_device: &RenderDevice, + binding: u32, + buffer: &'a Buffer, + ) -> BindGroupEntry<'a> { + let size = if skin::skins_use_uniform_buffers(&render_device.limits()) { + Some(JOINT_BUFFER_SIZE as u64) + } else { + None + }; + entry(binding, size, buffer) + } + pub(super) fn weights(binding: u32, buffer: &Buffer) -> BindGroupEntry<'_> { + entry(binding, Some(MORPH_BUFFER_SIZE as u64), buffer) + } + pub(super) fn targets(binding: u32, texture: &TextureView) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::TextureView(texture), + } + } + pub(super) fn lightmaps_texture_view( + binding: u32, + texture: &TextureView, + ) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::TextureView(texture), + } + } + pub(super) fn lightmaps_sampler(binding: u32, sampler: &Sampler) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::Sampler(sampler), + } + } + pub(super) fn lightmaps_texture_view_array<'a>( + binding: u32, + textures: &'a [&'a WgpuTextureView], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::TextureViewArray(textures), + } + } + pub(super) fn lightmaps_sampler_array<'a>( + binding: u32, + samplers: &'a [&'a WgpuSampler], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::SamplerArray(samplers), + } + } +} + +/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`). +#[derive(Clone)] +pub struct MeshLayouts { + /// The mesh model uniform (transform) and nothing else. + pub model_only: BindGroupLayoutDescriptor, + + /// Includes the lightmap texture and uniform. + pub lightmapped: BindGroupLayoutDescriptor, + + /// Also includes the uniform for skinning + pub skinned: BindGroupLayoutDescriptor, + + /// Like [`MeshLayouts::skinned`], but includes slots for the previous + /// frame's joint matrices, so that we can compute motion vectors. + pub skinned_motion: BindGroupLayoutDescriptor, + + /// Also includes the uniform and [`MorphAttributes`] for morph targets. + /// + /// [`MorphAttributes`]: bevy_mesh::morph::MorphAttributes + pub morphed: BindGroupLayoutDescriptor, + + /// Like [`MeshLayouts::morphed`], but includes a slot for the previous + /// frame's morph weights, so that we can compute motion vectors. + pub morphed_motion: BindGroupLayoutDescriptor, + + /// Also includes both uniforms for skinning and morph targets, also the + /// morph target [`MorphAttributes`] binding. + /// + /// [`MorphAttributes`]: bevy_mesh::morph::MorphAttributes + pub morphed_skinned: BindGroupLayoutDescriptor, + + /// Like [`MeshLayouts::morphed_skinned`], but includes slots for the + /// previous frame's joint matrices and morph weights, so that we can + /// compute motion vectors. + pub morphed_skinned_motion: BindGroupLayoutDescriptor, +} + +impl MeshLayouts { + /// Prepare the layouts used by the default bevy [`Mesh`]. + /// + /// [`Mesh`]: bevy_mesh::Mesh + pub fn new(render_device: &RenderDevice, render_adapter: &RenderAdapter) -> Self { + MeshLayouts { + model_only: Self::model_only_layout(render_device), + lightmapped: Self::lightmapped_layout(render_device, render_adapter), + skinned: Self::skinned_layout(render_device), + skinned_motion: Self::skinned_motion_layout(render_device), + morphed: Self::morphed_layout(render_device), + morphed_motion: Self::morphed_motion_layout(render_device), + morphed_skinned: Self::morphed_skinned_layout(render_device), + morphed_skinned_motion: Self::morphed_skinned_motion_layout(render_device), + } + } + + // ---------- create individual BindGroupLayouts ---------- + + fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "mesh_layout", + &BindGroupLayoutEntries::single( + ShaderStages::empty(), + layout_entry::model(&render_device.limits()), + ), + ) + } + + /// Creates the layout for skinned meshes. + fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "skinned_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(&render_device.limits())), + ), + ), + ) + } + + /// Creates the layout for skinned meshes with the infrastructure to compute + /// motion vectors. + fn skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "skinned_motion_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(&render_device.limits())), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning(&render_device.limits())), + ), + ), + ) + } + + /// Creates the layout for meshes with morph targets. + fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "morphed_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + ), + ), + ) + } + + /// Creates the layout for meshes with morph targets and the infrastructure + /// to compute motion vectors. + fn morphed_motion_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "morphed_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), + ), + ), + ) + } + + /// Creates the bind group layout for meshes with both skins and morph + /// targets. + fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "morphed_skinned_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(&render_device.limits())), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + ), + ), + ) + } + + /// Creates the bind group layout for meshes with both skins and morph + /// targets, in addition to the infrastructure to compute motion vectors. + fn morphed_skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayoutDescriptor { + BindGroupLayoutDescriptor::new( + "morphed_skinned_motion_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(&render_device.limits())), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning(&render_device.limits())), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), + ), + ), + ) + } + + fn lightmapped_layout( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> BindGroupLayoutDescriptor { + if binding_arrays_are_usable(render_device, render_adapter) { + BindGroupLayoutDescriptor::new( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + (4, layout_entry::lightmaps_texture_view_array()), + (5, layout_entry::lightmaps_sampler_array()), + ), + ), + ) + } else { + BindGroupLayoutDescriptor::new( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(&render_device.limits())), + (4, layout_entry::lightmaps_texture_view()), + (5, layout_entry::lightmaps_sampler()), + ), + ), + ) + } + } + + // ---------- BindGroup methods ---------- + + pub fn model_only( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + ) -> BindGroup { + render_device.create_bind_group( + "model_only_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.model_only), + &[entry::model(0, model.clone())], + ) + } + + pub fn lightmapped( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + lightmap_slab: &mut LightmapSlab, + bindless_lightmaps: bool, + ) -> BindGroup { + let layout = pipeline_cache.get_bind_group_layout(&self.lightmapped); + if let Some(cached) = lightmap_slab.cached_bind_group(model, layout.id()) { + return cached; + } + let bind_group = if bindless_lightmaps { + let (texture_views, samplers) = lightmap_slab.build_binding_arrays(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &layout, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view_array(4, &texture_views), + entry::lightmaps_sampler_array(5, &samplers), + ], + ) + } else { + let (texture_view, sampler) = lightmap_slab.bindings_for_first_lightmap(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &layout, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view(4, texture_view), + entry::lightmaps_sampler(5, sampler), + ], + ) + }; + lightmap_slab.cache_bind_group(model, layout.id(), bind_group.clone()); + bind_group + } + + /// Creates the bind group for skinned meshes with no morph targets. + pub fn skinned( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_skin: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "skinned_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.skinned), + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + ], + ) + } + + /// Creates the bind group for skinned meshes with no morph targets, with + /// the infrastructure to compute motion vectors. + /// + /// `current_skin` is the buffer of joint matrices for this frame; + /// `prev_skin` is the buffer for the previous frame. The latter is used for + /// motion vector computation. If there is no such applicable buffer, + /// `current_skin` and `prev_skin` will reference the same buffer. + pub fn skinned_motion( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_skin: &Buffer, + prev_skin: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "skinned_motion_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.skinned_motion), + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::skinning(render_device, 6, prev_skin), + ], + ) + } + + /// Creates the bind group for meshes with no skins but morph targets. + pub fn morphed( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_weights: &Buffer, + targets: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.morphed), + &[ + entry::model(0, model.clone()), + entry::weights(2, current_weights), + entry::targets(3, targets), + ], + ) + } + + /// Creates the bind group for meshes with no skins but morph targets, in + /// addition to the infrastructure to compute motion vectors. + /// + /// `current_weights` is the buffer of morph weights for this frame; + /// `prev_weights` is the buffer for the previous frame. The latter is used + /// for motion vector computation. If there is no such applicable buffer, + /// `current_weights` and `prev_weights` will reference the same buffer. + pub fn morphed_motion( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_weights: &Buffer, + targets: &TextureView, + prev_weights: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_motion_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.morphed_motion), + &[ + entry::model(0, model.clone()), + entry::weights(2, current_weights), + entry::targets(3, targets), + entry::weights(7, prev_weights), + ], + ) + } + + /// Creates the bind group for meshes with skins and morph targets. + pub fn morphed_skinned( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_skin: &Buffer, + current_weights: &Buffer, + targets: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_skinned_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.morphed_skinned), + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::weights(2, current_weights), + entry::targets(3, targets), + ], + ) + } + + /// Creates the bind group for meshes with skins and morph targets, in + /// addition to the infrastructure to compute motion vectors. + /// + /// See the documentation for [`MeshLayouts::skinned_motion`] and + /// [`MeshLayouts::morphed_motion`] above for more information about the + /// `current_skin`, `prev_skin`, `current_weights`, and `prev_weights` + /// buffers. + pub fn morphed_skinned_motion( + &self, + render_device: &RenderDevice, + pipeline_cache: &PipelineCache, + model: &BindingResource, + current_skin: &Buffer, + current_weights: &Buffer, + targets: &TextureView, + prev_skin: &Buffer, + prev_weights: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_skinned_motion_mesh_bind_group", + &pipeline_cache.get_bind_group_layout(&self.morphed_skinned_motion), + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::weights(2, current_weights), + entry::targets(3, targets), + entry::skinning(render_device, 6, prev_skin), + entry::weights(7, prev_weights), + ], + ) + } +} diff --git a/vendor/bevy_pbr/src/render/mesh_bindings.wgsl b/vendor/bevy_pbr/src/render/mesh_bindings.wgsl new file mode 100644 index 00000000..6e78dc43 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_bindings.wgsl @@ -0,0 +1,11 @@ +#define_import_path bevy_pbr::mesh_bindings + +#import bevy_pbr::mesh_types::Mesh + +#ifndef MESHLET_MESH_MATERIAL_PASS +#ifdef PER_OBJECT_BUFFER_BATCH_SIZE +@group(2) @binding(0) var mesh: array; +#else +@group(2) @binding(0) var mesh: array; +#endif // PER_OBJECT_BUFFER_BATCH_SIZE +#endif // MESHLET_MESH_MATERIAL_PASS diff --git a/vendor/bevy_pbr/src/render/mesh_functions.wgsl b/vendor/bevy_pbr/src/render/mesh_functions.wgsl new file mode 100644 index 00000000..4514f127 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_functions.wgsl @@ -0,0 +1,168 @@ +#define_import_path bevy_pbr::mesh_functions + +#import bevy_pbr::{ + mesh_view_bindings::{ + view, + visibility_ranges, + VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE + }, + mesh_bindings::mesh, + mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, + view_transformations::position_world_to_clip, +} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn get_world_from_local(instance_index: u32) -> mat4x4 { + return affine3_to_square(mesh[instance_index].world_from_local); +} + +fn get_previous_world_from_local(instance_index: u32) -> mat4x4 { + return affine3_to_square(mesh[instance_index].previous_world_from_local); +} + +fn get_local_from_world(instance_index: u32) -> mat4x4 { + // the model matrix is translation * rotation * scale + // the inverse is then scale^-1 * rotation ^-1 * translation^-1 + // the 3x3 matrix only contains the information for the rotation and scale + let inverse_model_3x3 = transpose(mat2x4_f32_to_mat3x3_unpack( + mesh[instance_index].local_from_world_transpose_a, + mesh[instance_index].local_from_world_transpose_b, + )); + // construct scale^-1 * rotation^-1 from the 3x3 + let inverse_model_4x4_no_trans = mat4x4( + vec4(inverse_model_3x3[0], 0.0), + vec4(inverse_model_3x3[1], 0.0), + vec4(inverse_model_3x3[2], 0.0), + vec4(0.0,0.0,0.0,1.0) + ); + // we can get translation^-1 by negating the translation of the model + let model = get_world_from_local(instance_index); + let inverse_model_4x4_only_trans = mat4x4( + vec4(1.0,0.0,0.0,0.0), + vec4(0.0,1.0,0.0,0.0), + vec4(0.0,0.0,1.0,0.0), + vec4(-model[3].xyz, 1.0) + ); + + return inverse_model_4x4_no_trans * inverse_model_4x4_only_trans; +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +fn mesh_position_local_to_world(world_from_local: mat4x4, vertex_position: vec4) -> vec4 { + return world_from_local * vertex_position; +} + +// NOTE: The intermediate world_position assignment is important +// for precision purposes when using the 'equals' depth comparison +// function. +fn mesh_position_local_to_clip(world_from_local: mat4x4, vertex_position: vec4) -> vec4 { + let world_position = mesh_position_local_to_world(world_from_local, vertex_position); + return position_world_to_clip(world_position.xyz); +} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn mesh_normal_local_to_world(vertex_normal: vec3, instance_index: u32) -> vec3 { + // NOTE: The mikktspace method of normal mapping requires that the world normal is + // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents + // and normal maps so that the exact inverse process is applied when shading. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. + // We only skip normalization for invalid normals so that they don't become NaN. + // Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + if any(vertex_normal != vec3(0.0)) { + return normalize( + mat2x4_f32_to_mat3x3_unpack( + mesh[instance_index].local_from_world_transpose_a, + mesh[instance_index].local_from_world_transpose_b, + ) * vertex_normal + ); + } else { + return vertex_normal; + } +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +// Calculates the sign of the determinant of the 3x3 model matrix based on a +// mesh flag +fn sign_determinant_model_3x3m(mesh_flags: u32) -> f32 { + // bool(u32) is false if 0u else true + // f32(bool) is 1.0 if true else 0.0 + // * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively + return f32(bool(mesh_flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0; +} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn mesh_tangent_local_to_world(world_from_local: mat4x4, vertex_tangent: vec4, instance_index: u32) -> vec4 { + // NOTE: The mikktspace method of normal mapping requires that the world tangent is + // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents + // and normal maps so that the exact inverse process is applied when shading. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. + // We only skip normalization for invalid tangents so that they don't become NaN. + // Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + if any(vertex_tangent != vec4(0.0)) { + return vec4( + normalize( + mat3x3( + world_from_local[0].xyz, + world_from_local[1].xyz, + world_from_local[2].xyz, + ) * vertex_tangent.xyz + ), + // NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for + // situations such as negative scaling. + vertex_tangent.w * sign_determinant_model_3x3m(mesh[instance_index].flags) + ); + } else { + return vertex_tangent; + } +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +// Returns an appropriate dither level for the current mesh instance. +// +// This looks up the LOD range in the `visibility_ranges` table and compares the +// camera distance to determine the dithering level. +#ifdef VISIBILITY_RANGE_DITHER +fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4) -> i32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a storage buffer, then the length is variable. + let visibility_buffer_array_len = arrayLength(&visibility_ranges); +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a uniform buffer, then the length is constant + let visibility_buffer_array_len = VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE; +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + + let visibility_buffer_index = mesh[instance_index].flags & 0xffffu; + if (visibility_buffer_index > visibility_buffer_array_len) { + return -16; + } + + let lod_range = visibility_ranges[visibility_buffer_index]; + let camera_distance = length(view.world_position.xyz - world_position.xyz); + + // This encodes the following mapping: + // + // `lod_range.` x y z w camera distance + // ←───────┼────────┼────────┼────────┼────────→ + // Dither Level -16 -16 0 0 16 16 Dither Level + let offset = select(-16, 0, camera_distance >= lod_range.z); + let bounds = select(lod_range.xy, lod_range.zw, camera_distance >= lod_range.z); + let level = i32(round((camera_distance - bounds.x) / (bounds.y - bounds.x) * 16.0)); + return offset + clamp(level, 0, 16); +} +#endif + + +#ifndef MESHLET_MESH_MATERIAL_PASS +fn get_tag(instance_index: u32) -> u32 { + return mesh[instance_index].tag; +} +#endif diff --git a/vendor/bevy_pbr/src/render/mesh_preprocess.wgsl b/vendor/bevy_pbr/src/render/mesh_preprocess.wgsl new file mode 100644 index 00000000..03986c0d --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -0,0 +1,376 @@ +// GPU mesh transforming and culling. +// +// This is a compute shader that expands each `MeshInputUniform` out to a full +// `MeshUniform` for each view before rendering. (Thus `MeshInputUniform` and +// `MeshUniform` are in a 1:N relationship.) It runs in parallel for all meshes +// for all views. As part of this process, the shader gathers each mesh's +// transform on the previous frame and writes it into the `MeshUniform` so that +// TAA works. It also performs frustum culling and occlusion culling, if +// requested. +// +// If occlusion culling is on, this shader runs twice: once to prepare the +// meshes that were visible last frame, and once to prepare the meshes that +// weren't visible last frame but became visible this frame. The two invocations +// are known as *early mesh preprocessing* and *late mesh preprocessing* +// respectively. + +#import bevy_pbr::mesh_preprocess_types::{ + IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, MeshInput +} +#import bevy_pbr::mesh_types::{Mesh, MESH_FLAGS_NO_FRUSTUM_CULLING_BIT} +#import bevy_pbr::mesh_view_bindings::view +#import bevy_pbr::occlusion_culling +#import bevy_pbr::prepass_bindings::previous_view_uniforms +#import bevy_pbr::view_transformations::{ + position_world_to_ndc, position_world_to_view, ndc_to_uv, view_z_to_depth_ndc, + position_world_to_prev_ndc, position_world_to_prev_view, prev_view_z_to_depth_ndc +} +#import bevy_render::maths +#import bevy_render::view::View + +// Information about each mesh instance needed to cull it on GPU. +// +// At the moment, this just consists of its axis-aligned bounding box (AABB). +struct MeshCullingData { + // The 3D center of the AABB in model space, padded with an extra unused + // float value. + aabb_center: vec4, + // The 3D extents of the AABB in model space, divided by two, padded with + // an extra unused float value. + aabb_half_extents: vec4, +} + +// One invocation of this compute shader: i.e. one mesh instance in a view. +struct PreprocessWorkItem { + // The index of the `MeshInput` in the `current_input` buffer that we read + // from. + input_index: u32, + // In direct mode, the index of the `Mesh` in `output` that we write to. In + // indirect mode, the index of the `IndirectParameters` in + // `indirect_parameters` that we write to. + output_or_indirect_parameters_index: u32, +} + +// The parameters for the indirect compute dispatch for the late mesh +// preprocessing phase. +struct LatePreprocessWorkItemIndirectParameters { + // The number of workgroups we're going to dispatch. + // + // This value should always be equal to `ceil(work_item_count / 64)`. + dispatch_x: atomic, + // The number of workgroups in the Y direction; always 1. + dispatch_y: u32, + // The number of workgroups in the Z direction; always 1. + dispatch_z: u32, + // The precise number of work items. + work_item_count: atomic, + // Padding. + // + // This isn't the usual structure padding; it's needed because some hardware + // requires indirect compute dispatch parameters to be aligned on 64-byte + // boundaries. + pad: vec4, +} + +// These have to be in a structure because of Naga limitations on DX12. +struct PushConstants { + // The offset into the `late_preprocess_work_item_indirect_parameters` + // buffer. + late_preprocess_work_item_indirect_offset: u32, +} + +// The current frame's `MeshInput`. +@group(0) @binding(3) var current_input: array; +// The `MeshInput` values from the previous frame. +@group(0) @binding(4) var previous_input: array; +// Indices into the `MeshInput` buffer. +// +// There may be many indices that map to the same `MeshInput`. +@group(0) @binding(5) var work_items: array; +// The output array of `Mesh`es. +@group(0) @binding(6) var output: array; + +#ifdef INDIRECT +// The array of indirect parameters for drawcalls. +@group(0) @binding(7) var indirect_parameters_cpu_metadata: + array; + +@group(0) @binding(8) var indirect_parameters_gpu_metadata: + array; +#endif + +#ifdef FRUSTUM_CULLING +// Data needed to cull the meshes. +// +// At the moment, this consists only of AABBs. +@group(0) @binding(9) var mesh_culling_data: array; +#endif // FRUSTUM_CULLING + +#ifdef OCCLUSION_CULLING +@group(0) @binding(10) var depth_pyramid: texture_2d; + +#ifdef EARLY_PHASE +@group(0) @binding(11) var late_preprocess_work_items: + array; +#endif // EARLY_PHASE + +@group(0) @binding(12) var late_preprocess_work_item_indirect_parameters: + array; + +var push_constants: PushConstants; +#endif // OCCLUSION_CULLING + +#ifdef FRUSTUM_CULLING +// Returns true if the view frustum intersects an oriented bounding box (OBB). +// +// `aabb_center.w` should be 1.0. +fn view_frustum_intersects_obb( + world_from_local: mat4x4, + aabb_center: vec4, + aabb_half_extents: vec3, +) -> bool { + + for (var i = 0; i < 5; i += 1) { + // Calculate relative radius of the sphere associated with this plane. + let plane_normal = view.frustum[i]; + let relative_radius = dot( + abs( + vec3( + dot(plane_normal.xyz, world_from_local[0].xyz), + dot(plane_normal.xyz, world_from_local[1].xyz), + dot(plane_normal.xyz, world_from_local[2].xyz), + ) + ), + aabb_half_extents + ); + + // Check the frustum plane. + if (!maths::sphere_intersects_plane_half_space( + plane_normal, aabb_center, relative_radius)) { + return false; + } + } + + return true; +} +#endif + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index. If this thread doesn't correspond to any + // index, bail. + let instance_index = global_invocation_id.x; + +#ifdef LATE_PHASE + if (instance_index >= atomicLoad(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].work_item_count)) { + return; + } +#else // LATE_PHASE + if (instance_index >= arrayLength(&work_items)) { + return; + } +#endif + + // Unpack the work item. + let input_index = work_items[instance_index].input_index; +#ifdef INDIRECT + let indirect_parameters_index = work_items[instance_index].output_or_indirect_parameters_index; + + // If we're the first mesh instance in this batch, write the index of our + // `MeshInput` into the appropriate slot so that the indirect parameters + // building shader can access it. +#ifndef LATE_PHASE + // https://github.com/gfx-rs/wgpu/issues/4394 + if (instance_index == 0u) { + indirect_parameters_gpu_metadata[indirect_parameters_index].mesh_index = input_index; + } else if (work_items[instance_index - 1].output_or_indirect_parameters_index != indirect_parameters_index) { + indirect_parameters_gpu_metadata[indirect_parameters_index].mesh_index = input_index; + } +#endif // LATE_PHASE + +#else // INDIRECT + let mesh_output_index = work_items[instance_index].output_or_indirect_parameters_index; +#endif // INDIRECT + + // Unpack the input matrix. + let world_from_local_affine_transpose = current_input[input_index].world_from_local; + let world_from_local = maths::affine3_to_square(world_from_local_affine_transpose); + + // Frustum cull if necessary. +#ifdef FRUSTUM_CULLING + if ((current_input[input_index].flags & MESH_FLAGS_NO_FRUSTUM_CULLING_BIT) == 0u) { + let aabb_center = mesh_culling_data[input_index].aabb_center.xyz; + let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz; + + // Do an OBB-based frustum cull. + let model_center = world_from_local * vec4(aabb_center, 1.0); + if (!view_frustum_intersects_obb(world_from_local, model_center, aabb_half_extents)) { + return; + } + } +#endif + + // See whether the `MeshInputUniform` was updated on this frame. If it + // wasn't, then we know the transforms of this mesh must be identical to + // those on the previous frame, and therefore we don't need to access the + // `previous_input_index` (in fact, we can't; that index are only valid for + // one frame and will be invalid). + let timestamp = current_input[input_index].timestamp; + let mesh_changed_this_frame = timestamp == view.frame_count; + + // Look up the previous model matrix, if it could have been. + let previous_input_index = current_input[input_index].previous_input_index; + var previous_world_from_local_affine_transpose: mat3x4; + if (mesh_changed_this_frame && previous_input_index != 0xffffffffu) { + previous_world_from_local_affine_transpose = + previous_input[previous_input_index].world_from_local; + } else { + previous_world_from_local_affine_transpose = world_from_local_affine_transpose; + } + let previous_world_from_local = + maths::affine3_to_square(previous_world_from_local_affine_transpose); + + // Occlusion cull if necessary. This is done by calculating the screen-space + // axis-aligned bounding box (AABB) of the mesh and testing it against the + // appropriate level of the depth pyramid (a.k.a. hierarchical Z-buffer). If + // no part of the AABB is in front of the corresponding pixel quad in the + // hierarchical Z-buffer, then this mesh must be occluded, and we can skip + // rendering it. +#ifdef OCCLUSION_CULLING + let aabb_center = mesh_culling_data[input_index].aabb_center.xyz; + let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz; + + // Initialize the AABB and the maximum depth. + let infinity = bitcast(0x7f800000u); + let neg_infinity = bitcast(0xff800000u); + var aabb = vec4(infinity, infinity, neg_infinity, neg_infinity); + var max_depth_view = neg_infinity; + + // Build up the AABB by taking each corner of this mesh's OBB, transforming + // it, and updating the AABB and depth accordingly. + for (var i = 0u; i < 8u; i += 1u) { + let local_pos = aabb_center + select( + vec3(-1.0), + vec3(1.0), + vec3((i & 1) != 0, (i & 2) != 0, (i & 4) != 0) + ) * aabb_half_extents; + +#ifdef EARLY_PHASE + // If we're in the early phase, we're testing against the last frame's + // depth buffer, so we need to use the previous frame's transform. + let prev_world_pos = (previous_world_from_local * vec4(local_pos, 1.0)).xyz; + let view_pos = position_world_to_prev_view(prev_world_pos); + let ndc_pos = position_world_to_prev_ndc(prev_world_pos); +#else // EARLY_PHASE + // Otherwise, if this is the late phase, we use the current frame's + // transform. + let world_pos = (world_from_local * vec4(local_pos, 1.0)).xyz; + let view_pos = position_world_to_view(world_pos); + let ndc_pos = position_world_to_ndc(world_pos); +#endif // EARLY_PHASE + + let uv_pos = ndc_to_uv(ndc_pos.xy); + + // Update the AABB and maximum view-space depth. + aabb = vec4(min(aabb.xy, uv_pos), max(aabb.zw, uv_pos)); + max_depth_view = max(max_depth_view, view_pos.z); + } + + // Clip to the near plane to avoid the NDC depth becoming negative. +#ifdef EARLY_PHASE + max_depth_view = min(-previous_view_uniforms.clip_from_view[3][2], max_depth_view); +#else // EARLY_PHASE + max_depth_view = min(-view.clip_from_view[3][2], max_depth_view); +#endif // EARLY_PHASE + + // Figure out the depth of the occluder, and compare it to our own depth. + + let aabb_pixel_size = occlusion_culling::get_aabb_size_in_pixels(aabb, depth_pyramid); + let occluder_depth_ndc = + occlusion_culling::get_occluder_depth(aabb, aabb_pixel_size, depth_pyramid); + +#ifdef EARLY_PHASE + let max_depth_ndc = prev_view_z_to_depth_ndc(max_depth_view); +#else // EARLY_PHASE + let max_depth_ndc = view_z_to_depth_ndc(max_depth_view); +#endif + + // Are we culled out? + if (max_depth_ndc < occluder_depth_ndc) { +#ifdef EARLY_PHASE + // If this is the early phase, we need to make a note of this mesh so + // that we examine it again in the late phase, so that we handle the + // case in which a mesh that was invisible last frame became visible in + // this frame. + let output_work_item_index = atomicAdd(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].work_item_count, 1u); + if (output_work_item_index % 64u == 0u) { + // Our workgroup size is 64, and the indirect parameters for the + // late mesh preprocessing phase are counted in workgroups, so if + // we're the first thread in this workgroup, bump the workgroup + // count. + atomicAdd(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].dispatch_x, 1u); + } + + // Enqueue a work item for the late prepass phase. + late_preprocess_work_items[output_work_item_index].input_index = input_index; + late_preprocess_work_items[output_work_item_index].output_or_indirect_parameters_index = + indirect_parameters_index; +#endif // EARLY_PHASE + // This mesh is culled. Skip it. + return; + } +#endif // OCCLUSION_CULLING + + // Calculate inverse transpose. + let local_from_world_transpose = transpose(maths::inverse_affine3(transpose( + world_from_local_affine_transpose))); + + // Pack inverse transpose. + let local_from_world_transpose_a = mat2x4( + vec4(local_from_world_transpose[0].xyz, local_from_world_transpose[1].x), + vec4(local_from_world_transpose[1].yz, local_from_world_transpose[2].xy)); + let local_from_world_transpose_b = local_from_world_transpose[2].z; + + // Figure out the output index. In indirect mode, this involves bumping the + // instance index in the indirect parameters metadata, which + // `build_indirect_params.wgsl` will use to generate the actual indirect + // parameters. Otherwise, this index was directly supplied to us. +#ifdef INDIRECT +#ifdef LATE_PHASE + let batch_output_index = atomicLoad( + &indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count + ) + atomicAdd( + &indirect_parameters_gpu_metadata[indirect_parameters_index].late_instance_count, + 1u + ); +#else // LATE_PHASE + let batch_output_index = atomicAdd( + &indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count, + 1u + ); +#endif // LATE_PHASE + + let mesh_output_index = + indirect_parameters_cpu_metadata[indirect_parameters_index].base_output_index + + batch_output_index; + +#endif // INDIRECT + + // Write the output. + output[mesh_output_index].world_from_local = world_from_local_affine_transpose; + output[mesh_output_index].previous_world_from_local = + previous_world_from_local_affine_transpose; + output[mesh_output_index].local_from_world_transpose_a = local_from_world_transpose_a; + output[mesh_output_index].local_from_world_transpose_b = local_from_world_transpose_b; + output[mesh_output_index].flags = current_input[input_index].flags; + output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect; + output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; + output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index; + output[mesh_output_index].material_and_lightmap_bind_group_slot = + current_input[input_index].material_and_lightmap_bind_group_slot; + output[mesh_output_index].tag = current_input[input_index].tag; +} diff --git a/vendor/bevy_pbr/src/render/mesh_types.wgsl b/vendor/bevy_pbr/src/render/mesh_types.wgsl new file mode 100644 index 00000000..476391d6 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_types.wgsl @@ -0,0 +1,47 @@ +#define_import_path bevy_pbr::mesh_types + +struct Mesh { + // Affine 4x3 matrices transposed to 3x4 + // Use bevy_render::maths::affine3_to_square to unpack + world_from_local: mat3x4, + previous_world_from_local: mat3x4, + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + // Use bevy_pbr::mesh_functions::mat2x4_f32_to_mat3x3_unpack to unpack + local_from_world_transpose_a: mat2x4, + local_from_world_transpose_b: f32, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + lightmap_uv_rect: vec2, + // The index of the mesh's first vertex in the vertex buffer. + first_vertex_index: u32, + current_skin_index: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, + // User supplied index to identify the mesh instance + tag: u32, + pad: u32, +}; + +#ifdef SKINNED +struct SkinnedMesh { + data: array, 256u>, +}; +#endif + +#ifdef MORPH_TARGETS +struct MorphWeights { + weights: array, 64u>, // 64 = 256 / 4 (256 = MAX_MORPH_WEIGHTS) +}; +#endif + +// [2^0, 2^16) +const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = (1u << 16u) - 1u; +const MESH_FLAGS_NO_FRUSTUM_CULLING_BIT: u32 = 1u << 28u; +const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 29u; +const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1u << 30u; +// if the flag is set, the sign is positive, else it is negative +const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 1u << 31u; diff --git a/vendor/bevy_pbr/src/render/mesh_view_bindings.rs b/vendor/bevy_pbr/src/render/mesh_view_bindings.rs new file mode 100644 index 00000000..78e7f3e6 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_view_bindings.rs @@ -0,0 +1,869 @@ +use alloc::sync::Arc; +use bevy_core_pipeline::{ + core_3d::ViewTransmissionTexture, + oit::{resolve::is_oit_supported, OitBuffers, OrderIndependentTransparencySettings}, + prepass::ViewPrepassTextures, + tonemapping::{ + get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, + }, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::Has, + resource::Resource, + system::{Commands, Query, Res}, + world::{FromWorld, World}, +}; +use bevy_image::BevyDefault as _; +use bevy_light::{EnvironmentMapLight, IrradianceVolume}; +use bevy_math::Vec4; +use bevy_render::{ + globals::{GlobalsBuffer, GlobalsUniform}, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, + view::{ + Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, + VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, +}; +use core::{array, num::NonZero}; + +use crate::{ + decal::{ + self, + clustered::{ + DecalsBuffer, RenderClusteredDecals, RenderViewClusteredDecalBindGroupEntries, + }, + }, + environment_map::{self, RenderViewEnvironmentMapBindGroupEntries}, + irradiance_volume::{ + self, RenderViewIrradianceVolumeBindGroupEntries, IRRADIANCE_VOLUMES_ARE_USABLE, + }, + prepass, + resources::{AtmosphereBuffer, AtmosphereData, AtmosphereSampler, AtmosphereTextures}, + EnvironmentMapUniformBuffer, ExtractedAtmosphere, FogMeta, GlobalClusterableObjectMeta, + GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, + MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, + ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, + ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, +}; + +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +use bevy_render::render_resource::binding_types::texture_cube; + +#[cfg(debug_assertions)] +use {crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES, bevy_utils::once, tracing::warn}; + +#[derive(Clone)] +pub struct MeshPipelineViewLayout { + pub main_layout: BindGroupLayoutDescriptor, + pub binding_array_layout: BindGroupLayoutDescriptor, + pub empty_layout: BindGroupLayoutDescriptor, + + #[cfg(debug_assertions)] + pub texture_count: usize, +} + +bitflags::bitflags! { + /// A key that uniquely identifies a [`MeshPipelineViewLayout`]. + /// + /// Used to generate all possible layouts for the mesh pipeline in [`generate_view_layouts`], + /// so special care must be taken to not add too many flags, as the number of possible layouts + /// will grow exponentially. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[repr(transparent)] + pub struct MeshPipelineViewLayoutKey: u32 { + const MULTISAMPLED = 1 << 0; + const DEPTH_PREPASS = 1 << 1; + const NORMAL_PREPASS = 1 << 2; + const MOTION_VECTOR_PREPASS = 1 << 3; + const DEFERRED_PREPASS = 1 << 4; + const OIT_ENABLED = 1 << 5; + const ATMOSPHERE = 1 << 6; + } +} + +impl MeshPipelineViewLayoutKey { + // The number of possible layouts + pub const COUNT: usize = Self::all().bits() as usize + 1; + + /// Builds a unique label for each layout based on the flags + pub fn label(&self) -> String { + use MeshPipelineViewLayoutKey as Key; + + format!( + "mesh_view_layout{}{}{}{}{}{}{}", + if self.contains(Key::MULTISAMPLED) { + "_multisampled" + } else { + Default::default() + }, + if self.contains(Key::DEPTH_PREPASS) { + "_depth" + } else { + Default::default() + }, + if self.contains(Key::NORMAL_PREPASS) { + "_normal" + } else { + Default::default() + }, + if self.contains(Key::MOTION_VECTOR_PREPASS) { + "_motion" + } else { + Default::default() + }, + if self.contains(Key::DEFERRED_PREPASS) { + "_deferred" + } else { + Default::default() + }, + if self.contains(Key::OIT_ENABLED) { + "_oit" + } else { + Default::default() + }, + if self.contains(Key::ATMOSPHERE) { + "_atmosphere" + } else { + Default::default() + }, + ) + } +} + +impl From for MeshPipelineViewLayoutKey { + fn from(value: MeshPipelineKey) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if value.msaa_samples() > 1 { + result |= MeshPipelineViewLayoutKey::MULTISAMPLED; + } + if value.contains(MeshPipelineKey::DEPTH_PREPASS) { + result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS; + } + if value.contains(MeshPipelineKey::NORMAL_PREPASS) { + result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS; + } + if value.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS; + } + if value.contains(MeshPipelineKey::DEFERRED_PREPASS) { + result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + } + if value.contains(MeshPipelineKey::OIT_ENABLED) { + result |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + if value.contains(MeshPipelineKey::ATMOSPHERE) { + result |= MeshPipelineViewLayoutKey::ATMOSPHERE; + } + + result + } +} + +impl From for MeshPipelineViewLayoutKey { + fn from(value: Msaa) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if value.samples() > 1 { + result |= MeshPipelineViewLayoutKey::MULTISAMPLED; + } + + result + } +} + +impl From> for MeshPipelineViewLayoutKey { + fn from(value: Option<&ViewPrepassTextures>) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if let Some(prepass_textures) = value { + if prepass_textures.depth.is_some() { + result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS; + } + if prepass_textures.normal.is_some() { + result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS; + } + if prepass_textures.motion_vectors.is_some() { + result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS; + } + if prepass_textures.deferred.is_some() { + result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + } + } + + result + } +} + +pub(crate) fn buffer_layout( + buffer_binding_type: BufferBindingType, + has_dynamic_offset: bool, + min_binding_size: Option>, +) -> BindGroupLayoutEntryBuilder { + match buffer_binding_type { + BufferBindingType::Uniform => uniform_buffer_sized(has_dynamic_offset, min_binding_size), + BufferBindingType::Storage { read_only } => { + if read_only { + storage_buffer_read_only_sized(has_dynamic_offset, min_binding_size) + } else { + storage_buffer_sized(has_dynamic_offset, min_binding_size) + } + } + } +} + +/// Returns the appropriate bind group layout vec based on the parameters +fn layout_entries( + clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, + layout_key: MeshPipelineViewLayoutKey, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [Vec; 2] { + // EnvironmentMapLight + let environment_map_entries = + environment_map::get_bind_group_layout_entries(render_device, render_adapter); + + let mut entries = DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::FRAGMENT, + ( + // View + ( + 0, + uniform_buffer::(true).visibility(ShaderStages::VERTEX_FRAGMENT), + ), + // Lights + (1, uniform_buffer::(true)), + // Point Shadow Texture Cube Array + ( + 2, + #[cfg(all( + not(target_abi = "sim"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + texture_cube_array(TextureSampleType::Depth), + #[cfg(any( + target_abi = "sim", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + texture_cube(TextureSampleType::Depth), + ), + // Point Shadow Texture Array Comparison Sampler + (3, sampler(SamplerBindingType::Comparison)), + // Point Shadow Texture Array Linear Sampler + #[cfg(feature = "experimental_pbr_pcss")] + (4, sampler(SamplerBindingType::Filtering)), + // Directional Shadow Texture Array + ( + 5, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + texture_2d_array(TextureSampleType::Depth), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + texture_2d(TextureSampleType::Depth), + ), + // Directional Shadow Texture Array Comparison Sampler + (6, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Linear Sampler + #[cfg(feature = "experimental_pbr_pcss")] + (7, sampler(SamplerBindingType::Filtering)), + // PointLights + ( + 8, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some(GpuClusterableObjects::min_size( + clustered_forward_buffer_binding_type, + )), + ), + ), + // ClusteredLightIndexLists + ( + 9, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some( + ViewClusterBindings::min_size_clusterable_object_index_lists( + clustered_forward_buffer_binding_type, + ), + ), + ), + ), + // ClusterOffsetsAndCounts + ( + 10, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some(ViewClusterBindings::min_size_cluster_offsets_and_counts( + clustered_forward_buffer_binding_type, + )), + ), + ), + // Globals + ( + 11, + uniform_buffer::(false).visibility(ShaderStages::VERTEX_FRAGMENT), + ), + // Fog + (12, uniform_buffer::(true)), + // Light probes + (13, uniform_buffer::(true)), + // Visibility ranges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + // Screen space reflection settings + (15, uniform_buffer::(true)), + // Screen space ambient occlusion texture + ( + 16, + texture_2d(TextureSampleType::Float { filterable: false }), + ), + (17, environment_map_entries[3]), + ), + ); + + // Tonemapping + let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); + entries = entries.extend_with_indices(( + (18, tonemapping_lut_entries[0]), + (19, tonemapping_lut_entries[1]), + )); + + // Prepass + if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) + || (cfg!(all(feature = "webgl", target_arch = "wasm32")) + && !layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)) + { + for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) + .iter() + .zip([20, 21, 22, 23]) + { + if let Some(entry) = entry { + entries = entries.extend_with_indices(((binding as u32, *entry),)); + } + } + } + + // View Transmission Texture + entries = entries.extend_with_indices(( + ( + 24, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + (25, sampler(SamplerBindingType::Filtering)), + )); + + // OIT + if layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) { + // Check if we can use OIT. This is a hack to avoid errors on webgl -- + // the OIT plugin will warn the user that OIT is not supported on their + // platform, so we don't need to do it here. + if is_oit_supported(render_adapter, render_device, false) { + entries = entries.extend_with_indices(( + // oit_layers + (26, storage_buffer_sized(false, None)), + // oit_layer_ids, + (27, storage_buffer_sized(false, None)), + // oit_layer_count + ( + 28, + uniform_buffer::(true), + ), + )); + } + } + + // Atmosphere + if layout_key.contains(MeshPipelineViewLayoutKey::ATMOSPHERE) { + entries = entries.extend_with_indices(( + // transmittance LUT + ( + 29, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + (30, sampler(SamplerBindingType::Filtering)), + // atmosphere data buffer + (31, storage_buffer_read_only::(false)), + )); + } + + let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT); + binding_array_entries = binding_array_entries.extend_with_indices(( + (0, environment_map_entries[0]), + (1, environment_map_entries[1]), + (2, environment_map_entries[2]), + )); + + // Irradiance volumes + if IRRADIANCE_VOLUMES_ARE_USABLE { + let irradiance_volume_entries = + irradiance_volume::get_bind_group_layout_entries(render_device, render_adapter); + binding_array_entries = binding_array_entries.extend_with_indices(( + (3, irradiance_volume_entries[0]), + (4, irradiance_volume_entries[1]), + )); + } + + // Clustered decals + if let Some(clustered_decal_entries) = + decal::clustered::get_bind_group_layout_entries(render_device, render_adapter) + { + binding_array_entries = binding_array_entries.extend_with_indices(( + (5, clustered_decal_entries[0]), + (6, clustered_decal_entries[1]), + (7, clustered_decal_entries[2]), + )); + } + + [entries.to_vec(), binding_array_entries.to_vec()] +} + +/// Stores the view layouts for every combination of pipeline keys. +/// +/// This is wrapped in an [`Arc`] so that it can be efficiently cloned and +/// placed inside specializable pipeline types. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct MeshPipelineViewLayouts( + pub Arc<[MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT]>, +); + +impl FromWorld for MeshPipelineViewLayouts { + fn from_world(world: &mut World) -> Self { + // Generates all possible view layouts for the mesh pipeline, based on all combinations of + // [`MeshPipelineViewLayoutKey`] flags. + + let render_device = world.resource::(); + let render_adapter = world.resource::(); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + let visibility_ranges_buffer_binding_type = render_device + .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + Self(Arc::new(array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + render_adapter, + ); + #[cfg(debug_assertions)] + let texture_count: usize = entries + .iter() + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) + .count(); + + MeshPipelineViewLayout { + main_layout: BindGroupLayoutDescriptor::new(key.label(), &entries[0]), + binding_array_layout: BindGroupLayoutDescriptor::new( + format!("{}_binding_array", key.label()), + &entries[1], + ), + empty_layout: BindGroupLayoutDescriptor::new(format!("{}_empty", key.label()), &[]), + #[cfg(debug_assertions)] + texture_count, + } + }))) + } +} + +impl MeshPipelineViewLayouts { + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { + let index = layout_key.bits() as usize; + let layout = &self[index]; + + #[cfg(debug_assertions)] + if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { + // Issue our own warning here because Naga's error message is a bit cryptic in this situation + once!(warn!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments.")); + } + + layout + } +} + +/// Generates all possible view layouts for the mesh pipeline, based on all combinations of +/// [`MeshPipelineViewLayoutKey`] flags. +pub fn generate_view_layouts( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, +) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] { + array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + render_adapter, + ); + + #[cfg(debug_assertions)] + let texture_count: usize = entries + .iter() + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) + .count(); + + MeshPipelineViewLayout { + main_layout: BindGroupLayoutDescriptor::new(key.label(), &entries[0]), + binding_array_layout: BindGroupLayoutDescriptor::new( + format!("{}_binding_array", key.label()), + &entries[1], + ), + empty_layout: BindGroupLayoutDescriptor::new(format!("{}_empty", key.label()), &[]), + #[cfg(debug_assertions)] + texture_count, + } + }) +} + +#[derive(Component)] +pub struct MeshViewBindGroup { + pub main: BindGroup, + pub binding_array: BindGroup, + pub empty: BindGroup, +} + +pub fn prepare_mesh_view_bind_groups( + mut commands: Commands, + (render_device, pipeline_cache, render_adapter): ( + Res, + Res, + Res, + ), + mesh_pipeline: Res, + shadow_samplers: Res, + (light_meta, global_light_meta): (Res, Res), + fog_meta: Res, + (view_uniforms, environment_map_uniform): (Res, Res), + views: Query<( + Entity, + &ViewShadowBindings, + &ViewClusterBindings, + &Msaa, + Option<&ScreenSpaceAmbientOcclusionResources>, + Option<&ViewPrepassTextures>, + Option<&ViewTransmissionTexture>, + &Tonemapping, + Option<&RenderViewLightProbes>, + Option<&RenderViewLightProbes>, + Has, + Option<&AtmosphereTextures>, + Has, + )>, + (images, mut fallback_images, fallback_image, fallback_image_zero): ( + Res>, + FallbackImageMsaa, + Res, + Res, + ), + globals_buffer: Res, + tonemapping_luts: Res, + light_probes_buffer: Res, + visibility_ranges: Res, + ssr_buffer: Res, + oit_buffers: Res, + (decals_buffer, render_decals, atmosphere_buffer, atmosphere_sampler): ( + Res, + Res, + Option>, + Option>, + ), +) { + if let ( + Some(view_binding), + Some(light_binding), + Some(clusterable_objects_binding), + Some(globals), + Some(fog_binding), + Some(light_probes_binding), + Some(visibility_ranges_buffer), + Some(ssr_binding), + Some(environment_map_binding), + ) = ( + view_uniforms.uniforms.binding(), + light_meta.view_gpu_lights.binding(), + global_light_meta.gpu_clusterable_objects.binding(), + globals_buffer.buffer.binding(), + fog_meta.gpu_fogs.binding(), + light_probes_buffer.binding(), + visibility_ranges.buffer().buffer(), + ssr_buffer.binding(), + environment_map_uniform.binding(), + ) { + for ( + entity, + shadow_bindings, + cluster_bindings, + msaa, + ssao_resources, + prepass_textures, + transmission_texture, + tonemapping, + render_view_environment_maps, + render_view_irradiance_volumes, + has_oit, + atmosphere_textures, + has_atmosphere, + ) in &views + { + let fallback_ssao = fallback_images + .image_for_samplecount(1, TextureFormat::bevy_default()) + .texture_view + .clone(); + let ssao_view = ssao_resources + .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) + .unwrap_or(&fallback_ssao); + + let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa) + | MeshPipelineViewLayoutKey::from(prepass_textures); + if has_oit { + layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + if has_atmosphere { + layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE; + } + + let layout = mesh_pipeline.get_view_layout(layout_key); + + let mut entries = DynamicBindGroupEntries::new_with_indices(( + (0, view_binding.clone()), + (1, light_binding.clone()), + (2, &shadow_bindings.point_light_depth_texture_view), + (3, &shadow_samplers.point_light_comparison_sampler), + #[cfg(feature = "experimental_pbr_pcss")] + (4, &shadow_samplers.point_light_linear_sampler), + (5, &shadow_bindings.directional_light_depth_texture_view), + (6, &shadow_samplers.directional_light_comparison_sampler), + #[cfg(feature = "experimental_pbr_pcss")] + (7, &shadow_samplers.directional_light_linear_sampler), + (8, clusterable_objects_binding.clone()), + ( + 9, + cluster_bindings + .clusterable_object_index_lists_binding() + .unwrap(), + ), + (10, cluster_bindings.offsets_and_counts_binding().unwrap()), + (11, globals.clone()), + (12, fog_binding.clone()), + (13, light_probes_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + (15, ssr_binding.clone()), + (16, ssao_view), + )); + + entries = entries.extend_with_indices(((17, environment_map_binding.clone()),)); + + let lut_bindings = + get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); + entries = entries.extend_with_indices(((18, lut_bindings.0), (19, lut_bindings.1))); + + // When using WebGL, we can't have a depth texture with multisampling + let prepass_bindings; + if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || msaa.samples() == 1 + { + prepass_bindings = prepass::get_bindings(prepass_textures); + for (binding, index) in prepass_bindings + .iter() + .map(Option::as_ref) + .zip([20, 21, 22, 23]) + .flat_map(|(b, i)| b.map(|b| (b, i))) + { + entries = entries.extend_with_indices(((index, binding),)); + } + }; + + let transmission_view = transmission_texture + .map(|transmission| &transmission.view) + .unwrap_or(&fallback_image_zero.texture_view); + + let transmission_sampler = transmission_texture + .map(|transmission| &transmission.sampler) + .unwrap_or(&fallback_image_zero.sampler); + + entries = + entries.extend_with_indices(((24, transmission_view), (25, transmission_sampler))); + + if has_oit + && let ( + Some(oit_layers_binding), + Some(oit_layer_ids_binding), + Some(oit_settings_binding), + ) = ( + oit_buffers.layers.binding(), + oit_buffers.layer_ids.binding(), + oit_buffers.settings.binding(), + ) + { + entries = entries.extend_with_indices(( + (26, oit_layers_binding.clone()), + (27, oit_layer_ids_binding.clone()), + (28, oit_settings_binding.clone()), + )); + } + + if has_atmosphere + && let Some(atmosphere_textures) = atmosphere_textures + && let Some(atmosphere_buffer) = atmosphere_buffer.as_ref() + && let Some(atmosphere_sampler) = atmosphere_sampler.as_ref() + && let Some(atmosphere_buffer_binding) = atmosphere_buffer.buffer.binding() + { + entries = entries.extend_with_indices(( + (29, &atmosphere_textures.transmittance_lut.default_view), + (30, &***atmosphere_sampler), + (31, atmosphere_buffer_binding), + )); + } + + let mut entries_binding_array = DynamicBindGroupEntries::new(); + + let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( + render_view_environment_maps, + &images, + &fallback_image, + &render_device, + &render_adapter, + ); + match environment_map_bind_group_entries { + RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view, + specular_texture_view, + sampler, + } => { + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_view), + (1, specular_texture_view), + (2, sampler), + )); + } + RenderViewEnvironmentMapBindGroupEntries::Multiple { + ref diffuse_texture_views, + ref specular_texture_views, + sampler, + } => { + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_views.as_slice()), + (1, specular_texture_views.as_slice()), + (2, sampler), + )); + } + } + + let irradiance_volume_bind_group_entries = if IRRADIANCE_VOLUMES_ARE_USABLE { + Some(RenderViewIrradianceVolumeBindGroupEntries::get( + render_view_irradiance_volumes, + &images, + &fallback_image, + &render_device, + &render_adapter, + )) + } else { + None + }; + + match irradiance_volume_bind_group_entries { + Some(RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view, + sampler, + }) => { + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_view), (4, sampler))); + } + Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { + ref texture_views, + sampler, + }) => { + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_views.as_slice()), (4, sampler))); + } + None => {} + } + + let decal_bind_group_entries = RenderViewClusteredDecalBindGroupEntries::get( + &render_decals, + &decals_buffer, + &images, + &fallback_image, + &render_device, + &render_adapter, + ); + + // Add the decal bind group entries. + if let Some(ref render_view_decal_bind_group_entries) = decal_bind_group_entries { + entries_binding_array = entries_binding_array.extend_with_indices(( + // `clustered_decals` + ( + 5, + render_view_decal_bind_group_entries + .decals + .as_entire_binding(), + ), + // `clustered_decal_textures` + ( + 6, + render_view_decal_bind_group_entries + .texture_views + .as_slice(), + ), + // `clustered_decal_sampler` + (7, render_view_decal_bind_group_entries.sampler), + )); + } + + commands.entity(entity).insert(MeshViewBindGroup { + main: render_device.create_bind_group( + "mesh_view_bind_group", + &pipeline_cache.get_bind_group_layout(&layout.main_layout), + &entries, + ), + binding_array: render_device.create_bind_group( + "mesh_view_bind_group_binding_array", + &pipeline_cache.get_bind_group_layout(&layout.binding_array_layout), + &entries_binding_array, + ), + empty: render_device.create_bind_group( + "mesh_view_bind_group_empty", + &pipeline_cache.get_bind_group_layout(&layout.empty_layout), + &[], + ), + }); + } + } +} diff --git a/vendor/bevy_pbr/src/render/mesh_view_bindings.wgsl b/vendor/bevy_pbr/src/render/mesh_view_bindings.wgsl new file mode 100644 index 00000000..5aef30ba --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -0,0 +1,126 @@ +#define_import_path bevy_pbr::mesh_view_bindings + +#import bevy_pbr::mesh_view_types as types +#import bevy_pbr::atmosphere::types as atmosphere +#import bevy_render::{ + view::View, + globals::Globals, +} + +@group(0) @binding(0) var view: View; +@group(0) @binding(1) var lights: types::Lights; +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT +@group(0) @binding(2) var point_shadow_textures: texture_depth_cube; +#else +@group(0) @binding(2) var point_shadow_textures: texture_depth_cube_array; +#endif +@group(0) @binding(3) var point_shadow_textures_comparison_sampler: sampler_comparison; +#ifdef PCSS_SAMPLERS_AVAILABLE +@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler; +#endif // PCSS_SAMPLERS_AVAILABLE +#ifdef NO_ARRAY_TEXTURES_SUPPORT +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d; +#else +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array; +#endif +@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison; +#ifdef PCSS_SAMPLERS_AVAILABLE +@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler; +#endif // PCSS_SAMPLERS_AVAILABLE + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +#else +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +#endif + +@group(0) @binding(11) var globals: Globals; +@group(0) @binding(12) var fog: types::Fog; +@group(0) @binding(13) var light_probes: types::LightProbes; + +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 +@group(0) @binding(14) var visibility_ranges: array>; +#else +@group(0) @binding(14) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; +#endif + +@group(0) @binding(15) var ssr_settings: types::ScreenSpaceReflectionsSettings; +@group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(17) var environment_map_uniform: types::EnvironmentMapUniform; + +// NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. +@group(0) @binding(18) var dt_lut_texture: texture_3d; +@group(0) @binding(19) var dt_lut_sampler: sampler; + +#ifdef MULTISAMPLED +#ifdef DEPTH_PREPASS +@group(0) @binding(20) var depth_prepass_texture: texture_depth_multisampled_2d; +#endif // DEPTH_PREPASS +#ifdef NORMAL_PREPASS +@group(0) @binding(21) var normal_prepass_texture: texture_multisampled_2d; +#endif // NORMAL_PREPASS +#ifdef MOTION_VECTOR_PREPASS +@group(0) @binding(22) var motion_vector_prepass_texture: texture_multisampled_2d; +#endif // MOTION_VECTOR_PREPASS + +#else // MULTISAMPLED + +#ifdef DEPTH_PREPASS +@group(0) @binding(20) var depth_prepass_texture: texture_depth_2d; +#endif // DEPTH_PREPASS +#ifdef NORMAL_PREPASS +@group(0) @binding(21) var normal_prepass_texture: texture_2d; +#endif // NORMAL_PREPASS +#ifdef MOTION_VECTOR_PREPASS +@group(0) @binding(22) var motion_vector_prepass_texture: texture_2d; +#endif // MOTION_VECTOR_PREPASS + +#endif // MULTISAMPLED + +#ifdef DEFERRED_PREPASS +@group(0) @binding(23) var deferred_prepass_texture: texture_2d; +#endif // DEFERRED_PREPASS + +@group(0) @binding(24) var view_transmission_texture: texture_2d; +@group(0) @binding(25) var view_transmission_sampler: sampler; + +#ifdef OIT_ENABLED +@group(0) @binding(26) var oit_layers: array>; +@group(0) @binding(27) var oit_layer_ids: array>; +@group(0) @binding(28) var oit_settings: types::OrderIndependentTransparencySettings; +#endif // OIT_ENABLED + +#ifdef ATMOSPHERE +@group(0) @binding(29) var atmosphere_transmittance_texture: texture_2d; +@group(0) @binding(30) var atmosphere_transmittance_sampler: sampler; +@group(0) @binding(31) var atmosphere_data: atmosphere::AtmosphereData; +#endif // ATMOSPHERE + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(0) var diffuse_environment_maps: binding_array, 8u>; +@group(1) @binding(1) var specular_environment_maps: binding_array, 8u>; +#else +@group(1) @binding(0) var diffuse_environment_map: texture_cube; +@group(1) @binding(1) var specular_environment_map: texture_cube; +#endif +@group(1) @binding(2) var environment_map_sampler: sampler; + +#ifdef IRRADIANCE_VOLUMES_ARE_USABLE +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(3) var irradiance_volumes: binding_array, 8u>; +#else +@group(1) @binding(3) var irradiance_volume: texture_3d; +#endif +@group(1) @binding(4) var irradiance_volume_sampler: sampler; +#endif + +#ifdef CLUSTERED_DECALS_ARE_USABLE +@group(1) @binding(5) var clustered_decals: types::ClusteredDecals; +@group(1) @binding(6) var clustered_decal_textures: binding_array, 8u>; +@group(1) @binding(7) var clustered_decal_sampler: sampler; +#endif // CLUSTERED_DECALS_ARE_USABLE diff --git a/vendor/bevy_pbr/src/render/mesh_view_types.wgsl b/vendor/bevy_pbr/src/render/mesh_view_types.wgsl new file mode 100644 index 00000000..112556ee --- /dev/null +++ b/vendor/bevy_pbr/src/render/mesh_view_types.wgsl @@ -0,0 +1,191 @@ +#define_import_path bevy_pbr::mesh_view_types + +struct ClusterableObject { + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + // For spot lights: the direction (x,z), spot_scale and spot_offset + light_custom_data: vec4, + color_inverse_square_range: vec4, + position_radius: vec4, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + spot_light_tan_angle: f32, + soft_shadow_size: f32, + shadow_map_near_z: f32, + decal_index: u32, + pad: f32, +}; + +const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 1u << 1u; +const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; +const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; + +struct DirectionalCascade { + clip_from_world: mat4x4, + texel_size: f32, + far_bound: f32, +} + +struct DirectionalLight { + cascades: array, + color: vec4, + direction_to_light: vec3, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + soft_shadow_size: f32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, + depth_texture_base_index: u32, + decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, +}; + +const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 1u; +const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 2u; + +struct Lights { + // NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs + directional_lights: array, + ambient_color: vec4, + // x/y/z dimensions and n_clusters in w + cluster_dimensions: vec4, + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // + // For perspective projections: + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + // + // For orthographic projections: + // NOTE: near and far are +ve but -z is infront of the camera + // z is -near + // w is cluster_dimensions.z / (-far - -near) + cluster_factors: vec4, + n_directional_lights: u32, + spot_light_shadowmap_offset: i32, + ambient_light_affects_lightmapped_meshes: u32 +}; + +struct Fog { + base_color: vec4, + directional_light_color: vec4, + // `be` and `bi` are allocated differently depending on the fog mode + // + // For Linear Fog: + // be.x = start, be.y = end + // For Exponential and ExponentialSquared Fog: + // be.x = density + // For Atmospheric Fog: + // be = per-channel extinction density + // bi = per-channel inscattering density + be: vec3, + directional_light_exponent: f32, + bi: vec3, + mode: u32, +} + +// Important: These must be kept in sync with `fog.rs` +const FOG_MODE_OFF: u32 = 0u; +const FOG_MODE_LINEAR: u32 = 1u; +const FOG_MODE_EXPONENTIAL: u32 = 2u; +const FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3u; +const FOG_MODE_ATMOSPHERIC: u32 = 4u; + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +struct ClusterableObjects { + data: array, +}; +struct ClusterLightIndexLists { + data: array, +}; +struct ClusterOffsetsAndCounts { + data: array, 2>>, +}; +#else +struct ClusterableObjects { + data: array, +}; +struct ClusterLightIndexLists { + // each u32 contains 4 u8 indices into the ClusterableObjects array + data: array, 1024u>, +}; +struct ClusterOffsetsAndCounts { + // each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits + // and an 8-bit count of the number of lights in the low 8 bits + data: array, 1024u>, +}; +#endif + +struct LightProbe { + // This is stored as the transpose in order to save space in this structure. + // It'll be transposed in the `environment_map_light` function. + light_from_world_transposed: mat3x4, + cubemap_index: i32, + intensity: f32, + // Whether this light probe contributes diffuse light to lightmapped meshes. + affects_lightmapped_mesh_diffuse: u32, +}; + +struct LightProbes { + // This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side. + reflection_probes: array, + irradiance_volumes: array, + reflection_probe_count: i32, + irradiance_volume_count: i32, + // The index of the view environment map cubemap binding, or -1 if there's + // no such cubemap. + view_cubemap_index: i32, + // The smallest valid mipmap level for the specular environment cubemap + // associated with the view. + smallest_specular_mip_level_for_view: u32, + // The intensity of the environment map associated with the view. + intensity_for_view: f32, + // Whether the environment map attached to the view affects the diffuse + // lighting for lightmapped meshes. + view_environment_map_affects_lightmapped_mesh_diffuse: u32, +}; + +// Settings for screen space reflections. +// +// For more information on these settings, see the documentation for +// `bevy_pbr::ssr::ScreenSpaceReflections`. +struct ScreenSpaceReflectionsSettings { + perceptual_roughness_threshold: f32, + thickness: f32, + linear_steps: u32, + linear_march_exponent: f32, + bisection_steps: u32, + use_secant: u32, +}; + +struct EnvironmentMapUniform { + // Transformation matrix for the environment cubemaps in world space. + transform: mat4x4, +}; + +// Shader version of the order independent transparency settings component. +struct OrderIndependentTransparencySettings { + layers_count: i32, + alpha_threshold: f32, +}; + +struct ClusteredDecal { + local_from_world: mat4x4, + base_color_texture_index: i32, + normal_map_texture_index: i32, + metallic_roughness_texture_index: i32, + emissive_texture_index: i32, + tag: u32, + pad_a: u32, + pad_b: u32, + pad_c: u32, +} + +struct ClusteredDecals { + decals: array, +} diff --git a/vendor/bevy_pbr/src/render/mod.rs b/vendor/bevy_pbr/src/render/mod.rs new file mode 100644 index 00000000..6a298230 --- /dev/null +++ b/vendor/bevy_pbr/src/render/mod.rs @@ -0,0 +1,17 @@ +mod fog; +mod gpu_preprocess; +mod light; +pub(crate) mod mesh; +mod mesh_bindings; +mod mesh_view_bindings; +mod morph; +pub(crate) mod skin; + +pub use fog::*; +pub use gpu_preprocess::*; +pub use light::*; +pub use mesh::*; +pub use mesh_bindings::MeshLayouts; +pub use mesh_view_bindings::*; +pub use morph::*; +pub use skin::{extract_skins, prepare_skins, skins_use_uniform_buffers, SkinUniforms, MAX_JOINTS}; diff --git a/vendor/bevy_pbr/src/render/morph.rs b/vendor/bevy_pbr/src/render/morph.rs new file mode 100644 index 00000000..c742c773 --- /dev/null +++ b/vendor/bevy_pbr/src/render/morph.rs @@ -0,0 +1,154 @@ +use core::{iter, mem}; + +use bevy_camera::visibility::ViewVisibility; +use bevy_ecs::prelude::*; +use bevy_mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS}; +use bevy_render::sync_world::MainEntityHashMap; +use bevy_render::{ + batching::NoAutomaticBatching, + render_resource::{BufferUsages, RawBufferVec}, + renderer::{RenderDevice, RenderQueue}, + Extract, +}; +use bytemuck::NoUninit; + +#[derive(Component)] +pub struct MorphIndex { + pub index: u32, +} + +/// Maps each mesh affected by morph targets to the applicable offset within the +/// [`MorphUniforms`] buffer. +/// +/// We store both the current frame's mapping and the previous frame's mapping +/// for the purposes of motion vector calculation. +#[derive(Default, Resource)] +pub struct MorphIndices { + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::current_buffer`]. + pub current: MainEntityHashMap, + + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::prev_buffer`]. + pub prev: MainEntityHashMap, +} + +/// The GPU buffers containing morph weights for all meshes with morph targets. +/// +/// This is double-buffered: we store the weights of the previous frame in +/// addition to those of the current frame. This is for motion vector +/// calculation. Every frame, we swap buffers and reuse the morph target weight +/// buffer from two frames ago for the current frame. +#[derive(Resource)] +pub struct MorphUniforms { + /// The morph weights for the current frame. + pub current_buffer: RawBufferVec, + /// The morph weights for the previous frame. + pub prev_buffer: RawBufferVec, +} + +impl Default for MorphUniforms { + fn default() -> Self { + Self { + current_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + prev_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + } + } +} + +pub fn prepare_morphs( + render_device: Res, + render_queue: Res, + mut uniform: ResMut, +) { + if uniform.current_buffer.is_empty() { + return; + } + let len = uniform.current_buffer.len(); + uniform.current_buffer.reserve(len, &render_device); + uniform + .current_buffer + .write_buffer(&render_device, &render_queue); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. +} + +const fn can_align(step: usize, target: usize) -> bool { + step.is_multiple_of(target) || target.is_multiple_of(step) +} + +const WGPU_MIN_ALIGN: usize = 256; + +/// Align a [`RawBufferVec`] to `N` bytes by padding the end with `T::default()` values. +fn add_to_alignment(buffer: &mut RawBufferVec) { + let n = WGPU_MIN_ALIGN; + let t_size = size_of::(); + if !can_align(n, t_size) { + // This panic is stripped at compile time, due to n, t_size and can_align being const + panic!( + "RawBufferVec should contain only types with a size multiple or divisible by {n}, \ + {} has a size of {t_size}, which is neither multiple or divisible by {n}", + core::any::type_name::() + ); + } + + let buffer_size = buffer.len(); + let byte_size = t_size * buffer_size; + let bytes_over_n = byte_size % n; + if bytes_over_n == 0 { + return; + } + let bytes_to_add = n - bytes_over_n; + let ts_to_add = bytes_to_add / t_size; + buffer.extend(iter::repeat_with(T::default).take(ts_to_add)); +} + +// Notes on implementation: see comment on top of the extract_skins system in skin module. +// This works similarly, but for `f32` instead of `Mat4` +pub fn extract_morphs( + morph_indices: ResMut, + uniform: ResMut, + query: Extract>, +) { + // Borrow check workaround. + let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner()); + + // Swap buffers. We need to keep the previous frame's buffer around for the + // purposes of motion vector computation. + mem::swap(&mut morph_indices.current, &mut morph_indices.prev); + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + morph_indices.current.clear(); + uniform.current_buffer.clear(); + + for (entity, view_visibility, morph_weights) in &query { + if !view_visibility.get() { + continue; + } + let start = uniform.current_buffer.len(); + let weights = morph_weights.weights(); + let legal_weights = weights + .iter() + .chain(iter::repeat(&0.0)) + .take(MAX_MORPH_WEIGHTS) + .copied(); + uniform.current_buffer.extend(legal_weights); + add_to_alignment::(&mut uniform.current_buffer); + + let index = (start * size_of::()) as u32; + morph_indices + .current + .insert(entity.into(), MorphIndex { index }); + } +} + +// NOTE: Because morph targets require per-morph target texture bindings, they cannot +// currently be batched. +pub fn no_automatic_morph_batching( + mut commands: Commands, + query: Query, Without)>, +) { + for entity in &query { + commands.entity(entity).try_insert(NoAutomaticBatching); + } +} diff --git a/vendor/bevy_pbr/src/render/morph.wgsl b/vendor/bevy_pbr/src/render/morph.wgsl new file mode 100644 index 00000000..6689d68c --- /dev/null +++ b/vendor/bevy_pbr/src/render/morph.wgsl @@ -0,0 +1,52 @@ +#define_import_path bevy_pbr::morph + +#ifdef MORPH_TARGETS + +#import bevy_pbr::mesh_types::MorphWeights; + +@group(2) @binding(2) var morph_weights: MorphWeights; +@group(2) @binding(3) var morph_targets: texture_3d; +@group(2) @binding(7) var prev_morph_weights: MorphWeights; + +// NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct +// in crates/bevy_render/src/mesh/morph/visitors.rs +// In an ideal world, the offsets are established dynamically and passed as #defines +// to the shader, but it's out of scope for the initial implementation of morph targets. +const position_offset: u32 = 0u; +const normal_offset: u32 = 3u; +const tangent_offset: u32 = 6u; +const total_component_count: u32 = 9u; + +fn layer_count() -> u32 { + let dimensions = textureDimensions(morph_targets); + return u32(dimensions.z); +} +fn component_texture_coord(vertex_index: u32, component_offset: u32) -> vec2 { + let width = u32(textureDimensions(morph_targets).x); + let component_index = total_component_count * vertex_index + component_offset; + return vec2(component_index % width, component_index / width); +} +fn weight_at(weight_index: u32) -> f32 { + let i = weight_index; + return morph_weights.weights[i / 4u][i % 4u]; +} +fn prev_weight_at(weight_index: u32) -> f32 { + let i = weight_index; + return prev_morph_weights.weights[i / 4u][i % 4u]; +} +fn morph_pixel(vertex: u32, component: u32, weight: u32) -> f32 { + let coord = component_texture_coord(vertex, component); + // Due to https://gpuweb.github.io/gpuweb/wgsl/#texel-formats + // While the texture stores a f32, the textureLoad returns a vec4<>, where + // only the first component is set. + return textureLoad(morph_targets, vec3(coord, weight), 0).r; +} +fn morph(vertex_index: u32, component_offset: u32, weight_index: u32) -> vec3 { + return vec3( + morph_pixel(vertex_index, component_offset, weight_index), + morph_pixel(vertex_index, component_offset + 1u, weight_index), + morph_pixel(vertex_index, component_offset + 2u, weight_index), + ); +} + +#endif // MORPH_TARGETS diff --git a/vendor/bevy_pbr/src/render/occlusion_culling.wgsl b/vendor/bevy_pbr/src/render/occlusion_culling.wgsl new file mode 100644 index 00000000..1be999cc --- /dev/null +++ b/vendor/bevy_pbr/src/render/occlusion_culling.wgsl @@ -0,0 +1,30 @@ +// Occlusion culling utility functions. + +#define_import_path bevy_pbr::occlusion_culling + +fn get_aabb_size_in_pixels(aabb: vec4, depth_pyramid: texture_2d) -> vec2 { + let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); + let aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; + let aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; + return vec2(aabb_width_pixels, aabb_height_pixels); +} + +fn get_occluder_depth( + aabb: vec4, + aabb_pixel_size: vec2, + depth_pyramid: texture_2d +) -> f32 { + let aabb_width_pixels = aabb_pixel_size.x; + let aabb_height_pixels = aabb_pixel_size.y; + + let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); + let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32 + let depth_pyramid_size = vec2(textureDimensions(depth_pyramid, depth_level)); + let aabb_top_left = vec2(aabb.xy * depth_pyramid_size); + + let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x; + let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x; + let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x; + let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x; + return min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); +} diff --git a/vendor/bevy_pbr/src/render/parallax_mapping.wgsl b/vendor/bevy_pbr/src/render/parallax_mapping.wgsl new file mode 100644 index 00000000..9005734d --- /dev/null +++ b/vendor/bevy_pbr/src/render/parallax_mapping.wgsl @@ -0,0 +1,139 @@ +#define_import_path bevy_pbr::parallax_mapping + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + pbr_bindings::{depth_map_texture, depth_map_sampler}, + mesh_bindings::mesh +} + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +fn sample_depth_map(uv: vec2, material_bind_group_slot: u32) -> f32 { + // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 + // backend (Fxc) panics when using "gradient instructions" inside a loop. + // It results in the whole loop being unrolled by the shader compiler, + // which it can't do because the upper limit of the loop in steep parallax + // mapping is a variable set by the user. + // The "gradient instructions" comes from `textureSample` computing MIP level + // based on UV derivative. With `textureSampleLevel`, we provide ourselves + // the MIP level, so no gradient instructions are used, and we can use + // sample_depth_map in our loop. + // See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing + return textureSampleLevel( +#ifdef BINDLESS + bindless_textures_2d[material_indices[material_bind_group_slot].depth_map_texture], + bindless_samplers_filtering[material_indices[material_bind_group_slot].depth_map_sampler], +#else // BINDLESS + depth_map_texture, + depth_map_sampler, +#endif // BINDLESS + uv, + 0.0 + ).r; +} + +// An implementation of parallax mapping, see https://en.wikipedia.org/wiki/Parallax_mapping +// Code derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28 +fn parallaxed_uv( + depth_scale: f32, + max_layer_count: f32, + max_steps: u32, + // The original interpolated uv + original_uv: vec2, + // The vector from the camera to the fragment at the surface in tangent space + Vt: vec3, + material_bind_group_slot: u32, +) -> vec2 { + if max_layer_count < 1.0 { + return original_uv; + } + var uv = original_uv; + + // Steep Parallax Mapping + // ====================== + // Split the depth map into `layer_count` layers. + // When Vt hits the surface of the mesh (excluding depth displacement), + // if the depth is not below or on surface including depth displacement (textureSample), then + // look forward (+= delta_uv) on depth texture according to + // Vt and distance between hit surface and depth map surface, + // repeat until below the surface. + // + // Where `layer_count` is interpolated between `1.0` and + // `max_layer_count` according to the steepness of Vt. + + let view_steepness = abs(Vt.z); + // We mix with minimum value 1.0 because otherwise, + // with 0.0, we get a division by zero in surfaces parallel to viewport, + // resulting in a singularity. + let layer_count = mix(max_layer_count, 1.0, view_steepness); + let layer_depth = 1.0 / layer_count; + var delta_uv = depth_scale * layer_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + + var current_layer_depth = 0.0; + var texture_depth = sample_depth_map(uv, material_bind_group_slot); + + // texture_depth > current_layer_depth means the depth map depth is deeper + // than the depth the ray would be at this UV offset so the ray has not + // intersected the surface + for (var i: i32 = 0; texture_depth > current_layer_depth && i <= i32(layer_count); i++) { + current_layer_depth += layer_depth; + uv += delta_uv; + texture_depth = sample_depth_map(uv, material_bind_group_slot); + } + +#ifdef RELIEF_MAPPING + // Relief Mapping + // ============== + // "Refine" the rough result from Steep Parallax Mapping + // with a **binary search** between the layer selected by steep parallax + // and the next one to find a point closer to the depth map surface. + // This reduces the jaggy step artifacts from steep parallax mapping. + + delta_uv *= 0.5; + var delta_depth = 0.5 * layer_depth; + + uv -= delta_uv; + current_layer_depth -= delta_depth; + + for (var i: u32 = 0u; i < max_steps; i++) { + texture_depth = sample_depth_map(uv, material_bind_group_slot); + + // Halve the deltas for the next step + delta_uv *= 0.5; + delta_depth *= 0.5; + + // Step based on whether the current depth is above or below the depth map + if (texture_depth > current_layer_depth) { + uv += delta_uv; + current_layer_depth += delta_depth; + } else { + uv -= delta_uv; + current_layer_depth -= delta_depth; + } + } +#else + // Parallax Occlusion mapping + // ========================== + // "Refine" Steep Parallax Mapping by interpolating between the + // previous layer's depth and the computed layer depth. + // Only requires a single lookup, unlike Relief Mapping, but + // may skip small details and result in writhing material artifacts. + let previous_uv = uv - delta_uv; + let next_depth = texture_depth - current_layer_depth; + let previous_depth = sample_depth_map(previous_uv, material_bind_group_slot) - + current_layer_depth + layer_depth; + + let weight = next_depth / (next_depth - previous_depth); + + uv = mix(uv, previous_uv, weight); + + current_layer_depth += mix(next_depth, previous_depth, weight); +#endif + + // Note: `current_layer_depth` is not returned, but may be useful + // for light computation later on in future improvements of the pbr shader. + return uv; +} diff --git a/vendor/bevy_pbr/src/render/pbr.wgsl b/vendor/bevy_pbr/src/render/pbr.wgsl new file mode 100644 index 00000000..ea2b33ee --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr.wgsl @@ -0,0 +1,106 @@ +#import bevy_pbr::{ + pbr_types, + pbr_functions::alpha_discard, + pbr_fragment::pbr_input_from_standard_material, + decal::clustered::apply_decals, +} + +#ifdef PREPASS_PIPELINE +#import bevy_pbr::{ + prepass_io::{VertexOutput, FragmentOutput}, + pbr_deferred_functions::deferred_output, +} +#else +#import bevy_pbr::{ + forward_io::{VertexOutput, FragmentOutput}, + pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, + pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, +} +#endif + +#ifdef VISIBILITY_RANGE_DITHER +#import bevy_pbr::pbr_functions::visibility_range_dither; +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + +#ifdef OIT_ENABLED +#import bevy_core_pipeline::oit::oit_draw +#endif // OIT_ENABLED + +#ifdef FORWARD_DECAL +#import bevy_pbr::decal::forward::get_forward_decal_info +#endif + +@fragment +fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else + vertex_output: VertexOutput, + @builtin(front_facing) is_front: bool, +#endif +) -> FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let vertex_output = resolve_vertex_output(frag_coord); + let is_front = true; +#endif + + var in = vertex_output; + + // If we're in the crossfade section of a visibility range, conditionally + // discard the fragment according to the visibility pattern. +#ifdef VISIBILITY_RANGE_DITHER + visibility_range_dither(in.position, in.visibility_range_dither); +#endif + +#ifdef FORWARD_DECAL + let forward_decal_info = get_forward_decal_info(in); + in.world_position = forward_decal_info.world_position; + in.uv = forward_decal_info.uv; +#endif + + // generate a PbrInput struct from the StandardMaterial bindings + var pbr_input = pbr_input_from_standard_material(in, is_front); + + // alpha discard + pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); + + // clustered decals + apply_decals(&pbr_input); + +#ifdef PREPASS_PIPELINE + // write the gbuffer, lighting pass id, and optionally normal and motion_vector textures + let out = deferred_output(in, pbr_input); +#else + // in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here. + // in deferred mode the lit color and these effects will be calculated in the deferred lighting shader + var out: FragmentOutput; + if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + out.color = apply_pbr_lighting(pbr_input); + } else { + out.color = pbr_input.material.base_color; + } + + // apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr) + // note this does not include fullscreen postprocessing effects like bloom. + out.color = main_pass_post_lighting_processing(pbr_input, out.color); +#endif + +#ifdef OIT_ENABLED + let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // The fragments will only be drawn during the oit resolve pass. + oit_draw(in.position, out.color); + discard; + } +#endif // OIT_ENABLED + +#ifdef FORWARD_DECAL + out.color.a = min(forward_decal_info.alpha, out.color.a); +#endif + + return out; +} diff --git a/vendor/bevy_pbr/src/render/pbr_ambient.wgsl b/vendor/bevy_pbr/src/render/pbr_ambient.wgsl new file mode 100644 index 00000000..7b174da3 --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_ambient.wgsl @@ -0,0 +1,29 @@ +#define_import_path bevy_pbr::ambient + +#import bevy_pbr::{ + lighting::{EnvBRDFApprox, F_AB}, + mesh_view_bindings::lights, +} + +// A precomputed `NdotV` is provided because it is computed regardless, +// but `world_normal` and the view vector `V` are provided separately for more advanced uses. +fn ambient_light( + world_position: vec4, + world_normal: vec3, + V: vec3, + NdotV: f32, + diffuse_color: vec3, + specular_color: vec3, + perceptual_roughness: f32, + occlusion: vec3, +) -> vec3 { + let diffuse_ambient = EnvBRDFApprox(diffuse_color, F_AB(1.0, NdotV)); + let specular_ambient = EnvBRDFApprox(specular_color, F_AB(perceptual_roughness, NdotV)); + + // No real world material has specular values under 0.02, so we use this range as a + // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. + // See: https://google.github.io/filament/Filament.html#specularocclusion + let specular_occlusion = saturate(dot(specular_color, vec3(50.0 * 0.33))); + + return (diffuse_ambient + specular_ambient * specular_occlusion) * lights.ambient_color.rgb * occlusion; +} diff --git a/vendor/bevy_pbr/src/render/pbr_bindings.wgsl b/vendor/bevy_pbr/src/render/pbr_bindings.wgsl new file mode 100644 index 00000000..6d21c81f --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_bindings.wgsl @@ -0,0 +1,89 @@ +#define_import_path bevy_pbr::pbr_bindings + +#import bevy_pbr::pbr_types::StandardMaterial + +#ifdef BINDLESS +struct StandardMaterialBindings { + material: u32, // 0 + base_color_texture: u32, // 1 + base_color_sampler: u32, // 2 + emissive_texture: u32, // 3 + emissive_sampler: u32, // 4 + metallic_roughness_texture: u32, // 5 + metallic_roughness_sampler: u32, // 6 + occlusion_texture: u32, // 7 + occlusion_sampler: u32, // 8 + normal_map_texture: u32, // 9 + normal_map_sampler: u32, // 10 + depth_map_texture: u32, // 11 + depth_map_sampler: u32, // 12 + anisotropy_texture: u32, // 13 + anisotropy_sampler: u32, // 14 + specular_transmission_texture: u32, // 15 + specular_transmission_sampler: u32, // 16 + thickness_texture: u32, // 17 + thickness_sampler: u32, // 18 + diffuse_transmission_texture: u32, // 19 + diffuse_transmission_sampler: u32, // 20 + clearcoat_texture: u32, // 21 + clearcoat_sampler: u32, // 22 + clearcoat_roughness_texture: u32, // 23 + clearcoat_roughness_sampler: u32, // 24 + clearcoat_normal_texture: u32, // 25 + clearcoat_normal_sampler: u32, // 26 + specular_texture: u32, // 27 + specular_sampler: u32, // 28 + specular_tint_texture: u32, // 29 + specular_tint_sampler: u32, // 30 +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material_indices: array; +@group(#{MATERIAL_BIND_GROUP}) @binding(10) var material_array: array; + +#else // BINDLESS + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: StandardMaterial; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_color_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var base_color_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(3) var emissive_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(4) var emissive_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(5) var metallic_roughness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(6) var metallic_roughness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(7) var occlusion_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(8) var occlusion_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(9) var normal_map_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(10) var normal_map_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(11) var depth_map_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(12) var depth_map_sampler: sampler; + +#ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(13) var anisotropy_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(14) var anisotropy_sampler: sampler; +#endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED + +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(15) var specular_transmission_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(16) var specular_transmission_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(17) var thickness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(18) var thickness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(19) var diffuse_transmission_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(20) var diffuse_transmission_sampler: sampler; +#endif // PBR_TRANSMISSION_TEXTURES_SUPPORTED + +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(21) var clearcoat_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(22) var clearcoat_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(23) var clearcoat_roughness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(24) var clearcoat_roughness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(25) var clearcoat_normal_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(26) var clearcoat_normal_sampler: sampler; +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(27) var specular_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(28) var specular_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(29) var specular_tint_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(30) var specular_tint_sampler: sampler; +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED + +#endif // BINDLESS diff --git a/vendor/bevy_pbr/src/render/pbr_fragment.wgsl b/vendor/bevy_pbr/src/render/pbr_fragment.wgsl new file mode 100644 index 00000000..a78abcbf --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_fragment.wgsl @@ -0,0 +1,844 @@ +#define_import_path bevy_pbr::pbr_fragment + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + pbr_functions, + pbr_functions::SampleBias, + pbr_bindings, + pbr_types, + prepass_utils, + lighting, + mesh_bindings::mesh, + mesh_view_bindings::view, + parallax_mapping::parallaxed_uv, + lightmap::lightmap, +} + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION +#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture +#import bevy_pbr::ssao_utils::ssao_multibounce +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else +#import bevy_pbr::forward_io::VertexOutput +#endif + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +// prepare a basic PbrInput from the vertex stage output, mesh binding and view binding +fn pbr_input_from_vertex_output( + in: VertexOutput, + is_front: bool, + double_sided: bool, +) -> pbr_types::PbrInput { + var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new(); + +#ifdef MESHLET_MESH_MATERIAL_PASS + pbr_input.flags = in.mesh_flags; +#else + pbr_input.flags = mesh[in.instance_index].flags; +#endif + + pbr_input.is_orthographic = view.clip_from_view[3].w == 1.0; + pbr_input.V = pbr_functions::calculate_view(in.world_position, pbr_input.is_orthographic); + pbr_input.frag_coord = in.position; + pbr_input.world_position = in.world_position; + +#ifdef VERTEX_COLORS + pbr_input.material.base_color = in.color; +#endif + + pbr_input.world_normal = pbr_functions::prepare_world_normal( + in.world_normal, + double_sided, + is_front, + ); + +#ifdef LOAD_PREPASS_NORMALS + pbr_input.N = prepass_utils::prepass_normal(in.position, 0u); +#else + pbr_input.N = normalize(pbr_input.world_normal); +#endif + + return pbr_input; +} + +// Prepare a full PbrInput by sampling all textures to resolve +// the material members +fn pbr_input_from_standard_material( + in: VertexOutput, + is_front: bool, +) -> pbr_types::PbrInput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let slot = in.material_bind_group_slot; +#else // MESHLET_MESH_MATERIAL_PASS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let base_color = pbr_bindings::material_array[material_indices[slot].material].base_color; + let deferred_lighting_pass_id = + pbr_bindings::material_array[material_indices[slot].material].deferred_lighting_pass_id; +#else // BINDLESS + let flags = pbr_bindings::material.flags; + let base_color = pbr_bindings::material.base_color; + let deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; +#endif + + let double_sided = (flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; + + var pbr_input: pbr_types::PbrInput = pbr_input_from_vertex_output(in, is_front, double_sided); + pbr_input.material.flags = flags; + pbr_input.material.base_color *= base_color; + pbr_input.material.deferred_lighting_pass_id = deferred_lighting_pass_id; + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef VERTEX_UVS + +#ifdef BINDLESS + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + +pbr_input.material.uv_transform = uv_transform; + +#ifdef VERTEX_UVS_A + var uv = (uv_transform * vec3(in.uv, 1.0)).xy; +#endif + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef VERTEX_UVS_B + var uv_b = (uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + var uv_b = uv; +#endif + +#ifdef VERTEX_TANGENTS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) { + let V = pbr_input.V; + let TBN = pbr_functions::calculate_tbn_mikktspace(in.world_normal, in.world_tangent); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + // Transform V from fragment to camera in world space to tangent space. + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); +#ifdef VERTEX_UVS_A + // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass + uv = parallaxed_uv( +#ifdef BINDLESS + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, +#else // BINDLESS + pbr_bindings::material.parallax_depth_scale, + pbr_bindings::material.max_parallax_layer_count, + pbr_bindings::material.max_relief_mapping_search_steps, +#endif // BINDLESS + uv, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + slot, + ); +#endif + +#ifdef VERTEX_UVS_B + // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass + uv_b = parallaxed_uv( +#ifdef BINDLESS + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, +#else // BINDLESS + pbr_bindings::material.parallax_depth_scale, + pbr_bindings::material.max_parallax_layer_count, + pbr_bindings::material.max_relief_mapping_search_steps, +#endif // BINDLESS + uv_b, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + slot, + ); +#else + uv_b = uv; +#endif + } +#endif // VERTEX_TANGENTS + + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { + pbr_input.material.base_color *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], +#else // BINDLESS + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_BASE_COLOR_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ); + +#ifdef ALPHA_TO_COVERAGE + // Sharpen alpha edges. + // + // https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f + let alpha_mode = flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { + +#ifdef BINDLESS + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + let alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + + pbr_input.material.base_color.a = (pbr_input.material.base_color.a - alpha_cutoff) / + max(fwidth(pbr_input.material.base_color.a), 0.0001) + 0.5; + } +#endif // ALPHA_TO_COVERAGE + + } +#endif // VERTEX_UVS + + pbr_input.material.flags = flags; + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { +#ifdef BINDLESS + pbr_input.material.ior = pbr_bindings::material_array[material_indices[slot].material].ior; + pbr_input.material.attenuation_color = + pbr_bindings::material_array[material_indices[slot].material].attenuation_color; + pbr_input.material.attenuation_distance = + pbr_bindings::material_array[material_indices[slot].material].attenuation_distance; + pbr_input.material.alpha_cutoff = + pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + pbr_input.material.ior = pbr_bindings::material.ior; + pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color; + pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance; + pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + + // reflectance +#ifdef BINDLESS + pbr_input.material.reflectance = + pbr_bindings::material_array[material_indices[slot].material].reflectance; +#else // BINDLESS + pbr_input.material.reflectance = pbr_bindings::material.reflectance; +#endif // BINDLESS + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +#ifdef VERTEX_UVS + + // Specular texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT) != 0u) { + let specular = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].specular_texture], + bindless_samplers_filtering[material_indices[slot].specular_sampler], +#else // BINDLESS + pbr_bindings::specular_texture, + pbr_bindings::specular_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).a; + // This 0.5 factor is from the `KHR_materials_specular` specification: + // + pbr_input.material.reflectance *= specular * 0.5; + } + + // Specular tint texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT) != 0u) { + let specular_tint = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].specular_tint_texture], + bindless_samplers_filtering[material_indices[slot].specular_tint_sampler], +#else // BINDLESS + pbr_bindings::specular_tint_texture, + pbr_bindings::specular_tint_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_TINT_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + pbr_input.material.reflectance *= specular_tint; + } + +#endif // VERTEX_UVS +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED + + // emissive +#ifdef BINDLESS + var emissive: vec4 = pbr_bindings::material_array[material_indices[slot].material].emissive; +#else // BINDLESS + var emissive: vec4 = pbr_bindings::material.emissive; +#endif // BINDLESS + +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { + emissive = vec4(emissive.rgb * +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].emissive_texture], + bindless_samplers_filtering[material_indices[slot].emissive_sampler], +#else // BINDLESS + pbr_bindings::emissive_texture, + pbr_bindings::emissive_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_EMISSIVE_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb, + emissive.a); + } +#endif + pbr_input.material.emissive = emissive; + + // metallic and perceptual roughness +#ifdef BINDLESS + var metallic: f32 = pbr_bindings::material_array[material_indices[slot].material].metallic; + var perceptual_roughness: f32 = pbr_bindings::material_array[material_indices[slot].material].perceptual_roughness; +#else // BINDLESS + var metallic: f32 = pbr_bindings::material.metallic; + var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness; +#endif // BINDLESS + +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { + let metallic_roughness = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].metallic_roughness_texture], + bindless_samplers_filtering[material_indices[slot].metallic_roughness_sampler], +#else // BINDLESS + pbr_bindings::metallic_roughness_texture, + pbr_bindings::metallic_roughness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ); + // Sampling from GLTF standard channels for now + metallic *= metallic_roughness.b; + perceptual_roughness *= metallic_roughness.g; + } +#endif + pbr_input.material.metallic = metallic; + pbr_input.material.perceptual_roughness = perceptual_roughness; + + // Clearcoat factor +#ifdef BINDLESS + pbr_input.material.clearcoat = + pbr_bindings::material_array[material_indices[slot].material].clearcoat; +#else // BINDLESS + pbr_input.material.clearcoat = pbr_bindings::material.clearcoat; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_texture, + pbr_bindings::clearcoat_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + + // Clearcoat roughness +#ifdef BINDLESS + pbr_input.material.clearcoat_perceptual_roughness = + pbr_bindings::material_array[material_indices[slot].material].clearcoat_perceptual_roughness; +#else // BINDLESS + pbr_input.material.clearcoat_perceptual_roughness = + pbr_bindings::material.clearcoat_perceptual_roughness; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat_perceptual_roughness *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_roughness_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_roughness_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_roughness_texture, + pbr_bindings::clearcoat_roughness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).g; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + +#ifdef BINDLESS + var specular_transmission: f32 = pbr_bindings::material_array[slot].specular_transmission; +#else // BINDLESS + var specular_transmission: f32 = pbr_bindings::material.specular_transmission; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { + specular_transmission *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[ + material_indices[slot].specular_transmission_texture + ], + bindless_samplers_filtering[ + material_indices[slot].specular_transmission_sampler + ], +#else // BINDLESS + pbr_bindings::specular_transmission_texture, + pbr_bindings::specular_transmission_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif +#endif + pbr_input.material.specular_transmission = specular_transmission; + +#ifdef BINDLESS + var thickness: f32 = pbr_bindings::material_array[material_indices[slot].material].thickness; +#else // BINDLESS + var thickness: f32 = pbr_bindings::material.thickness; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { + thickness *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].thickness_texture], + bindless_samplers_filtering[material_indices[slot].thickness_sampler], +#else // BINDLESS + pbr_bindings::thickness_texture, + pbr_bindings::thickness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_THICKNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).g; + } +#endif +#endif + // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) + // TODO: Meshlet support +#ifndef MESHLET_MESH_MATERIAL_PASS + thickness *= length( + (transpose(mesh[in.instance_index].world_from_local) * vec4(pbr_input.N, 0.0)).xyz + ); +#endif + pbr_input.material.thickness = thickness; + +#ifdef BINDLESS + var diffuse_transmission = + pbr_bindings::material_array[material_indices[slot].material].diffuse_transmission; +#else // BINDLESS + var diffuse_transmission = pbr_bindings::material.diffuse_transmission; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { + diffuse_transmission *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].diffuse_transmission_texture], + bindless_samplers_filtering[material_indices[slot].diffuse_transmission_sampler], +#else // BINDLESS + pbr_bindings::diffuse_transmission_texture, + pbr_bindings::diffuse_transmission_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).a; + } +#endif +#endif + pbr_input.material.diffuse_transmission = diffuse_transmission; + + var diffuse_occlusion: vec3 = vec3(1.0); + var specular_occlusion: f32 = 1.0; +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { + diffuse_occlusion *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].occlusion_texture], + bindless_samplers_filtering[material_indices[slot].occlusion_sampler], +#else // BINDLESS + pbr_bindings::occlusion_texture, + pbr_bindings::occlusion_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_OCCLUSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION + let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); + diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + let roughness = lighting::perceptualRoughnessToRoughness(pbr_input.material.perceptual_roughness); + specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); +#endif + pbr_input.diffuse_occlusion = diffuse_occlusion; + pbr_input.specular_occlusion = specular_occlusion; + + // N (normal vector) +#ifndef LOAD_PREPASS_NORMALS + + pbr_input.N = normalize(pbr_input.world_normal); + pbr_input.clearcoat_N = pbr_input.N; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS + + let TBN = pbr_functions::calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent); + +#ifdef STANDARD_MATERIAL_NORMAL_MAP + + let Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], +#else // BINDLESS + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + pbr_input.N = pbr_functions::apply_normal_mapping(flags, TBN, double_sided, is_front, Nt); + +#endif // STANDARD_MATERIAL_NORMAL_MAP + +#ifdef STANDARD_MATERIAL_CLEARCOAT + + // Note: `KHR_materials_clearcoat` specifies that, if there's no + // clearcoat normal map, we must set the normal to the mesh's normal, + // and not to the main layer's bumped normal. + +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + + let clearcoat_Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_normal_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_normal_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_normal_texture, + pbr_bindings::clearcoat_normal_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + pbr_input.clearcoat_N = pbr_functions::apply_normal_mapping( + flags, + TBN, + double_sided, + is_front, + clearcoat_Nt, + ); + +#endif // STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + +#endif // STANDARD_MATERIAL_CLEARCOAT + +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + + // Take anisotropy into account. + // + // This code comes from the `KHR_materials_anisotropy` spec: + // +#ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_ANISOTROPY + +#ifdef BINDLESS + var anisotropy_strength = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_strength; + var anisotropy_direction = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_rotation; +#else // BINDLESS + var anisotropy_strength = pbr_bindings::material.anisotropy_strength; + var anisotropy_direction = pbr_bindings::material.anisotropy_rotation; +#endif // BINDLESS + + // Adjust based on the anisotropy map if there is one. + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT) != 0u) { + let anisotropy_texel = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].anisotropy_texture], + bindless_samplers_filtering[material_indices[slot].anisotropy_sampler], +#else // BINDLESS + pbr_bindings::anisotropy_texture, + pbr_bindings::anisotropy_sampler, +#endif +#ifdef STANDARD_MATERIAL_ANISOTROPY_UV_B + uv_b, +#else // STANDARD_MATERIAL_ANISOTROPY_UV_B + uv, +#endif // STANDARD_MATERIAL_ANISOTROPY_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + let anisotropy_direction_from_texture = normalize(anisotropy_texel.rg * 2.0 - 1.0); + // Rotate by the anisotropy direction. + anisotropy_direction = + mat2x2(anisotropy_direction.xy, anisotropy_direction.yx * vec2(-1.0, 1.0)) * + anisotropy_direction_from_texture; + anisotropy_strength *= anisotropy_texel.b; + } + + pbr_input.anisotropy_strength = anisotropy_strength; + + let anisotropy_T = normalize(TBN * vec3(anisotropy_direction, 0.0)); + let anisotropy_B = normalize(cross(pbr_input.world_normal, anisotropy_T)); + pbr_input.anisotropy_T = anisotropy_T; + pbr_input.anisotropy_B = anisotropy_B; + +#endif // STANDARD_MATERIAL_ANISOTROPY +#endif // VERTEX_TANGENTS +#endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED + +#endif // LOAD_PREPASS_NORMALS + +// TODO: Meshlet support +#ifdef LIGHTMAP + +#ifdef BINDLESS + let lightmap_exposure = + pbr_bindings::material_array[material_indices[slot].material].lightmap_exposure; +#else // BINDLESS + let lightmap_exposure = pbr_bindings::material.lightmap_exposure; +#endif // BINDLESS + + pbr_input.lightmap_light = lightmap(in.uv_b, lightmap_exposure, in.instance_index); +#endif + } + + return pbr_input; +} diff --git a/vendor/bevy_pbr/src/render/pbr_functions.wgsl b/vendor/bevy_pbr/src/render/pbr_functions.wgsl new file mode 100644 index 00000000..33e550c0 --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_functions.wgsl @@ -0,0 +1,899 @@ +#define_import_path bevy_pbr::pbr_functions + +#import bevy_pbr::{ + pbr_types, + pbr_bindings, + mesh_view_bindings as view_bindings, + mesh_view_types, + lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, + transmission, + clustered_forward as clustering, + shadows, + ambient, + irradiance_volume, + view_transformations, + mesh_types::{MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT}, +} +#import bevy_render::maths::{E, powsafe} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else // PREPASS_PIPELINE +#import bevy_pbr::forward_io::VertexOutput +#endif // PREPASS_PIPELINE + +#ifdef ENVIRONMENT_MAP +#import bevy_pbr::environment_map +#endif + +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping::{tone_mapping, screen_space_dither} +#endif + + +// Biasing info needed to sample from a texture. How this is done depends on +// whether we're rendering meshlets or regular meshes. +struct SampleBias { +#ifdef MESHLET_MESH_MATERIAL_PASS + ddx_uv: vec2, + ddy_uv: vec2, +#else // MESHLET_MESH_MATERIAL_PASS + mip_bias: f32, +#endif // MESHLET_MESH_MATERIAL_PASS +} + +// This is the standard 4x4 ordered dithering pattern from [1]. +// +// We can't use `array, 4>` because they can't be indexed dynamically +// due to Naga limitations. So instead we pack into a single `vec4` and extract +// individual bytes. +// +// [1]: https://en.wikipedia.org/wiki/Ordered_dithering#Threshold_map +const DITHER_THRESHOLD_MAP: vec4 = vec4( + 0x0a020800, + 0x060e040c, + 0x09010b03, + 0x050d070f +); + +// Processes a visibility range dither value and discards the fragment if +// needed. +// +// Visibility ranges, also known as HLODs, are crossfades between different +// levels of detail. +// +// The `dither` value ranges from [-16, 16]. When zooming out, positive values +// are used for meshes that are in the process of disappearing, while negative +// values are used for meshes that are in the process of appearing. In other +// words, when the camera is moving backwards, the `dither` value counts up from +// -16 to 0 when the object is fading in, stays at 0 while the object is +// visible, and then counts up to 16 while the object is fading out. +// Distinguishing between negative and positive values allows the dither +// patterns for different LOD levels of a single mesh to mesh together properly. +#ifdef VISIBILITY_RANGE_DITHER +fn visibility_range_dither(frag_coord: vec4, dither: i32) { + // If `dither` is 0, the object is visible. + if (dither == 0) { + return; + } + + // If `dither` is less than -15 or greater than 15, the object is culled. + if (dither <= -16 || dither >= 16) { + discard; + } + + // Otherwise, check the dither pattern. + let coords = vec2(floor(frag_coord.xy)) % 4u; + let threshold = i32((DITHER_THRESHOLD_MAP[coords.y] >> (coords.x * 8)) & 0xff); + if ((dither >= 0 && dither + threshold >= 16) || (dither < 0 && 1 + dither + threshold <= 0)) { + discard; + } +} +#endif + +fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) -> vec4 { + var color = output_color; + let alpha_mode = material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + color.a = 1.0; + } + +#ifdef MAY_DISCARD + // NOTE: `MAY_DISCARD` is only defined in the alpha to coverage case if MSAA + // was off. This special situation causes alpha to coverage to fall back to + // alpha mask. + else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { + if color.a >= material.alpha_cutoff { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + color.a = 1.0; + } else { + // NOTE: output_color.a < in.material.alpha_cutoff should not be rendered + discard; + } + } +#endif + + return color; +} + +fn prepare_world_normal( + world_normal: vec3, + double_sided: bool, + is_front: bool, +) -> vec3 { + var output: vec3 = world_normal; +#ifndef VERTEX_TANGENTS +#ifndef STANDARD_MATERIAL_NORMAL_MAP + // NOTE: When NOT using normal-mapping, if looking at the back face of a double-sided + // material, the normal needs to be inverted. This is a branchless version of that. + output = (f32(!double_sided || is_front) * 2.0 - 1.0) * output; +#endif +#endif + return output; +} + +// Calculates the three TBN vectors according to [mikktspace]. Returns a matrix +// with T, B, N columns in that order. +// +// [mikktspace]: http://www.mikktspace.com/ +fn calculate_tbn_mikktspace(world_normal: vec3, world_tangent: vec4) -> mat3x3 { + // NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT + // be re-normalized in the fragment shader. This is primarily to match the way mikktspace + // bakes vertex tangents and normal maps so that this is the exact inverse. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code + // unless you really know what you are doing. + // http://www.mikktspace.com/ + var N: vec3 = world_normal; + + // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be + // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the + // vertex tangent! Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + var T: vec3 = world_tangent.xyz; + var B: vec3 = world_tangent.w * cross(N, T); + +#ifdef MESHLET_MESH_MATERIAL_PASS + // https://www.jeremyong.com/graphics/2023/12/16/surface-gradient-bump-mapping/#a-note-on-mikktspace-usage + let inverse_length_n = 1.0 / length(N); + T *= inverse_length_n; + B *= inverse_length_n; + N *= inverse_length_n; +#endif + + return mat3x3(T, B, N); +} + +fn apply_normal_mapping( + standard_material_flags: u32, + TBN: mat3x3, + double_sided: bool, + is_front: bool, + in_Nt: vec3, +) -> vec3 { + // Unpack the TBN vectors. + var T = TBN[0]; + var B = TBN[1]; + var N = TBN[2]; + + // Nt is the tangent-space normal. + var Nt = in_Nt; + if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { + // Only use the xy components and derive z for 2-component normal maps. + Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); + Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); + } else { + Nt = Nt * 2.0 - 1.0; + } + // Normal maps authored for DirectX require flipping the y component + if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u { + Nt.y = -Nt.y; + } + + if double_sided && !is_front { + Nt = -Nt; + } + + // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from + // the normal map texture in this way to be an EXACT inverse of how the normal map baker + // calculates the normal maps so there is no error introduced. Do not change this code + // unless you really know what you are doing. + // http://www.mikktspace.com/ + N = Nt.x * T + Nt.y * B + Nt.z * N; + + return normalize(N); +} + +#ifdef STANDARD_MATERIAL_ANISOTROPY + +// Modifies the normal to achieve a better approximate direction from the +// environment map when using anisotropy. +// +// This follows the suggested implementation in the `KHR_materials_anisotropy` specification: +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#image-based-lighting +fn bend_normal_for_anisotropy(lighting_input: ptr) { + // Unpack. + let N = (*lighting_input).layers[LAYER_BASE].N; + let roughness = (*lighting_input).layers[LAYER_BASE].roughness; + let V = (*lighting_input).V; + let anisotropy = (*lighting_input).anisotropy; + let Ba = (*lighting_input).Ba; + + var bent_normal = normalize(cross(cross(Ba, V), Ba)); + + // The `KHR_materials_anisotropy` spec states: + // + // > This heuristic can probably be improved upon + let a = pow(2.0, pow(2.0, 1.0 - anisotropy * (1.0 - roughness))); + bent_normal = normalize(mix(bent_normal, N, a)); + + // The `KHR_materials_anisotropy` spec states: + // + // > Mixing the reflection with the normal is more accurate both with and + // > without anisotropy and keeps rough objects from gathering light from + // > behind their tangent plane. + let R = normalize(mix(reflect(-V, bent_normal), bent_normal, roughness * roughness)); + + (*lighting_input).layers[LAYER_BASE].N = bent_normal; + (*lighting_input).layers[LAYER_BASE].R = R; +} + +#endif // STANDARD_MATERIAL_ANISOTROPY + +// NOTE: Correctly calculates the view vector depending on whether +// the projection is orthographic or perspective. +fn calculate_view( + world_position: vec4, + is_orthographic: bool, +) -> vec3 { + var V: vec3; + if is_orthographic { + // Orthographic view vector + V = normalize(vec3(view_bindings::view.clip_from_world[0].z, view_bindings::view.clip_from_world[1].z, view_bindings::view.clip_from_world[2].z)); + } else { + // Only valid for a perspective projection + V = normalize(view_bindings::view.world_position.xyz - world_position.xyz); + } + return V; +} + +// Diffuse strength is inversely related to metallicity, specular and diffuse transmission +fn calculate_diffuse_color( + base_color: vec3, + metallic: f32, + specular_transmission: f32, + diffuse_transmission: f32 +) -> vec3 { + return base_color * (1.0 - metallic) * (1.0 - specular_transmission) * + (1.0 - diffuse_transmission); +} + +// Remapping [0,1] reflectance to F0 +// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping +fn calculate_F0(base_color: vec3, metallic: f32, reflectance: vec3) -> vec3 { + return 0.16 * reflectance * reflectance * (1.0 - metallic) + base_color * metallic; +} + +#ifndef PREPASS_FRAGMENT +fn apply_pbr_lighting( + in: pbr_types::PbrInput, +) -> vec4 { + var output_color: vec4 = in.material.base_color; + + let emissive = in.material.emissive; + + // calculate non-linear roughness from linear perceptualRoughness + let metallic = in.material.metallic; + let perceptual_roughness = in.material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let ior = in.material.ior; + let thickness = in.material.thickness; + let reflectance = in.material.reflectance; + let diffuse_transmission = in.material.diffuse_transmission; + let specular_transmission = in.material.specular_transmission; + + let specular_transmissive_color = specular_transmission * in.material.base_color.rgb; + + let diffuse_occlusion = in.diffuse_occlusion; + let specular_occlusion = in.specular_occlusion; + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(in.N, in.V), 0.0001); + let R = reflect(-in.V, in.N); + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = in.material.clearcoat; + let clearcoat_perceptual_roughness = in.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = in.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, in.V), 0.0001); + let clearcoat_R = reflect(-in.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT + + let diffuse_color = calculate_diffuse_color( + output_color.rgb, + metallic, + specular_transmission, + diffuse_transmission + ); + + // Diffuse transmissive strength is inversely related to metallicity and specular transmission, but directly related to diffuse transmission + let diffuse_transmissive_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * diffuse_transmission; + + // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness + let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; + + let F0 = calculate_F0(output_color.rgb, metallic, reflectance); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); + + var direct_light: vec3 = vec3(0.0); + + // Transmitted Light (Specular and Diffuse) + var transmitted_light: vec3 = vec3(0.0); + + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = in.N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = in.world_position.xyz; + lighting_input.V = in.V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + lighting_input.anisotropy = in.anisotropy_strength; + lighting_input.Ta = in.anisotropy_T; + lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY + + // And do the same for transmissive if we need to. +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmissive_lighting_input: lighting::LightingInput; + transmissive_lighting_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].N = -in.N; + transmissive_lighting_input.layers[LAYER_BASE].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_BASE].perceptual_roughness = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].roughness = 1.0; + transmissive_lighting_input.P = diffuse_transmissive_lobe_world_position.xyz; + transmissive_lighting_input.V = -in.V; + transmissive_lighting_input.diffuse_color = diffuse_transmissive_color; + transmissive_lighting_input.F0_ = vec3(0.0); + transmissive_lighting_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + transmissive_lighting_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].N = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].roughness = 0.0; + transmissive_lighting_input.clearcoat_strength = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + transmissive_lighting_input.anisotropy = in.anisotropy_strength; + transmissive_lighting_input.Ta = in.anisotropy_T; + transmissive_lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + + let view_z = dot(vec4( + view_bindings::view.view_from_world[0].z, + view_bindings::view.view_from_world[1].z, + view_bindings::view.view_from_world[2].z, + view_bindings::view.view_from_world[3].z + ), in.world_position); + let cluster_index = clustering::fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); + var clusterable_object_index_ranges = + clustering::unpack_clusterable_object_index_ranges(cluster_index); + + // Point lights (direct) + for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; + i < clusterable_object_index_ranges.first_spot_light_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); + } + + let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse, true); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); + } + + let transmitted_light_contrib = + lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse, true); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + // Spot lights (direct) + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); + } + + let light_contrib = lighting::spot_light(light_id, &lighting_input, enable_diffuse); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); + } + + let transmitted_light_contrib = + lighting::spot_light(light_id, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + // directional lights (direct) + let n_directional_lights = view_bindings::lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + // check if this light should be skipped, which occurs if this light does not intersect with the view + // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters` + let light = &view_bindings::lights.directional_lights[i]; + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + ((*light).flags & + mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != + 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); + } + + var light_contrib = lighting::directional_light(i, &lighting_input, enable_diffuse); + +#ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES + light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z); +#endif + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_directional_shadow(i, diffuse_transmissive_lobe_world_position, -in.world_normal, view_z); + } + + let transmitted_light_contrib = + lighting::directional_light(i, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // perceptual_roughness = 1.0; + // NdotV = 1.0; + // F0 = vec3(0.0) + // diffuse_occlusion = vec3(1.0) + transmitted_light += ambient::ambient_light(diffuse_transmissive_lobe_world_position, -in.N, -in.V, 1.0, diffuse_transmissive_color, vec3(0.0), 1.0, vec3(1.0)); +#endif + + // Diffuse indirect lighting can come from a variety of sources. The + // priority goes like this: + // + // 1. Lightmap (highest) + // 2. Irradiance volume + // 3. Environment map (lowest) + // + // When we find a source of diffuse indirect lighting, we stop accumulating + // any more diffuse indirect light. This avoids double-counting if, for + // example, both lightmaps and irradiance volumes are present. + + var indirect_light = vec3(0.0f); + var found_diffuse_indirect = false; + +#ifdef LIGHTMAP + indirect_light += in.lightmap_light * diffuse_color; + found_diffuse_indirect = true; +#endif + +#ifdef IRRADIANCE_VOLUME + // Irradiance volume light (indirect) + if (!found_diffuse_indirect) { + let irradiance_volume_light = irradiance_volume::irradiance_volume_light( + in.world_position.xyz, + in.N, + &clusterable_object_index_ranges, + ); + indirect_light += irradiance_volume_light * diffuse_color * diffuse_occlusion; + found_diffuse_indirect = true; + } +#endif + + // Environment map light (indirect) +#ifdef ENVIRONMENT_MAP + // If screen space reflections are going to be used for this material, don't + // accumulate environment map light yet. The SSR shader will do it. +#ifdef SCREEN_SPACE_REFLECTIONS + let use_ssr = perceptual_roughness <= + view_bindings::ssr_settings.perceptual_roughness_threshold; +#else // SCREEN_SPACE_REFLECTIONS + let use_ssr = false; +#endif // SCREEN_SPACE_REFLECTIONS + + if (!use_ssr) { +#ifdef STANDARD_MATERIAL_ANISOTROPY + var bent_normal_lighting_input = lighting_input; + bend_normal_for_anisotropy(&bent_normal_lighting_input); + let environment_map_lighting_input = &bent_normal_lighting_input; +#else // STANDARD_MATERIAL_ANISOTROPY + let environment_map_lighting_input = &lighting_input; +#endif // STANDARD_MATERIAL_ANISOTROPY + + let environment_light = environment_map::environment_map_light( + environment_map_lighting_input, + &clusterable_object_index_ranges, + found_diffuse_indirect, + ); + + indirect_light += environment_light.diffuse * diffuse_occlusion + + environment_light.specular * specular_occlusion; + } +#endif // ENVIRONMENT_MAP + + // Ambient light (indirect) + // If we are lightmapped, disable the ambient contribution if requested. + // This is to avoid double-counting ambient light. (It might be part of the lightmap) +#ifdef LIGHTMAP + let enable_ambient = view_bindings::lights.ambient_light_affects_lightmapped_meshes != 0u; +#else // LIGHTMAP + let enable_ambient = true; +#endif // LIGHTMAP + if (enable_ambient) { + indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); + } + + // we'll use the specular component of the transmitted environment + // light in the call to `specular_transmissive_light()` below + var specular_transmitted_environment_light = vec3(0.0); + +#ifdef ENVIRONMENT_MAP + +#ifdef STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION + // NOTE: We use the diffuse transmissive color, inverted normal and view vectors, + // and the following simplified values for the transmitted environment light contribution + // approximation: + // + // diffuse_color = vec3(1.0) // later we use `diffuse_transmissive_color` and `specular_transmissive_color` + // NdotV = 1.0; + // R = T // see definition below + // F0 = vec3(1.0) + // diffuse_occlusion = 1.0 + // + // (This one is slightly different from the other light types above, because the environment + // map light returns both diffuse and specular components separately, and we want to use both) + + let T = -normalize( + in.V + // start with view vector at entry point + refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point + ); // normalize to find exit point view vector + + var transmissive_environment_light_input: lighting::LightingInput; + transmissive_environment_light_input.diffuse_color = vec3(1.0); + transmissive_environment_light_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_environment_light_input.P = in.world_position.xyz; + transmissive_environment_light_input.layers[LAYER_BASE].N = -in.N; + transmissive_environment_light_input.V = in.V; + transmissive_environment_light_input.layers[LAYER_BASE].R = T; + transmissive_environment_light_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + transmissive_environment_light_input.layers[LAYER_BASE].roughness = roughness; + transmissive_environment_light_input.F0_ = vec3(1.0); + transmissive_environment_light_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + // No clearcoat. + transmissive_environment_light_input.clearcoat_strength = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].N = in.N; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT + + let transmitted_environment_light = environment_map::environment_map_light( + &transmissive_environment_light_input, + &clusterable_object_index_ranges, + false, + ); + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION + specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; +#endif // STANDARD_MATERIAL_SPECULAR_TRANSMISSION + +#endif // STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION + +#endif // ENVIRONMENT_MAP + + var emissive_light = emissive.rgb * output_color.a; + + // "The clearcoat layer is on top of emission in the layering stack. + // Consequently, the emission is darkened by the Fresnel term." + // + // +#ifdef STANDARD_MATERIAL_CLEARCOAT + emissive_light = emissive_light * (0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0)); +#endif + + emissive_light = emissive_light * mix(1.0, view_bindings::view.exposure, emissive.a); + +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION + transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; + + if (in.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT) != 0u { + // We reuse the `atmospheric_fog()` function here, as it's fundamentally + // equivalent to the attenuation that takes place inside the material volume, + // and will allow us to eventually hook up subsurface scattering more easily + var attenuation_fog: mesh_view_types::Fog; + attenuation_fog.base_color.a = 1.0; + attenuation_fog.be = pow(1.0 - in.material.attenuation_color.rgb, vec3(E)) / in.material.attenuation_distance; + // TODO: Add the subsurface scattering factor below + // attenuation_fog.bi = /* ... */ + transmitted_light = bevy_pbr::fog::atmospheric_fog( + attenuation_fog, vec4(transmitted_light, 1.0), thickness, + vec3(0.0) // TODO: Pass in (pre-attenuated) scattered light contribution here + ).rgb; + } +#endif + + // Total light + output_color = vec4( + (view_bindings::view.exposure * (transmitted_light + direct_light + indirect_light)) + emissive_light, + output_color.a + ); + + output_color = clustering::cluster_debug_visualization( + output_color, + view_z, + in.is_orthographic, + clusterable_object_index_ranges, + cluster_index, + ); + + return output_color; +} +#endif // PREPASS_FRAGMENT + +#ifdef DISTANCE_FOG +fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4, fragment_world_position: vec3, view_world_position: vec3) -> vec4 { + let view_to_world = fragment_world_position.xyz - view_world_position.xyz; + + // `length()` is used here instead of just `view_to_world.z` since that produces more + // high quality results, especially for denser/smaller fogs. we get a "curved" + // fog shape that remains consistent with camera rotation, instead of a "linear" + // fog shape that looks a bit fake + let distance = length(view_to_world); + + // Calculate view_z for shadow cascade selection + let view_pos = view_transformations::position_world_to_view(fragment_world_position); + let view_z = view_pos.z; + + // Approximate surface normal using view direction for shadow sampling + let view_direction_normal = normalize(-view_to_world); + let fragment_world_position_vec4 = vec4(fragment_world_position, 1.0); + + var scattering = vec3(0.0); + if fog_params.directional_light_color.a > 0.0 { + let view_to_world_normalized = view_to_world / distance; + let n_directional_lights = view_bindings::lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + let light = view_bindings::lights.directional_lights[i]; + let scattering_contribution = pow( + max( + dot(view_to_world_normalized, light.direction_to_light), + 0.0 + ), + fog_params.directional_light_exponent + ) * light.color.rgb * view_bindings::view.exposure; + + // Sample shadow map to attenuate inscattering in shadowed areas + var shadow: f32 = 1.0; + if ((light.flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_directional_shadow(i, fragment_world_position_vec4, view_direction_normal, view_z); + } + scattering += scattering_contribution * shadow; + } + } + + if fog_params.mode == mesh_view_types::FOG_MODE_LINEAR { + return bevy_pbr::fog::linear_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL { + return bevy_pbr::fog::exponential_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL_SQUARED { + return bevy_pbr::fog::exponential_squared_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_ATMOSPHERIC { + return bevy_pbr::fog::atmospheric_fog(fog_params, input_color, distance, scattering); + } else { + return input_color; + } +} +#endif // DISTANCE_FOG + +#ifdef PREMULTIPLY_ALPHA +fn premultiply_alpha(standard_material_flags: u32, color: vec4) -> vec4 { +// `Blend`, `Premultiplied` and `Alpha` all share the same `BlendState`. Depending +// on the alpha mode, we premultiply the color channels by the alpha channel value, +// (and also optionally replace the alpha value with 0.0) so that the result produces +// the desired blend mode when sent to the blending operation. +#ifdef BLEND_PREMULTIPLIED_ALPHA + // For `BlendState::PREMULTIPLIED_ALPHA_BLENDING` the blend function is: + // + // result = 1 * src_color + (1 - src_alpha) * dst_color + let alpha_mode = standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD { + // Here, we premultiply `src_color` by `src_alpha`, and replace `src_alpha` with 0.0: + // + // src_color *= src_alpha + // src_alpha = 0.0 + // + // We end up with: + // + // result = 1 * (src_alpha * src_color) + (1 - 0) * dst_color + // result = src_alpha * src_color + 1 * dst_color + // + // Which is the blend operation for additive blending + return vec4(color.rgb * color.a, 0.0); + } else { + // Here, we don't do anything, so that we get premultiplied alpha blending. (As expected) + return color.rgba; + } +#endif +// `Multiply` uses its own `BlendState`, but we still need to premultiply here in the +// shader so that we get correct results as we tweak the alpha channel +#ifdef BLEND_MULTIPLY + // The blend function is: + // + // result = dst_color * src_color + (1 - src_alpha) * dst_color + // + // We premultiply `src_color` by `src_alpha`: + // + // src_color *= src_alpha + // + // We end up with: + // + // result = dst_color * (src_color * src_alpha) + (1 - src_alpha) * dst_color + // result = src_alpha * (src_color * dst_color) + (1 - src_alpha) * dst_color + // + // Which is the blend operation for multiplicative blending with arbitrary mixing + // controlled by the source alpha channel + return vec4(color.rgb * color.a, color.a); +#endif +} +#endif + +// fog, alpha premultiply +// for non-hdr cameras, tonemapping and debanding +fn main_pass_post_lighting_processing( + pbr_input: pbr_types::PbrInput, + input_color: vec4, +) -> vec4 { + var output_color = input_color; + +#ifdef DISTANCE_FOG + // fog + if ((pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) { + output_color = apply_fog(view_bindings::fog, output_color, pbr_input.world_position.xyz, view_bindings::view.world_position.xyz); + } +#endif // DISTANCE_FOG + +#ifdef TONEMAP_IN_SHADER + output_color = tone_mapping(output_color, view_bindings::view.color_grading); +#ifdef DEBAND_DITHER + var output_rgb = output_color.rgb; + output_rgb = powsafe(output_rgb, 1.0 / 2.2); + output_rgb += screen_space_dither(pbr_input.frag_coord.xy); + // This conversion back to linear space is required because our output texture format is + // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. + output_rgb = powsafe(output_rgb, 2.2); + output_color = vec4(output_rgb, output_color.a); +#endif +#endif +#ifdef PREMULTIPLY_ALPHA + output_color = premultiply_alpha(pbr_input.material.flags, output_color); +#endif + return output_color; +} diff --git a/vendor/bevy_pbr/src/render/pbr_lighting.wgsl b/vendor/bevy_pbr/src/render/pbr_lighting.wgsl new file mode 100644 index 00000000..c1d97489 --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_lighting.wgsl @@ -0,0 +1,935 @@ +#define_import_path bevy_pbr::lighting + +#import bevy_pbr::{ + mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + mesh_view_bindings as view_bindings, + atmosphere::functions::{calculate_visible_sun_ratio, clamp_to_surface}, + atmosphere::bruneton_functions::transmittance_lut_r_mu_to_uv, +} +#import bevy_render::maths::PI + +const LAYER_BASE: u32 = 0; +const LAYER_CLEARCOAT: u32 = 1; + +// From the Filament design doc +// https://google.github.io/filament/Filament.html#table_symbols +// Symbol Definition +// v View unit vector +// l Incident light unit vector +// n Surface normal unit vector +// h Half unit vector between l and v +// f BRDF +// f_d Diffuse component of a BRDF +// f_r Specular component of a BRDF +// α Roughness, remapped from using input perceptualRoughness +// σ Diffuse reflectance +// Ω Spherical domain +// f0 Reflectance at normal incidence +// f90 Reflectance at grazing angle +// χ+(a) Heaviside function (1 if a>0 and 0 otherwise) +// nior Index of refraction (IOR) of an interface +// ⟨n⋅l⟩ Dot product clamped to [0..1] +// ⟨a⟩ Saturated value (clamped to [0..1]) + +// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material +// and consists of two components, the diffuse component (f_d) and the specular component (f_r): +// f(v,l) = f_d(v,l) + f_r(v,l) +// +// The form of the microfacet model is the same for diffuse and specular +// f_r(v,l) = f_d(v,l) = 1 / { |n⋅v||n⋅l| } ∫_Ω D(m,α) G(v,l,m) f_m(v,l,m) (v⋅m) (l⋅m) dm +// +// In which: +// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets +// G models the visibility (or occlusion or shadow-masking) of the microfacets +// f_m is the microfacet BRDF and differs between specular and diffuse components +// +// The above integration needs to be approximated. + +// Input to a lighting function for a single layer (either the base layer or the +// clearcoat layer). +struct LayerLightingInput { + // The normal vector. + N: vec3, + // The reflected vector. + R: vec3, + // The normal vector ⋅ the view vector. + NdotV: f32, + + // The perceptual roughness of the layer. + perceptual_roughness: f32, + // The roughness of the layer. + roughness: f32, +} + +// Input to a lighting function (`point_light`, `spot_light`, +// `directional_light`). +struct LightingInput { +#ifdef STANDARD_MATERIAL_CLEARCOAT + layers: array, +#else // STANDARD_MATERIAL_CLEARCOAT + layers: array, +#endif // STANDARD_MATERIAL_CLEARCOAT + + // The world-space position. + P: vec3, + // The vector to the view. + V: vec3, + + // The diffuse color of the material. + diffuse_color: vec3, + + // Specular reflectance at the normal incidence angle. + // + // This should be read F₀, but due to Naga limitations we can't name it that. + F0_: vec3, + // Constants for the BRDF approximation. + // + // See `EnvBRDFApprox` in + // . + // What we call `F_ab` they call `AB`. + F_ab: vec2, + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // The strength of the clearcoat layer. + clearcoat_strength: f32, +#endif // STANDARD_MATERIAL_CLEARCOAT + +#ifdef STANDARD_MATERIAL_ANISOTROPY + // The anisotropy strength, reflecting the amount of increased roughness in + // the tangent direction. + anisotropy: f32, + // The tangent direction for anisotropy: i.e. the direction in which + // roughness increases. + Ta: vec3, + // The bitangent direction, which is the cross product of the normal with + // the tangent direction. + Ba: vec3, +#endif // STANDARD_MATERIAL_ANISOTROPY +} + +// Values derived from the `LightingInput` for both diffuse and specular lights. +struct DerivedLightingInput { + // The half-vector between L, the incident light vector, and V, the view + // vector. + H: vec3, + // The normal vector ⋅ the incident light vector. + NdotL: f32, + // The normal vector ⋅ the half-vector. + NdotH: f32, + // The incident light vector ⋅ the half-vector. + LdotH: f32, +} + +// distanceAttenuation is simply the square falloff of light intensity +// combined with a smooth attenuation at the edge of the light radius +// +// light radius is a non-physical construct for efficiency purposes, +// because otherwise every light affects every fragment in the scene +fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 { + let factor = distanceSquare * inverseRangeSquared; + let smoothFactor = saturate(1.0 - factor * factor); + let attenuation = smoothFactor * smoothFactor; + return attenuation * 1.0 / max(distanceSquare, 0.0001); +} + +// Normal distribution function (specular D) +// Based on https://google.github.io/filament/Filament.html#citation-walter07 + +// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α2−1) + 1)^2 } + +// Simple implementation, has precision problems when using fp16 instead of fp32 +// see https://google.github.io/filament/Filament.html#listing_speculardfp16 +fn D_GGX(roughness: f32, NdotH: f32) -> f32 { + let oneMinusNdotHSquared = 1.0 - NdotH * NdotH; + let a = NdotH * roughness; + let k = roughness / (oneMinusNdotHSquared + a * a); + let d = k * k * (1.0 / PI); + return d; +} + +// An approximation of the anisotropic GGX distribution function. +// +// 1 +// D(𝐡) = ─────────────────────────────────────────────────── +// παₜα_b((𝐡 ⋅ 𝐭)² / αₜ²) + (𝐡 ⋅ 𝐛)² / α_b² + (𝐡 ⋅ 𝐧)²)² +// +// * `T` = 𝐭 = the tangent direction = the direction of increased roughness. +// +// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness. +// +// * `at` = αₜ = the alpha-roughness in the tangent direction. +// +// * `ab` = α_b = the alpha-roughness in the bitangent direction. +// +// This is from the `KHR_materials_anisotropy` spec: +// +fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 { + let a2 = at * ab; + let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH); + let w2 = a2 / dot(f, f); + let d = a2 * w2 * w2 * (1.0 / PI); + return d; +} + +// Visibility function (Specular G) +// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) } +// such that f_r becomes +// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0) +// where +// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) } +// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv +fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 { + let a2 = roughness * roughness; + let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2); + let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2); + let v = 0.5 / (lambdaV + lambdaL); + return v; +} + +// The visibility function, anisotropic variant. +fn V_GGX_anisotropic( + at: f32, + ab: f32, + NdotL: f32, + NdotV: f32, + BdotV: f32, + TdotV: f32, + TdotL: f32, + BdotL: f32, +) -> f32 { + let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV)); + let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL)); + let v = 0.5 / (GGX_V + GGX_L); + return saturate(v); +} + +// Probability-density function that matches the bounded VNDF sampler +// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 2) +fn ggx_vndf_pdf(i: vec3, NdotH: f32, roughness: f32) -> f32 { + let ndf = D_GGX(roughness, NdotH); + + // Common terms + let ai = roughness * i.xy; + let len2 = dot(ai, ai); + let t = sqrt(len2 + i.z * i.z); + if i.z >= 0.0 { + let a = roughness; + let s = 1.0 + length(i.xy); + let a2 = a * a; + let s2 = s * s; + let k = (1.0 - a2) * s2 / (s2 + a2 * i.z * i.z); + return ndf / (2.0 * (k * i.z + t)); + } + + // Backfacing case + return ndf * (t - i.z) / (2.0 * len2); +} + +// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1) +fn sample_visible_ggx( + xi: vec2, + roughness: f32, + normal: vec3, + view: vec3, +) -> vec3 { + let n = normal; + let alpha = roughness; + + // Decompose view into components parallel/perpendicular to the normal + let wi_n = dot(view, n); + let wi_z = -n * wi_n; + let wi_xy = view + wi_z; + + // Warp view vector to the unit-roughness configuration + let wi_std = -normalize(alpha * wi_xy + wi_z); + + // Compute wi_std.z once for reuse + let wi_std_z = dot(wi_std, n); + + // Bounded VNDF sampling + // Compute the bound parameter k (Eq. 5) and the scaled z–limit b (Eq. 6) + let s = 1.0 + length(wi_xy); + let a = clamp(alpha, 0.0, 1.0); + let a2 = a * a; + let s2 = s * s; + let k = (1.0 - a2) * s2 / (s2 + a2 * wi_n * wi_n); + let b = select(wi_std_z, k * wi_std_z, wi_n > 0.0); + + // Sample a spherical cap in (-b, 1] + let z = 1.0 - xi.y * (1.0 + b); + let sin_theta = sqrt(max(0.0, 1.0 - z * z)); + let phi = 2.0 * PI * xi.x - PI; + let x = sin_theta * cos(phi); + let y = sin_theta * sin(phi); + let c_std = vec3f(x, y, z); + + // Reflect the sample so that the normal aligns with +Z + let up = vec3f(0.0, 0.0, 1.0); + let wr = n + up; + let c = dot(wr, c_std) * wr / wr.z - c_std; + + // Half-vector in the standard frame + let wm_std = c + wi_std; + let wm_std_z = n * dot(n, wm_std); + let wm_std_xy = wm_std_z - wm_std; + + // Unwarp back to original roughness and compute microfacet normal + let H = normalize(alpha * wm_std_xy + wm_std_z); + + // Reflect view to obtain the outgoing (light) direction + return reflect(-view, H); +} + +// Smith geometric shadowing function +fn G_Smith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 { + let k = roughness / 2.0; + let GGXL = NdotL / (NdotL * (1.0 - k) + k); + let GGXV = NdotV / (NdotV * (1.0 - k) + k); + return GGXL * GGXV; +} + +// A simpler, but nonphysical, alternative to Smith-GGX. We use this for +// clearcoat, per the Filament spec. +// +// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1 +fn V_Kelemen(LdotH: f32) -> f32 { + return 0.25 / (LdotH * LdotH); +} + +// Fresnel function +// see https://google.github.io/filament/Filament.html#citation-schlick94 +// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5 +fn F_Schlick_vec(f0: vec3, f90: f32, VdotH: f32) -> vec3 { + // not using mix to keep the vec3 and float versions identical + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); +} + +fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 { + // not using mix to keep the vec3 and float versions identical + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); +} + +fn fresnel(f0: vec3, LdotH: f32) -> vec3 { + // f_90 suitable for ambient occlusion + // see https://google.github.io/filament/Filament.html#lighting/occlusion + let f90 = saturate(dot(f0, vec3(50.0 * 0.33))); + return F_Schlick_vec(f0, f90, LdotH); +} + +// Given distribution, visibility, and Fresnel term, calculates the final +// specular light. +// +// Multiscattering approximation: +// +fn specular_multiscatter( + D: f32, + V: f32, + F: vec3, + F0: vec3, + F_ab: vec2, + specular_intensity: f32, +) -> vec3 { + var Fr = (specular_intensity * D * V) * F; + Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); + return Fr; +} + +// Specular BRDF +// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf + +// N, V, and L must all be normalized. +fn derive_lighting_input(N: vec3, V: vec3, L: vec3) -> DerivedLightingInput { + var input: DerivedLightingInput; + var H: vec3 = normalize(L + V); + input.H = H; + input.NdotL = saturate(dot(N, L)); + input.NdotH = saturate(dot(N, H)); + input.LdotH = saturate(dot(L, H)); + return input; +} + +// Returns L in the `xyz` components and the modified roughness in the `w` component. +fn compute_specular_layer_values_for_point_light( + input: ptr, + layer: u32, + V: vec3, + light_to_frag: vec3, + light_radius: f32, + distance: f32, +) -> vec4 { + // Unpack. + let R = (*input).layers[layer].R; + let a = (*input).layers[layer].roughness; + + // Representative Point Area Lights. + // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 + var LtFdotR = dot(light_to_frag, R); + + // HACK: the following line is an amendment to fix a discontinuity when a surface + // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318 + // + // This sentence in the reference is crux of the problem: "We approximate finding the point with the + // smallest angle to the reflection ray by finding the point with the smallest distance to the ray." + // This approximation turns out to be completely wrong for points inside or near the sphere. + // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it. + // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero. + // However, this is still far from physically accurate. Deriving an exact solution would help, + // but really we should adopt a superior solution to area lighting, such as: + // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al. + LtFdotR = max(0.0001, LtFdotR); + + let centerToRay = LtFdotR * R - light_to_frag; + let closestPoint = light_to_frag + centerToRay * saturate( + light_radius * inverseSqrt(dot(centerToRay, centerToRay))); + let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); + + // Karis 2013, page 14. The constant 2 (or 3) is hand tuned to fit reference. + // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + let a_prime = saturate(a + light_radius / (2.0 * distance)); + + let L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? + return vec4(L, a_prime); +} + +// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m +// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) } +fn specular( + input: ptr, + derived_input: ptr, + roughness: f32, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F0 = (*input).F0_; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let D = D_GGX(roughness, NdotH); + // Calculate visibility. + let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL); + // Calculate the Fresnel term. + let F = fresnel(F0, LdotH); + + // Calculate the specular light. + let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity); + return Fr; +} + +// Calculates the specular light for the clearcoat layer. Returns Fc, the +// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in +// the second channel. +// +// +fn specular_clearcoat( + input: ptr, + derived_input: ptr, + clearcoat_strength: f32, + roughness: f32, + specular_intensity: f32, +) -> vec2 { + // Unpack. + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let Dc = D_GGX(roughness, NdotH); + // Calculate visibility. + let Vc = V_Kelemen(LdotH); + // Calculate the Fresnel term. + let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength; + // Calculate the specular light. + let Frc = (specular_intensity * Dc * Vc) * Fc; + return vec2(Fc, Frc); +} + +#ifdef STANDARD_MATERIAL_ANISOTROPY + +fn specular_anisotropy( + input: ptr, + derived_input: ptr, + L: vec3, + roughness: f32, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let V = (*input).V; + let F0 = (*input).F0_; + let anisotropy = (*input).anisotropy; + let Ta = (*input).Ta; + let Ba = (*input).Ba; + let H = (*derived_input).H; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + let TdotL = dot(Ta, L); + let BdotL = dot(Ba, L); + let TdotH = dot(Ta, H); + let BdotH = dot(Ba, H); + let TdotV = dot(Ta, V); + let BdotV = dot(Ba, V); + + let ab = roughness * roughness; + let at = mix(ab, 1.0, anisotropy * anisotropy); + + let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH); + let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL); + let Fa = fresnel(F0, LdotH); + + // Calculate the specular light. + let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity); + return Fr; +} + +#endif // STANDARD_MATERIAL_ANISOTROPY + +// Diffuse BRDF +// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf +// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm +// +// simplest approximation +// float Fd_Lambert() { +// return 1.0 / PI; +// } +// +// vec3 Fd = diffuseColor * Fd_Lambert(); +// +// Disney approximation +// See https://google.github.io/filament/Filament.html#citation-burley12 +// minimal quality difference +fn Fd_Burley( + input: ptr, + derived_input: ptr, +) -> f32 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let NdotL = (*derived_input).NdotL; + let LdotH = (*derived_input).LdotH; + + let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH; + let lightScatter = F_Schlick(1.0, f90, NdotL); + let viewScatter = F_Schlick(1.0, f90, NdotV); + return lightScatter * viewScatter * (1.0 / PI); +} + +// Scale/bias approximation +// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile +// TODO: Use a LUT (more accurate) +fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2 { + let c0 = vec4(-1.0, -0.0275, -0.572, 0.022); + let c1 = vec4(1.0, 0.0425, 1.04, -0.04); + let r = perceptual_roughness * c0 + c1; + let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; + return vec2(-1.04, 1.04) * a004 + r.zw; +} + +fn EnvBRDFApprox(F0: vec3, F_ab: vec2) -> vec3 { + return F0 * F_ab.x + F_ab.y; +} + +fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { + // clamp perceptual roughness to prevent precision problems + // According to Filament design 0.089 is recommended for mobile + // Filament uses 0.045 for non-mobile + let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0); + return clampedPerceptualRoughness * clampedPerceptualRoughness; +} + +// this must align with CubemapLayout in decal/clustered.rs +const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0; +const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1; +const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2; +const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3; + +const X_PLUS: u32 = 0; +const X_MINUS: u32 = 1; +const Y_PLUS: u32 = 2; +const Y_MINUS: u32 = 3; +const Z_MINUS: u32 = 4; +const Z_PLUS: u32 = 5; + +fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { + let abs_direction = abs(direction); + let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z)); + + let face_index = select( + select(X_PLUS, X_MINUS, direction.x < 0.0), + select( + select(Y_PLUS, Y_MINUS, direction.y < 0.0), + select(Z_PLUS, Z_MINUS, direction.z < 0.0), + max_axis != abs_direction.y + ), + max_axis != abs_direction.x + ); + + var face_uv: vec2; + var divisor: f32; + var corner_uv: vec2 = vec2(0, 0); + var face_size: vec2; + + switch face_index { + case X_PLUS: { face_uv = vec2(direction.z, -direction.y); divisor = direction.x; } + case X_MINUS: { face_uv = vec2(-direction.z, -direction.y); divisor = -direction.x; } + case Y_PLUS: { face_uv = vec2(direction.x, -direction.z); divisor = direction.y; } + case Y_MINUS: { face_uv = vec2(direction.x, direction.z); divisor = -direction.y; } + case Z_PLUS: { face_uv = vec2(direction.x, direction.y); divisor = direction.z; } + case Z_MINUS: { face_uv = vec2(direction.x, -direction.y); divisor = -direction.z; } + default: {} + } + face_uv = (face_uv / divisor) * 0.5 + 0.5; + + switch cubemap_type { + case CUBEMAP_TYPE_CROSS_VERTICAL: { + face_size = vec2(1.0/3.0, 1.0/4.0); + corner_uv = vec2((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_CROSS_HORIZONTAL: { + face_size = vec2(1.0/4.0, 1.0/3.0); + corner_uv = vec2((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: { + face_size = vec2(1.0/6.0, 1.0); + corner_uv.x = face_index; + } + case CUBEMAP_TYPE_SEQUENCE_VERTICAL: { + face_size = vec2(1.0, 1.0/6.0); + corner_uv.y = face_index; + } + default: {} + } + + return (vec2(corner_uv) + face_uv) * face_size; +} + +// This is a modification to Karis 2013 for area lights to fix an issue where the specular +// reflection on smooth materials looks too rough and dim. We lerp between the base roughness +// and Karis2013 roughness with a lerp factor tuned by looking at reference renders. The goal +// is to preserve sharp specular highlights on smooth materials, without blowing out specular +// highlights on rough materials. +// +// The ideal solution is to switch to Linearly Transformed Cosines, which is more accurate in +// all cases, and a popular choice for realtime. +fn specular_fix_remap(a: f32) -> f32 { + let inv_a_sq = (1.0 - a) * (1.0 - a); + return 1.0 - inv_a_sq * inv_a_sq; +} + +fn point_light( + light_id: u32, + input: ptr, + enable_diffuse: bool, + enable_texture: bool, +) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let P = (*input).P; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + + let light = &view_bindings::clusterable_objects.data[light_id]; + let light_to_frag = (*light).position_radius.xyz - P; + let L = normalize(light_to_frag); + let distance_square = dot(light_to_frag, light_to_frag); + let distance = sqrt(distance_square); + let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + + // Base layer + + let a = (*input).layers[LAYER_BASE].roughness; + let specular_L_a_prime = compute_specular_layer_values_for_point_light( + input, + LAYER_BASE, + V, + light_to_frag, + (*light).position_radius.w, + distance, + ); + let L_spec = specular_L_a_prime.xyz; + let a_prime = specular_L_a_prime.w; + var specular_derived_input = derive_lighting_input(N, V, L_spec); + + let normalizationFactor = a / a_prime; + let specular_intensity = normalizationFactor * normalizationFactor; + + let brdf_roughness = mix(a, a_prime, specular_fix_remap(a)); + +#ifdef STANDARD_MATERIAL_ANISOTROPY + var specular_light = specular_anisotropy(input, &specular_derived_input, L, brdf_roughness, specular_intensity); +#else // STANDARD_MATERIAL_ANISOTROPY + var specular_light = specular(input, &specular_derived_input, brdf_roughness, specular_intensity); +#endif // STANDARD_MATERIAL_ANISOTROPY + + // Sphere area light visibility (solid-angle attenuation) + let light_radius = (*light).position_radius.w; + if light_radius > 0.0 { + let solid_angle = light_radius * light_radius / (distance * distance); + specular_light *= saturate(specular_derived_input.NdotL / max(specular_derived_input.NdotL + solid_angle, 1e-4)); + } + + // Clearcoat + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Unpack. + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + let clearcoat_a = (*input).layers[LAYER_CLEARCOAT].roughness; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + let clearcoat_specular_L_a_prime = compute_specular_layer_values_for_point_light( + input, + LAYER_CLEARCOAT, + V, + light_to_frag, + (*light).position_radius.w, + distance, + ); + let L_clearcoat_spec = clearcoat_specular_L_a_prime.xyz; + let clearcoat_a_prime = clearcoat_specular_L_a_prime.w; + var clearcoat_specular_derived_input = + derive_lighting_input(clearcoat_N, V, L_clearcoat_spec); + + // Calculate the specular light. + let clearcoat_normalizationFactor = clearcoat_a / clearcoat_a_prime; + + let clearcoat_specular_intensity = clearcoat_normalizationFactor * clearcoat_normalizationFactor; + + let clearcoat_brdf_roughness = mix(clearcoat_a, clearcoat_a_prime, specular_fix_remap(clearcoat_a)); + + let Fc_Frc = specular_clearcoat( + input, + &clearcoat_specular_derived_input, + clearcoat_strength, + clearcoat_brdf_roughness, + clearcoat_specular_intensity + ); + let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term. + var Frc = Fc_Frc.g; // Clearcoat light. + + // Sphere area light visibility (solid-angle attenuation) for clearcoat + if light_radius > 0.0 { + let solid_angle = light_radius * light_radius / (distance * distance); + Frc *= saturate(clearcoat_specular_derived_input.NdotL / max(clearcoat_specular_derived_input.NdotL + solid_angle, 1e-4)); + } +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Diffuse. + // Comes after specular since its N⋅L is used in the lighting equation. + var derived_input = derive_lighting_input(N, V, L); + var diffuse = vec3(0.0); + if (enable_diffuse) { + diffuse = diffuse_color * Fd_Burley(input, &derived_input); + } + + // See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation + // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩ + // where + // f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color + // Φ is luminous power in lumens + // our rangeAttenuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius + + // For a point light, luminous intensity, I, in lumens per steradian is given by: + // I = Φ / 4 π + // The derivation of this can be seen here: https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + + // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU + + var color_times_NdotL: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color_times_NdotL = (diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL * inv_Fc) * inv_Fc + Frc * clearcoat_specular_derived_input.NdotL; +#else // STANDARD_MATERIAL_CLEARCOAT + color_times_NdotL = diffuse * derived_input.NdotL + specular_light * specular_derived_input.NdotL; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if enable_texture && (*light).decal_index != 0xFFFFFFFFu { + let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz; + let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag; + let decal_uv = cubemap_uv(relative_position, cubemap_type); + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } +#endif + + return color_times_NdotL * (*light).color_inverse_square_range.rgb * + rangeAttenuation * texture_sample; +} + +fn spot_light( + light_id: u32, + input: ptr, + enable_diffuse: bool +) -> vec3 { + // reuse the point light calculations + let point_light = point_light(light_id, input, enable_diffuse, false); + + let light = &view_bindings::clusterable_objects.data[light_id]; + + // reconstruct spot dir from x/z and y-direction flag + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { + spot_dir.y = -spot_dir.y; + } + let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz; + + // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight + // spot_scale and spot_offset have been precomputed + // note we normalize here to get "l" from the filament listing. spot_dir is already normalized + let cd = dot(-spot_dir, normalize(light_to_frag)); + let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); + let spot_attenuation = attenuation * attenuation; + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + if local_position.z < 0.0 { + let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5; + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } + } +#endif + + return point_light * spot_attenuation * texture_sample; +} + +fn directional_light( + light_id: u32, + input: ptr, + enable_diffuse: bool +) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let roughness = (*input).layers[LAYER_BASE].roughness; + + let light = &view_bindings::lights.directional_lights[light_id]; + + let L = (*light).direction_to_light.xyz; + var derived_input = derive_lighting_input(N, V, L); + + var diffuse = vec3(0.0); + if (enable_diffuse) { + diffuse = diffuse_color * Fd_Burley(input, &derived_input); + } + +#ifdef STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular_anisotropy(input, &derived_input, L, roughness, 1.0); +#else // STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular(input, &derived_input, roughness, 1.0); +#endif // STANDARD_MATERIAL_ANISOTROPY + +#ifdef STANDARD_MATERIAL_CLEARCOAT + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + let clearcoat_roughness = (*input).layers[LAYER_CLEARCOAT].roughness; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L); + + let Fc_Frc = + specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, clearcoat_roughness, 1.0); + let inv_Fc = 1.0 - Fc_Frc.r; + let Frc = Fc_Frc.g; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL + + Frc * derived_clearcoat_input.NdotL; +#else // STANDARD_MATERIAL_CLEARCOAT + color = (diffuse + specular_light) * derived_input.NdotL; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5; + + // if tiled or within tile + if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u) + || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv) + { + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].base_color_texture_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv - floor(decal_uv), + 0.0 + ).r; + } else { + texture_sample = 0f; + } + } +#endif + +color *= (*light).color.rgb * texture_sample; + +#ifdef ATMOSPHERE + let P = (*input).P; + let atmosphere = view_bindings::atmosphere_data.atmosphere; + let O = vec3(0.0, atmosphere.bottom_radius, 0.0); + let P_scaled = P * vec3(view_bindings::atmosphere_data.settings.scene_units_to_m); + let P_as = P_scaled + O; + let P_clamped = clamp_to_surface(atmosphere, P_as); + let r = length(P_clamped); + let local_up = normalize(P_clamped); + let mu_light = dot(L, local_up); + + // Sample atmosphere + let transmittance = sample_transmittance_lut(r, mu_light); + let sun_visibility = calculate_visible_sun_ratio(atmosphere, r, mu_light, (*light).sun_disk_angular_size); + + // Apply atmospheric effects + color *= transmittance * sun_visibility; +#endif + + return color; +} + +#ifdef ATMOSPHERE +fn sample_transmittance_lut(r: f32, mu: f32) -> vec3 { + let uv = transmittance_lut_r_mu_to_uv(view_bindings::atmosphere_data.atmosphere, r, mu); + return textureSampleLevel( + view_bindings::atmosphere_transmittance_texture, + view_bindings::atmosphere_transmittance_sampler, uv, 0.0).rgb; +} +#endif // ATMOSPHERE diff --git a/vendor/bevy_pbr/src/render/pbr_prepass.wgsl b/vendor/bevy_pbr/src/render/pbr_prepass.wgsl new file mode 100644 index 00000000..68c36024 --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_prepass.wgsl @@ -0,0 +1,151 @@ +#import bevy_pbr::{ + pbr_prepass_functions, + pbr_bindings, + pbr_bindings::material, + pbr_types, + pbr_functions, + pbr_functions::SampleBias, + prepass_io, + mesh_bindings::mesh, + mesh_view_bindings::view, +} + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +#ifdef PREPASS_FRAGMENT +@fragment +fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else + in: prepass_io::VertexOutput, + @builtin(front_facing) is_front: bool, +#endif +) -> prepass_io::FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let in = resolve_vertex_output(frag_coord); + let is_front = true; +#else // MESHLET_MESH_MATERIAL_PASS + +#ifdef BINDLESS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let flags = pbr_bindings::material.flags; + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + + // If we're in the crossfade section of a visibility range, conditionally + // discard the fragment according to the visibility pattern. +#ifdef VISIBILITY_RANGE_DITHER + pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); +#endif // VISIBILITY_RANGE_DITHER + + pbr_prepass_functions::prepass_alpha_discard(in); +#endif // MESHLET_MESH_MATERIAL_PASS + + var out: prepass_io::FragmentOutput; + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.frag_depth = in.unclipped_depth; +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef NORMAL_PREPASS + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + let double_sided = (flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; + + let world_normal = pbr_functions::prepare_world_normal( + in.world_normal, + double_sided, + is_front, + ); + + var normal = world_normal; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_NORMAL_MAP + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + let uv = (uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + let uv = (uv_transform * vec3(in.uv, 1.0)).xy; +#endif + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + + let Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], +#else // BINDLESS + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, +#endif // BINDLESS + uv, +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + let TBN = pbr_functions::calculate_tbn_mikktspace(normal, in.world_tangent); + + normal = pbr_functions::apply_normal_mapping( + flags, + TBN, + double_sided, + is_front, + Nt, + ); + +#endif // STANDARD_MATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + + out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); + } else { + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); + } +#endif // NORMAL_PREPASS + +#ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else + out.motion_vector = pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position); +#endif +#endif + + return out; +} +#else +@fragment +fn fragment(in: prepass_io::VertexOutput) { + pbr_prepass_functions::prepass_alpha_discard(in); +} +#endif // PREPASS_FRAGMENT diff --git a/vendor/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/vendor/bevy_pbr/src/render/pbr_prepass_functions.wgsl new file mode 100644 index 00000000..d2d2c71e --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -0,0 +1,102 @@ +#define_import_path bevy_pbr::pbr_prepass_functions + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + prepass_io::VertexOutput, + prepass_bindings::previous_view_uniforms, + mesh_bindings::mesh, + mesh_view_bindings::view, + pbr_bindings, + pbr_types, +} + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +// Cutoff used for the premultiplied alpha modes BLEND, ADD, and ALPHA_TO_COVERAGE. +const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; + +// We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff +fn prepass_alpha_discard(in: VertexOutput) { + +#ifdef MAY_DISCARD +#ifdef BINDLESS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; + var output_color: vec4 = pbr_bindings::material_array[material_indices[slot].material].base_color; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; +#else // BINDLESS + var output_color: vec4 = pbr_bindings::material.base_color; + let flags = pbr_bindings::material.flags; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef STANDARD_MATERIAL_BASE_COLOR_UV_B + var uv = in.uv_b; +#else // STANDARD_MATERIAL_BASE_COLOR_UV_B + var uv = in.uv; +#endif // STANDARD_MATERIAL_BASE_COLOR_UV_B + +#ifdef BINDLESS + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + + uv = (uv_transform * vec3(uv, 1.0)).xy; + if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { + output_color = output_color * textureSampleBias( +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], +#else // BINDLESS + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, +#endif // BINDLESS + uv, + view.mip_bias + ); + } +#endif // VERTEX_UVS + + let alpha_mode = flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { +#ifdef BINDLESS + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + let alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + if output_color.a < alpha_cutoff { + discard; + } + } else if (alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE) { + if output_color.a < PREMULTIPLIED_ALPHA_CUTOFF { + discard; + } + } else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED { + if all(output_color < vec4(PREMULTIPLIED_ALPHA_CUTOFF)) { + discard; + } + } + +#endif // MAY_DISCARD +} + +#ifdef MOTION_VECTOR_PREPASS +fn calculate_motion_vector(world_position: vec4, previous_world_position: vec4) -> vec2 { + let clip_position_t = view.unjittered_clip_from_world * world_position; + let clip_position = clip_position_t.xy / clip_position_t.w; + let previous_clip_position_t = previous_view_uniforms.clip_from_world * previous_world_position; + let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; + // These motion vectors are used as offsets to UV positions and are stored + // in the range -1,1 to allow offsetting from the one corner to the + // diagonally-opposite corner in UV coordinates, in either direction. + // A difference between diagonally-opposite corners of clip space is in the + // range -2,2, so this needs to be scaled by 0.5. And the V direction goes + // down where clip space y goes up, so y needs to be flipped. + return (clip_position - previous_clip_position) * vec2(0.5, -0.5); +} +#endif // MOTION_VECTOR_PREPASS diff --git a/vendor/bevy_pbr/src/render/pbr_transmission.wgsl b/vendor/bevy_pbr/src/render/pbr_transmission.wgsl new file mode 100644 index 00000000..720a42bc --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_transmission.wgsl @@ -0,0 +1,192 @@ +#define_import_path bevy_pbr::transmission + +#import bevy_pbr::{ + lighting, + prepass_utils, + utils::interleaved_gradient_noise, + utils, + mesh_view_bindings as view_bindings, +}; + +#import bevy_render::maths::PI + +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping::approximate_inverse_tone_mapping +#endif + +fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { + // Calculate the ratio between refraction indexes. Assume air/vacuum for the space outside the mesh + let eta = 1.0 / ior; + + // Calculate incidence vector (opposite to view vector) and its dot product with the mesh normal + let I = -V; + let NdotI = dot(N, I); + + // Calculate refracted direction using Snell's law + let k = 1.0 - eta * eta * (1.0 - NdotI * NdotI); + let T = eta * I - (eta * NdotI + sqrt(k)) * N; + + // Calculate the exit position of the refracted ray, by propagating refracted direction through thickness + let exit_position = world_position.xyz + T * thickness; + + // Transform exit_position into clip space + let clip_exit_position = view_bindings::view.clip_from_world * vec4(exit_position, 1.0); + + // Scale / offset position so that coordinate is in right space for sampling transmissive background texture + let offset_position = (clip_exit_position.xy / clip_exit_position.w) * vec2(0.5, -0.5) + 0.5; + + // Fetch background color + var background_color: vec4; + if perceptual_roughness == 0.0 { + // If the material has zero roughness, we can use a faster approach without the blur + background_color = fetch_transmissive_background_non_rough(offset_position, frag_coord); + } else { + background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness); + } + + // Compensate for exposure, since the background color is coming from an already exposure-adjusted texture + background_color = vec4(background_color.rgb / view_bindings::view.exposure, background_color.a); + + // Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted) + let MinusNdotT = dot(-N, T); + + // Calculate 1.0 - fresnel factor (how much light is _NOT_ reflected, i.e. how much is transmitted) + let F = vec3(1.0) - lighting::fresnel(F0, MinusNdotT); + + // Calculate final color by applying fresnel multiplied specular transmissive color to a mix of background color and transmitted specular environment light + return F * specular_transmissive_color * mix(transmitted_environment_light_specular, background_color.rgb, background_color.a); +} + +fn fetch_transmissive_background_non_rough(offset_position: vec2, frag_coord: vec3) -> vec4 { + var background_color = textureSampleLevel( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + offset_position, + 0.0 + ); + +#ifdef DEPTH_PREPASS +#ifndef WEBGL2 + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + background_color.a = 0.0; + } +#endif +#endif + +#ifdef TONEMAP_IN_SHADER + background_color = approximate_inverse_tone_mapping(background_color, view_bindings::view.color_grading); +#endif + + return background_color; +} + +fn fetch_transmissive_background(offset_position: vec2, frag_coord: vec3, view_z: f32, perceptual_roughness: f32) -> vec4 { + // Calculate view aspect ratio, used to scale offset so that it's proportionate + let aspect = view_bindings::view.viewport.z / view_bindings::view.viewport.w; + + // Calculate how “blurry” the transmission should be. + // Blur is more or less eyeballed to look approximately “right”, since the “correct” + // approach would involve projecting many scattered rays and figuring out their individual + // exit positions. IRL, light rays can be scattered when entering/exiting a material (due to + // roughness) or inside the material (due to subsurface scattering). Here, we only consider + // the first scenario. + // + // Blur intensity is: + // - proportional to the square of `perceptual_roughness` + // - proportional to the inverse of view z + let blur_intensity = (perceptual_roughness * perceptual_roughness) / view_z; + +#ifdef SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS + let num_taps = #{SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS}; // Controlled by the `Camera3d::screen_space_specular_transmission_quality` property +#else + let num_taps = 8; // Fallback to 8 taps, if not specified +#endif + let num_spirals = i32(ceil(f32(num_taps) / 8.0)); +#ifdef TEMPORAL_JITTER + let random_angle = interleaved_gradient_noise(frag_coord.xy, view_bindings::globals.frame_count); +#else + let random_angle = interleaved_gradient_noise(frag_coord.xy, 0u); +#endif + // Pixel checkerboard pattern (helps make the interleaved gradient noise pattern less visible) + let pixel_checkboard = ( +#ifdef TEMPORAL_JITTER + // 0 or 1 on even/odd pixels, alternates every frame + (i32(frag_coord.x) + i32(frag_coord.y) + i32(view_bindings::globals.frame_count)) % 2 +#else + // 0 or 1 on even/odd pixels + (i32(frag_coord.x) + i32(frag_coord.y)) % 2 +#endif + ); + + var result = vec4(0.0); + for (var i: i32 = 0; i < num_taps; i = i + 1) { + let current_spiral = (i >> 3u); + let angle = (random_angle + f32(current_spiral) / f32(num_spirals)) * 2.0 * PI; + let m = vec2(sin(angle), cos(angle)); + let rotation_matrix = mat2x2( + m.y, -m.x, + m.x, m.y + ); + + // Get spiral offset + var spiral_offset: vec2; + switch i & 7 { + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) + // TODO: Figure out a more reasonable way of doing this, as WGSL + // seems to only allow constant indexes into constant arrays at the moment. + // The downstream shader compiler should be able to optimize this into a single + // constant when unrolling the for loop, but it's still not ideal. + case 0: { spiral_offset = utils::SPIRAL_OFFSET_0_; } // Note: We go even first and then odd, so that the lowest + case 1: { spiral_offset = utils::SPIRAL_OFFSET_2_; } // quality possible (which does 4 taps) still does a full spiral + case 2: { spiral_offset = utils::SPIRAL_OFFSET_4_; } // instead of just the first half of it + case 3: { spiral_offset = utils::SPIRAL_OFFSET_6_; } + case 4: { spiral_offset = utils::SPIRAL_OFFSET_1_; } + case 5: { spiral_offset = utils::SPIRAL_OFFSET_3_; } + case 6: { spiral_offset = utils::SPIRAL_OFFSET_5_; } + case 7: { spiral_offset = utils::SPIRAL_OFFSET_7_; } + default: {} + } + + // Make each consecutive spiral slightly smaller than the previous one + spiral_offset *= 1.0 - (0.5 * f32(current_spiral + 1) / f32(num_spirals)); + + // Rotate and correct for aspect ratio + let rotated_spiral_offset = (rotation_matrix * spiral_offset) * vec2(1.0, aspect); + + // Calculate final offset position, with blur and spiral offset + let modified_offset_position = offset_position + rotated_spiral_offset * blur_intensity * (1.0 - f32(pixel_checkboard) * 0.1); + + // Sample the view transmission texture at the offset position + noise offset, to get the background color + var sample = textureSampleLevel( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + modified_offset_position, + 0.0 + ); + +#ifdef DEPTH_PREPASS +#ifndef WEBGL2 + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(modified_offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + sample = vec4(0.0); + } +#endif +#endif + + // As blur intensity grows higher, gradually limit *very bright* color RGB values towards a + // maximum length of 1.0 to prevent stray “firefly” pixel artifacts. This can potentially make + // very strong emissive meshes appear much dimmer, but the artifacts are noticeable enough to + // warrant this treatment. + let normalized_rgb = normalize(sample.rgb); + result += vec4(min(sample.rgb, normalized_rgb / saturate(blur_intensity / 2.0)), sample.a); + } + + result /= f32(num_taps); + +#ifdef TONEMAP_IN_SHADER + result = approximate_inverse_tone_mapping(result, view_bindings::view.color_grading); +#endif + + return result; +} diff --git a/vendor/bevy_pbr/src/render/pbr_types.wgsl b/vendor/bevy_pbr/src/render/pbr_types.wgsl new file mode 100644 index 00000000..b8b51c57 --- /dev/null +++ b/vendor/bevy_pbr/src/render/pbr_types.wgsl @@ -0,0 +1,151 @@ +#define_import_path bevy_pbr::pbr_types + +// Since this is a hot path, try to keep the alignment and size of the struct members in mind. +// You can find the alignment and sizes at . +struct StandardMaterial { + base_color: vec4, + emissive: vec4, + attenuation_color: vec4, + uv_transform: mat3x3, + reflectance: vec3, + perceptual_roughness: f32, + metallic: f32, + diffuse_transmission: f32, + specular_transmission: f32, + thickness: f32, + ior: f32, + attenuation_distance: f32, + clearcoat: f32, + clearcoat_perceptual_roughness: f32, + anisotropy_strength: f32, + anisotropy_rotation: vec2, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + alpha_cutoff: f32, + parallax_depth_scale: f32, + max_parallax_layer_count: f32, + lightmap_exposure: f32, + max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + deferred_lighting_pass_id: u32, +}; + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: if these flags are updated or changed. Be sure to also update +// deferred_flags_from_mesh_material_flags and mesh_material_flags_from_deferred_flags +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u << 0u; +const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 1u << 1u; +const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 2u; +const STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 1u << 3u; +const STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 1u << 4u; +const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 1u << 5u; +const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 1u << 6u; +const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1u << 7u; +const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 8u; +const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 1u << 9u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 10u; +const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 1u << 11u; +const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 12u; +const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 1u << 13u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 1u << 14u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 15u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 1u << 16u; +const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 1u << 17u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT: u32 = 1u << 18u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 1u << 19u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 7u << 29u; // (0b111u << 29u) +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 3u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 4u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 5u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE: u32 = 6u << 29u; + + +// Creates a StandardMaterial with default values +fn standard_material_new() -> StandardMaterial { + var material: StandardMaterial; + + // NOTE: Keep in-sync with src/pbr_material.rs! + material.base_color = vec4(1.0, 1.0, 1.0, 1.0); + material.emissive = vec4(0.0, 0.0, 0.0, 1.0); + material.perceptual_roughness = 0.5; + material.metallic = 0.00; + material.reflectance = vec3(0.5); + material.diffuse_transmission = 0.0; + material.specular_transmission = 0.0; + material.thickness = 0.0; + material.ior = 1.5; + material.attenuation_distance = 1.0; + material.attenuation_color = vec4(1.0, 1.0, 1.0, 1.0); + material.clearcoat = 0.0; + material.clearcoat_perceptual_roughness = 0.0; + material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; + material.alpha_cutoff = 0.5; + material.parallax_depth_scale = 0.1; + material.max_parallax_layer_count = 16.0; + material.max_relief_mapping_search_steps = 5u; + material.deferred_lighting_pass_id = 1u; + // scale 1, translation 0, rotation 0 + material.uv_transform = mat3x3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + + return material; +} + +struct PbrInput { + material: StandardMaterial, + // Note: this gets monochromized upon deferred PbrInput reconstruction. + diffuse_occlusion: vec3, + // Note: this is 1.0 (entirely unoccluded) when SSAO and SSR are off. + specular_occlusion: f32, + frag_coord: vec4, + world_position: vec4, + // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow + // mapping + world_normal: vec3, + // Normalized normal-mapped world normal used for lighting + N: vec3, + // Normalized view vector in world space, pointing from the fragment world position toward the + // view world position + V: vec3, + lightmap_light: vec3, + clearcoat_N: vec3, + anisotropy_strength: f32, + // These two aren't specific to anisotropy, but we only fill them in if + // we're doing anisotropy, so they're prefixed with `anisotropy_`. + anisotropy_T: vec3, + anisotropy_B: vec3, + is_orthographic: bool, + flags: u32, +}; + +// Creates a PbrInput with default values +fn pbr_input_new() -> PbrInput { + var pbr_input: PbrInput; + + pbr_input.material = standard_material_new(); + pbr_input.diffuse_occlusion = vec3(1.0); + // If SSAO is enabled, then this gets overwritten with proper specular occlusion. If its not, then we get specular environment map unoccluded (we have no data with which to occlude it with). + pbr_input.specular_occlusion = 1.0; + + pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_normal = vec3(0.0, 0.0, 1.0); + + pbr_input.is_orthographic = false; + + pbr_input.N = vec3(0.0, 0.0, 1.0); + pbr_input.V = vec3(1.0, 0.0, 0.0); + + pbr_input.clearcoat_N = vec3(0.0); + pbr_input.anisotropy_T = vec3(0.0); + pbr_input.anisotropy_B = vec3(0.0); + + pbr_input.lightmap_light = vec3(0.0); + + pbr_input.flags = 0u; + + return pbr_input; +} diff --git a/vendor/bevy_pbr/src/render/reset_indirect_batch_sets.wgsl b/vendor/bevy_pbr/src/render/reset_indirect_batch_sets.wgsl new file mode 100644 index 00000000..93095947 --- /dev/null +++ b/vendor/bevy_pbr/src/render/reset_indirect_batch_sets.wgsl @@ -0,0 +1,25 @@ +// Resets the indirect draw counts to zero. +// +// This shader is needed because we reuse the same indirect batch set count +// buffer (i.e. the buffer that gets passed to `multi_draw_indirect_count` to +// determine how many objects to draw) between phases (early, late, and main). +// Before launching `build_indirect_params.wgsl`, we need to reinitialize the +// value to 0. + +#import bevy_pbr::mesh_preprocess_types::IndirectBatchSet + +@group(0) @binding(0) var indirect_batch_sets: array; + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index. If this thread doesn't correspond to any + // index, bail. + let instance_index = global_invocation_id.x; + if (instance_index >= arrayLength(&indirect_batch_sets)) { + return; + } + + // Reset the number of batch sets to 0. + atomicStore(&indirect_batch_sets[instance_index].indirect_parameters_count, 0u); +} diff --git a/vendor/bevy_pbr/src/render/rgb9e5.wgsl b/vendor/bevy_pbr/src/render/rgb9e5.wgsl new file mode 100644 index 00000000..c635c83d --- /dev/null +++ b/vendor/bevy_pbr/src/render/rgb9e5.wgsl @@ -0,0 +1,63 @@ +#define_import_path bevy_pbr::rgb9e5 + +const RGB9E5_EXPONENT_BITS = 5u; +const RGB9E5_MANTISSA_BITS = 9; +const RGB9E5_MANTISSA_BITSU = 9u; +const RGB9E5_EXP_BIAS = 15; +const RGB9E5_MAX_VALID_BIASED_EXP = 31u; + +//#define MAX_RGB9E5_EXP (RGB9E5_MAX_VALID_BIASED_EXP - RGB9E5_EXP_BIAS) +//#define RGB9E5_MANTISSA_VALUES (1< i32 { + let f = bitcast(x); + let biasedexponent = (f & 0x7F800000u) >> 23u; + return i32(biasedexponent) - 127; +} + +// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_shared_exponent.txt +fn vec3_to_rgb9e5_(rgb_in: vec3) -> u32 { + let rgb = clamp(rgb_in, vec3(0.0), vec3(MAX_RGB9E5_)); + + let maxrgb = max(rgb.r, max(rgb.g, rgb.b)); + var exp_shared = max(-RGB9E5_EXP_BIAS - 1, floor_log2_(maxrgb)) + 1 + RGB9E5_EXP_BIAS; + var denom = exp2(f32(exp_shared - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS)); + + let maxm = i32(floor(maxrgb / denom + 0.5)); + if (maxm == RGB9E5_MANTISSA_VALUES) { + denom *= 2.0; + exp_shared += 1; + } + + let n = vec3(floor(rgb / denom + 0.5)); + + return (u32(exp_shared) << 27u) | (n.b << 18u) | (n.g << 9u) | (n.r << 0u); +} + +// Builtin extractBits() is not working on WEBGL or DX12 +// DX12: HLSL: Unimplemented("write_expr_math ExtractBits") +fn extract_bits(value: u32, offset: u32, bits: u32) -> u32 { + let mask = (1u << bits) - 1u; + return (value >> offset) & mask; +} + +fn rgb9e5_to_vec3_(v: u32) -> vec3 { + let exponent = i32(extract_bits(v, 27u, RGB9E5_EXPONENT_BITS)) - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS; + let scale = exp2(f32(exponent)); + + return vec3( + f32(extract_bits(v, 0u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 9u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 18u, RGB9E5_MANTISSA_BITSU)) + ) * scale; +} diff --git a/vendor/bevy_pbr/src/render/shadow_sampling.wgsl b/vendor/bevy_pbr/src/render/shadow_sampling.wgsl new file mode 100644 index 00000000..2b35e570 --- /dev/null +++ b/vendor/bevy_pbr/src/render/shadow_sampling.wgsl @@ -0,0 +1,599 @@ +#define_import_path bevy_pbr::shadow_sampling + +#import bevy_pbr::{ + mesh_view_bindings as view_bindings, + utils::interleaved_gradient_noise, + utils, +} +#import bevy_render::maths::{orthonormalize, PI} + +// Do the lookup, using HW 2x2 PCF and comparison +fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i32) -> f32 { +#ifdef NO_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_comparison_sampler, + light_local, + depth, + ); +#else + return textureSampleCompareLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_comparison_sampler, + light_local, + array_index, + depth, + ); +#endif +} + +// Does a single sample of the blocker search, a part of the PCSS algorithm. +// This is the variant used for directional lights. +fn search_for_blockers_in_shadow_map_hardware( + light_local: vec2, + depth: f32, + array_index: i32, +) -> vec2 { +#ifdef WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#ifdef PCSS_SAMPLERS_AVAILABLE + +#ifdef NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + 0u, + ); +#else // NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + array_index, + 0u, + ); +#endif // NO_ARRAY_TEXTURES_SUPPORT + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); + +#else // PCSS_SAMPLERS_AVAILABLE + return vec2(0.0); +#endif // PCSS_SAMPLERS_AVAILABLE + +#endif // WEBGL2 +} + +// Numbers determined by trial and error that gave nice results. +const SPOT_SHADOW_TEXEL_SIZE: f32 = 0.0134277345; +const POINT_SHADOW_SCALE: f32 = 0.003; +const POINT_SHADOW_TEMPORAL_OFFSET_SCALE: f32 = 0.5; + +// These are the standard MSAA sample point positions from D3D. They were chosen +// to get a reasonable distribution that's not too regular. +// +// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels?redirectedfrom=MSDN +const D3D_SAMPLE_POINT_POSITIONS: array, 8> = array( + vec2( 0.125, -0.375), + vec2(-0.125, 0.375), + vec2( 0.625, 0.125), + vec2(-0.375, -0.625), + vec2(-0.625, 0.625), + vec2(-0.875, -0.125), + vec2( 0.375, 0.875), + vec2( 0.875, -0.875), +); + +// And these are the coefficients corresponding to the probability distribution +// function of a 2D Gaussian lobe with zero mean and the identity covariance +// matrix at those points. +const D3D_SAMPLE_POINT_COEFFS: array = array( + 0.157112, + 0.157112, + 0.138651, + 0.130251, + 0.114946, + 0.114946, + 0.107982, + 0.079001, +); + +// https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1 +fn sample_shadow_map_castano_thirteen(light_local: vec2, depth: f32, array_index: i32) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let inv_shadow_map_size = 1.0 / shadow_map_size; + + let uv = light_local * shadow_map_size; + var base_uv = floor(uv + 0.5); + let s = (uv.x + 0.5 - base_uv.x); + let t = (uv.y + 0.5 - base_uv.y); + base_uv -= 0.5; + base_uv *= inv_shadow_map_size; + + let uw0 = (4.0 - 3.0 * s); + let uw1 = 7.0; + let uw2 = (1.0 + 3.0 * s); + + let u0 = (3.0 - 2.0 * s) / uw0 - 2.0; + let u1 = (3.0 + s) / uw1; + let u2 = s / uw2 + 2.0; + + let vw0 = (4.0 - 3.0 * t); + let vw1 = 7.0; + let vw2 = (1.0 + 3.0 * t); + + let v0 = (3.0 - 2.0 * t) / vw0 - 2.0; + let v1 = (3.0 + t) / vw1; + let v2 = t / vw2 + 2.0; + + var sum = 0.0; + + sum += uw0 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u0, v0) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u1, v0) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u2, v0) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u0, v1) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u1, v1) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u2, v1) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u0, v2) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u1, v2) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u2, v2) * inv_shadow_map_size), depth, array_index); + + return sum * (1.0 / 144.0); +} + +fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { + return min2 + (value - min1) * (max2 - min2) / (max1 - min1); +} + +// Creates a random rotation matrix using interleaved gradient noise. +// +// See: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/ +fn random_rotation_matrix(scale: vec2, temporal: bool) -> mat2x2 { + let random_angle = 2.0 * PI * interleaved_gradient_noise( + scale, select(1u, view_bindings::globals.frame_count, temporal)); + let m = vec2(sin(random_angle), cos(random_angle)); + return mat2x2( + m.y, -m.x, + m.x, m.y + ); +} + +// Calculates the distance between spiral samples for the given texel size and +// penumbra size. This is used for the Jimenez '14 (i.e. temporal) variant of +// shadow sampling. +fn calculate_uv_offset_scale_jimenez_fourteen(texel_size: f32, blur_size: f32) -> vec2 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + + // Empirically chosen fudge factor to make PCF look better across different CSM cascades + let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size); + return f * blur_size / (texel_size * shadow_map_size); +} + +fn sample_shadow_map_jimenez_fourteen( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + blur_size: f32, + temporal: bool, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, temporal); + let uv_offset_scale = calculate_uv_offset_scale_jimenez_fourteen(texel_size, blur_size); + + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) + let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; + let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale; + let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale; + let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale; + let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale; + let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale; + let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale; + let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale; + + var sum = 0.0; + sum += sample_shadow_map_hardware(light_local + sample_offset0, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset2, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset3, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset4, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset5, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset6, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset7, depth, array_index); + return sum / 8.0; +} + +// Performs the blocker search portion of percentage-closer soft shadows (PCSS). +// This is the variation used for directional lights. +// +// We can't use Castano '13 here because that has a hard-wired fixed size, while +// the PCSS algorithm requires a search size that varies based on the size of +// the light. So we instead use the D3D sample point positions, spaced according +// to the search size, to provide a sample pattern in a similar manner to the +// cubemap sampling approach we use for PCF. +// +// `search_size` is the size of the search region in texels. +fn search_for_blockers_in_shadow_map( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + search_size: f32, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let uv_offset_scale = search_size / (texel_size * shadow_map_size); + + let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; + let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; + let offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; + let offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; + let offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; + let offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; + let offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; + let offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; + + var sum = vec2(0.0); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset0, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset1, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset2, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset3, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset4, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset5, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset6, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset7, depth, array_index); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + +fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN + return sample_shadow_map_castano_thirteen(light_local, depth, array_index); +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, 1.0, true); +#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 + return sample_shadow_map_hardware(light_local, depth, array_index); +#else + // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. + // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) + // This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined. + // Set to 0 to make it obvious that something is wrong. + return 0.0; +#endif +} + +// Samples the shadow map for a directional light when percentage-closer soft +// shadows are being used. +// +// We first search for a *blocker*, which is the average depth value of any +// shadow map samples that are adjacent to the sample we're considering. That +// allows us to determine the penumbra size; a larger gap between the blocker +// and the depth of this sample results in a wider penumbra. Finally, we sample +// the shadow map the same way we do in PCF, using that penumbra width. +// +// A good overview of the technique: +// +fn sample_shadow_map_pcss( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + light_size: f32, +) -> f32 { + // Determine the average Z value of the closest blocker. + let z_blocker = search_for_blockers_in_shadow_map( + light_local, depth, array_index, texel_size, light_size); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + + // FIXME: We can't use Castano '13 here because that has a hard-wired fixed + // size. So we instead use Jimenez '14 unconditionally. In the non-temporal + // variant this is unfortunately rather noisy. This may be improvable in the + // future by generating a mip chain of the shadow map and using that to + // provide better blurs. +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, true); +#else // SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, false); +#endif // SHADOW_FILTER_METHOD_TEMPORAL +} + +// NOTE: Due to the non-uniform control flow in `shadows::fetch_point_shadow`, +// we must use the Level variant of textureSampleCompare to avoid undefined +// behavior due to some of the fragments in a quad (2x2 fragments) being +// processed not being sampled, and this messing with mip-mapping functionality. +// The shadow maps have no mipmaps so Level just samples from LOD 0. +fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: u32) -> f32 { +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + depth + ); +#else + return textureSampleCompareLevel( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + i32(light_id), + depth + ); +#endif +} + +// Performs one sample of the blocker search. This variation of the blocker +// search function is for point and spot lights. +fn search_for_blockers_in_shadow_cubemap_hardware( + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { +#ifdef WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#ifdef PCSS_SAMPLERS_AVAILABLE + +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + ); +#else + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + i32(light_id), + ); +#endif + + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); + +#else // PCSS_SAMPLERS_AVAILABLE + return vec2(0.0); +#endif // PCSS_SAMPLERS_AVAILABLE + +#endif // WEBGL2 +} + +fn sample_shadow_cubemap_at_offset( + position: vec2, + coeff: f32, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> f32 { + return sample_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ) * coeff; +} + +// Computes the search position and performs one sample of the blocker search. +// This variation of the blocker search function is for point and spot lights. +// +// `x_basis`, `y_basis`, and `light_local` form an orthonormal basis over which +// the blocker search happens. +fn search_for_blockers_in_shadow_cubemap_at_offset( + position: vec2, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { + return search_for_blockers_in_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ); +} + +// This more or less does what Castano13 does, but in 3D space. Castano13 is +// essentially an optimized 2D Gaussian filter that takes advantage of the +// bilinear filtering hardware to reduce the number of samples needed. This +// trick doesn't apply to cubemaps, so we manually apply a Gaussian filter over +// the standard 8xMSAA pattern instead. +fn sample_shadow_cubemap_gaussian( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7], + basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +// This is a port of the Jimenez14 filter above to the 3D space. It jitters the +// points in the spiral pattern after first creating a 2D orthonormal basis +// along the principal light direction. +fn sample_shadow_cubemap_jittered( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, + temporal: bool, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + let rotation_matrix = random_rotation_matrix(vec2(1.0), temporal); + + let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset1 = rotation_matrix * utils::SPIRAL_OFFSET_1_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset2 = rotation_matrix * utils::SPIRAL_OFFSET_2_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset3 = rotation_matrix * utils::SPIRAL_OFFSET_3_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset4 = rotation_matrix * utils::SPIRAL_OFFSET_4_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset5 = rotation_matrix * utils::SPIRAL_OFFSET_5_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset6 = rotation_matrix * utils::SPIRAL_OFFSET_6_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset7 = rotation_matrix * utils::SPIRAL_OFFSET_7_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +fn sample_shadow_cubemap( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, +) -> f32 { +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN + return sample_shadow_cubemap_gaussian( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id); +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id, true); +#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 + return sample_shadow_cubemap_hardware(light_local, depth, light_id); +#else + // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. + // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) + // This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined. + // Set to 0 to make it obvious that something is wrong. + return 0.0; +#endif +} + +// Searches for PCSS blockers in a cubemap. This is the variant of the blocker +// search used for point and spot lights. +// +// This follows the logic in `sample_shadow_cubemap_gaussian`, but uses linear +// sampling instead of percentage-closer filtering. +// +// The `scale` parameter represents the size of the light. +fn search_for_blockers_in_shadow_cubemap( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + var sum: vec2 = vec2(0.0); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_id); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + +// Samples the shadow map for a point or spot light when percentage-closer soft +// shadows are being used. +// +// A good overview of the technique: +// +fn sample_shadow_cubemap_pcss( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, + light_size: f32, +) -> f32 { + let z_blocker = search_for_blockers_in_shadow_cubemap( + light_local, depth, light_size, distance_to_light, light_id); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, true); +#else + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, false); +#endif +} diff --git a/vendor/bevy_pbr/src/render/shadows.wgsl b/vendor/bevy_pbr/src/render/shadows.wgsl new file mode 100644 index 00000000..a3727b45 --- /dev/null +++ b/vendor/bevy_pbr/src/render/shadows.wgsl @@ -0,0 +1,231 @@ +#define_import_path bevy_pbr::shadows + +#import bevy_pbr::{ + mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + mesh_view_bindings as view_bindings, + shadow_sampling::{ + SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_cubemap_pcss, + sample_shadow_map, sample_shadow_map_pcss, + } +} + +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::{orthonormalize, PI_2} +} + +const flip_z: vec3 = vec3(1.0, 1.0, -1.0); + +fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { + let light = &view_bindings::clusterable_objects.data[light_id]; + + // because the shadow maps align with the axes and the frustum planes are at 45 degrees + // we can get the worldspace depth by taking the largest absolute axis + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + let surface_to_light_abs = abs(surface_to_light); + let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); + + // The normal bias here is already scaled by the texel size at 1 world unit from the light. + // The texel size increases proportionally with distance from the light so multiplying by + // distance to light scales the normal bias to the texel size at the fragment distance. + let normal_offset = (*light).shadow_normal_bias * distance_to_light * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz); + let offset_position = frag_position.xyz + normal_offset + depth_offset; + + // similar largest-absolute-axis trick as above, but now with the offset fragment position + let frag_ls = offset_position.xyz - (*light).position_radius.xyz ; + let abs_position_ls = abs(frag_ls); + let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); + + // NOTE: These simplifications come from multiplying: + // projection * vec4(0, 0, -major_axis_magnitude, 1.0) + // and keeping only the terms that have any impact on the depth. + // Projection-agnostic approach: + let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; + let depth = zw.x / zw.y; + + // If soft shadows are enabled, use the PCSS path. Cubemaps assume a + // left-handed coordinate space, so we have to flip the z-axis when + // sampling. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_cubemap_pcss( + frag_ls * flip_z, + distance_to_light, + depth, + light_id, + (*light).soft_shadow_size, + ); + } + + // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed + // coordinate space, so we have to flip the z-axis when sampling. + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id); +} + +fn fetch_spot_shadow( + light_id: u32, + frag_position: vec4, + surface_normal: vec3, + near_z: f32, +) -> f32 { + let light = &view_bindings::clusterable_objects.data[light_id]; + + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + + // construct the light view matrix + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + // reconstruct spot dir from x/z and y-direction flag + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { + spot_dir.y = -spot_dir.y; + } + + // view matrix z_axis is the reverse of transform.forward() + let fwd = -spot_dir; + let distance_to_light = dot(fwd, surface_to_light); + let offset_position = + -surface_to_light + + ((*light).shadow_depth_bias * normalize(surface_to_light)) + + (surface_normal.xyz * (*light).shadow_normal_bias) * distance_to_light; + + let light_inv_rot = orthonormalize(fwd); + + // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate + // the product of the transpose with a vector we can just post-multiply instead of pre-multiplying. + // this allows us to keep the matrix construction code identical between CPU and GPU. + let projected_position = offset_position * light_inv_rot; + + // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w) + // to get ndc coordinates + let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z); + let shadow_xy_ndc = projected_position.xy * f_div_minus_z; + // convert to uv coordinates + let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); + + let depth = near_z / -projected_position.z; + + // If soft shadows are enabled, use the PCSS path. + let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset; + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE, (*light).soft_shadow_size); + } + + return sample_shadow_map(shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE); +} + +fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { + let light = &view_bindings::lights.directional_lights[light_id]; + + for (var i: u32 = 0u; i < (*light).num_cascades; i = i + 1u) { + if (-view_z < (*light).cascades[i].far_bound) { + return i; + } + } + return (*light).num_cascades; +} + +// Converts from world space to the uv position in the light's shadow map. +// +// The depth is stored in the return value's z coordinate. If the return value's +// w coordinate is 0.0, then we landed outside the shadow map entirely. +fn world_to_directional_light_local( + light_id: u32, + cascade_index: u32, + offset_position: vec4 +) -> vec4 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + let offset_position_clip = (*cascade).clip_from_world * offset_position; + if (offset_position_clip.w <= 0.0) { + return vec4(0.0); + } + let offset_position_ndc = offset_position_clip.xyz / offset_position_clip.w; + // No shadow outside the orthographic projection volume + if (any(offset_position_ndc.xy < vec2(-1.0)) || offset_position_ndc.z < 0.0 + || any(offset_position_ndc > vec3(1.0))) { + return vec4(0.0); + } + + // compute texture coordinates for shadow lookup, compensating for the Y-flip difference + // between the NDC and texture coordinates + let flip_correction = vec2(0.5, -0.5); + let light_local = offset_position_ndc.xy * flip_correction + vec2(0.5, 0.5); + + let depth = offset_position_ndc.z; + + return vec4(light_local, depth, 1.0); +} + +fn sample_directional_cascade( + light_id: u32, + cascade_index: u32, + frag_position: vec4, + surface_normal: vec3, +) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + // The normal bias is scaled to the texel size. + let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); + + let light_local = world_to_directional_light_local(light_id, cascade_index, offset_position); + if (light_local.w == 0.0) { + return 1.0; + } + + let array_index = i32((*light).depth_texture_base_index + cascade_index); + let texel_size = (*cascade).texel_size; + + // If soft shadows are enabled, use the PCSS path. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + light_local.xy, light_local.z, array_index, texel_size, (*light).soft_shadow_size); + } + + return sample_shadow_map(light_local.xy, light_local.z, array_index, texel_size); +} + +fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade_index = get_cascade_index(light_id, view_z); + + if (cascade_index >= (*light).num_cascades) { + return 1.0; + } + + var shadow = sample_directional_cascade(light_id, cascade_index, frag_position, surface_normal); + + // Blend with the next cascade, if there is one. + let next_cascade_index = cascade_index + 1u; + if (next_cascade_index < (*light).num_cascades) { + let this_far_bound = (*light).cascades[cascade_index].far_bound; + let next_near_bound = (1.0 - (*light).cascades_overlap_proportion) * this_far_bound; + if (-view_z >= next_near_bound) { + let next_shadow = sample_directional_cascade(light_id, next_cascade_index, frag_position, surface_normal); + shadow = mix(shadow, next_shadow, (-view_z - next_near_bound) / (this_far_bound - next_near_bound)); + } + } + return shadow; +} + +fn cascade_debug_visualization( + output_color: vec3, + light_id: u32, + view_z: f32, +) -> vec3 { + let overlay_alpha = 0.95; + let cascade_index = get_cascade_index(light_id, view_z); + let cascade_color_hsv = vec3( + f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u) * PI_2, + 1.0, + 0.5 + ); + let cascade_color = hsv_to_rgb(cascade_color_hsv); + return vec3( + (1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color + ); +} diff --git a/vendor/bevy_pbr/src/render/skin.rs b/vendor/bevy_pbr/src/render/skin.rs new file mode 100644 index 00000000..75c42431 --- /dev/null +++ b/vendor/bevy_pbr/src/render/skin.rs @@ -0,0 +1,623 @@ +use core::mem::{self, size_of}; +use std::sync::OnceLock; + +use bevy_asset::{prelude::AssetChanged, Assets}; +use bevy_camera::visibility::ViewVisibility; +use bevy_ecs::prelude::*; +use bevy_math::Mat4; +use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}; +use bevy_platform::collections::hash_map::Entry; +use bevy_render::render_resource::{Buffer, BufferDescriptor}; +use bevy_render::settings::WgpuLimits; +use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}; +use bevy_render::{ + batching::NoAutomaticBatching, + render_resource::BufferUsages, + renderer::{RenderDevice, RenderQueue}, + Extract, +}; +use bevy_transform::prelude::GlobalTransform; +use offset_allocator::{Allocation, Allocator}; +use smallvec::SmallVec; +use tracing::error; + +/// Maximum number of joints supported for skinned meshes. +/// +/// It is used to allocate buffers. +/// The correctness of the value depends on the GPU/platform. +/// The current value is chosen because it is guaranteed to work everywhere. +/// To allow for bigger values, a check must be made for the limits +/// of the GPU at runtime, which would mean not using consts anymore. +pub const MAX_JOINTS: usize = 256; + +/// The total number of joints we support. +/// +/// This is 256 GiB worth of joint matrices, which we will never hit under any +/// reasonable circumstances. +const MAX_TOTAL_JOINTS: u32 = 1024 * 1024 * 1024; + +/// The number of joints that we allocate at a time. +/// +/// Some hardware requires that uniforms be allocated on 256-byte boundaries, so +/// we need to allocate 4 64-byte matrices at a time to satisfy alignment +/// requirements. +const JOINTS_PER_ALLOCATION_UNIT: u32 = (256 / size_of::()) as u32; + +/// The maximum ratio of the number of entities whose transforms changed to the +/// total number of joints before we re-extract all joints. +/// +/// We use this as a heuristic to decide whether it's worth switching over to +/// fine-grained detection to determine which skins need extraction. If the +/// number of changed entities is over this threshold, we skip change detection +/// and simply re-extract the transforms of all joints. +const JOINT_EXTRACTION_THRESHOLD_FACTOR: f64 = 0.25; + +/// The location of the first joint matrix in the skin uniform buffer. +#[derive(Clone, Copy)] +pub struct SkinByteOffset { + /// The byte offset of the first joint matrix. + pub byte_offset: u32, +} + +impl SkinByteOffset { + /// Index to be in address space based on the size of a skin uniform. + const fn from_index(index: usize) -> Self { + SkinByteOffset { + byte_offset: (index * size_of::()) as u32, + } + } + + /// Returns this skin index in elements (not bytes). + /// + /// Each element is a 4x4 matrix. + pub fn index(&self) -> u32 { + self.byte_offset / size_of::() as u32 + } +} + +/// The GPU buffers containing joint matrices for all skinned meshes. +/// +/// This is double-buffered: we store the joint matrices of each mesh for the +/// previous frame in addition to those of each mesh for the current frame. This +/// is for motion vector calculation. Every frame, we swap buffers and overwrite +/// the joint matrix buffer from two frames ago with the data for the current +/// frame. +/// +/// Notes on implementation: see comment on top of the `extract_skins` system. +#[derive(Resource)] +pub struct SkinUniforms { + /// The CPU-side buffer that stores the joint matrices for skinned meshes in + /// the current frame. + pub current_staging_buffer: Vec, + /// The GPU-side buffer that stores the joint matrices for skinned meshes in + /// the current frame. + pub current_buffer: Buffer, + /// The GPU-side buffer that stores the joint matrices for skinned meshes in + /// the previous frame. + pub prev_buffer: Buffer, + /// The offset allocator that manages the placement of the joints within the + /// [`Self::current_buffer`]. + allocator: Allocator, + /// Allocation information that we keep about each skin. + skin_uniform_info: MainEntityHashMap, + /// Maps each joint entity to the skins it's associated with. + /// + /// We use this in conjunction with change detection to only update the + /// skins that need updating each frame. + /// + /// Note that conceptually this is a hash map of sets, but we use a + /// [`SmallVec`] to avoid allocations for the vast majority of the cases in + /// which each bone belongs to exactly one skin. + joint_to_skins: MainEntityHashMap>, + /// The total number of joints in the scene. + /// + /// We use this as part of our heuristic to decide whether to use + /// fine-grained change detection. + total_joints: usize, +} + +impl FromWorld for SkinUniforms { + fn from_world(world: &mut World) -> Self { + let device = world.resource::(); + let buffer_usages = (if skins_use_uniform_buffers(&device.limits()) { + BufferUsages::UNIFORM + } else { + BufferUsages::STORAGE + }) | BufferUsages::COPY_DST; + + // Create the current and previous buffer with the minimum sizes. + // + // These will be swapped every frame. + let current_buffer = device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + size: MAX_JOINTS as u64 * size_of::() as u64, + usage: buffer_usages, + mapped_at_creation: false, + }); + let prev_buffer = device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + size: MAX_JOINTS as u64 * size_of::() as u64, + usage: buffer_usages, + mapped_at_creation: false, + }); + + Self { + current_staging_buffer: vec![], + current_buffer, + prev_buffer, + allocator: Allocator::new(MAX_TOTAL_JOINTS), + skin_uniform_info: MainEntityHashMap::default(), + joint_to_skins: MainEntityHashMap::default(), + total_joints: 0, + } + } +} + +impl SkinUniforms { + /// Returns the current offset in joints of the skin in the buffer. + pub fn skin_index(&self, skin: MainEntity) -> Option { + self.skin_uniform_info + .get(&skin) + .map(SkinUniformInfo::offset) + } + + /// Returns the current offset in bytes of the skin in the buffer. + pub fn skin_byte_offset(&self, skin: MainEntity) -> Option { + self.skin_uniform_info.get(&skin).map(|skin_uniform_info| { + SkinByteOffset::from_index(skin_uniform_info.offset() as usize) + }) + } + + /// Returns an iterator over all skins in the scene. + pub fn all_skins(&self) -> impl Iterator { + self.skin_uniform_info.keys() + } +} + +/// Allocation information about each skin. +struct SkinUniformInfo { + /// The allocation of the joints within the [`SkinUniforms::current_buffer`]. + allocation: Allocation, + /// The entities that comprise the joints. + joints: Vec, +} + +impl SkinUniformInfo { + /// The offset in joints within the [`SkinUniforms::current_staging_buffer`]. + fn offset(&self) -> u32 { + self.allocation.offset * JOINTS_PER_ALLOCATION_UNIT + } +} + +/// Returns true if skinning must use uniforms (and dynamic offsets) because +/// storage buffers aren't supported on the current platform. +pub fn skins_use_uniform_buffers(limits: &WgpuLimits) -> bool { + static SKINS_USE_UNIFORM_BUFFERS: OnceLock = OnceLock::new(); + *SKINS_USE_UNIFORM_BUFFERS.get_or_init(|| limits.max_storage_buffers_per_shader_stage == 0) +} + +/// Uploads the buffers containing the joints to the GPU. +pub fn prepare_skins( + render_device: Res, + render_queue: Res, + uniform: ResMut, +) { + let uniform = uniform.into_inner(); + + if uniform.current_staging_buffer.is_empty() { + return; + } + + // Swap current and previous buffers. + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + + // Resize the buffers if necessary. Include extra space equal to `MAX_JOINTS` + // because we need to be able to bind a full uniform buffer's worth of data + // if skins use uniform buffers on this platform. + let needed_size = (uniform.current_staging_buffer.len() as u64 + MAX_JOINTS as u64) + * size_of::() as u64; + if uniform.current_buffer.size() < needed_size { + let mut new_size = uniform.current_buffer.size(); + while new_size < needed_size { + // 1.5× growth factor. + new_size = (new_size + new_size / 2).next_multiple_of(4); + } + + // Create the new buffers. + let buffer_usages = if skins_use_uniform_buffers(&render_device.limits()) { + BufferUsages::UNIFORM + } else { + BufferUsages::STORAGE + } | BufferUsages::COPY_DST; + uniform.current_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + usage: buffer_usages, + size: new_size, + mapped_at_creation: false, + }); + uniform.prev_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + usage: buffer_usages, + size: new_size, + mapped_at_creation: false, + }); + + // We've created a new `prev_buffer` but we don't have the previous joint + // data needed to fill it out correctly. Use the current joint data + // instead. + // + // TODO: This is a bug - will cause motion blur to ignore joint movement + // for one frame. + render_queue.write_buffer( + &uniform.prev_buffer, + 0, + bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]), + ); + } + + // Write the data from `uniform.current_staging_buffer` into + // `uniform.current_buffer`. + render_queue.write_buffer( + &uniform.current_buffer, + 0, + bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]), + ); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. +} + +// Notes on implementation: +// We define the uniform binding as an array, N> in the shader, +// where N is the maximum number of Mat4s we can fit in the uniform binding, +// which may be as little as 16kB or 64kB. But, we may not need all N. +// We may only need, for example, 10. +// +// If we used uniform buffers ‘normally’ then we would have to write a full +// binding of data for each dynamic offset binding, which is wasteful, makes +// the buffer much larger than it needs to be, and uses more memory bandwidth +// to transfer the data, which then costs frame time So @superdump came up +// with this design: just bind data at the specified offset and interpret +// the data at that offset as an array regardless of what is there. +// +// So instead of writing N Mat4s when you only need 10, you write 10, and +// then pad up to the next dynamic offset alignment. Then write the next. +// And for the last dynamic offset binding, make sure there is a full binding +// of data after it so that the buffer is of size +// `last dynamic offset` + `array>`. +// +// Then when binding the first dynamic offset, the first 10 entries in the array +// are what you expect, but if you read the 11th you’re reading ‘invalid’ data +// which could be padding or could be from the next binding. +// +// In this way, we can pack ‘variable sized arrays’ into uniform buffer bindings +// which normally only support fixed size arrays. You just have to make sure +// in the shader that you only read the values that are valid for that binding. +pub fn extract_skins( + skin_uniforms: ResMut, + skinned_meshes: Extract>, + changed_skinned_meshes: Extract< + Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + >, + skinned_mesh_inverse_bindposes: Extract>>, + changed_transforms: Extract>>, + joints: Extract>, + mut removed_skinned_meshes_query: Extract>, +) { + let skin_uniforms = skin_uniforms.into_inner(); + + // Find skins that have become visible or invisible on this frame. Allocate, + // reallocate, or free space for them as necessary. + add_or_delete_skins( + skin_uniforms, + &changed_skinned_meshes, + &skinned_mesh_inverse_bindposes, + &joints, + ); + + // Extract the transforms for all joints from the scene, and write them into + // the staging buffer at the appropriate spot. + extract_joints( + skin_uniforms, + &skinned_meshes, + &changed_skinned_meshes, + &skinned_mesh_inverse_bindposes, + &changed_transforms, + &joints, + ); + + // Delete skins that became invisible. + for skinned_mesh_entity in removed_skinned_meshes_query.read() { + // Only remove a skin if we didn't pick it up in `add_or_delete_skins`. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_skinned_meshes.contains(skinned_mesh_entity) { + remove_skin(skin_uniforms, skinned_mesh_entity.into()); + } + } +} + +/// Searches for all skins that have become visible or invisible this frame and +/// allocations for them as necessary. +fn add_or_delete_skins( + skin_uniforms: &mut SkinUniforms, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // Find every skinned mesh that changed one of (1) visibility; (2) joint + // entities (part of `SkinnedMesh`); (3) the associated + // `SkinnedMeshInverseBindposes` asset. + for (skinned_mesh_entity, skinned_mesh_view_visibility, skinned_mesh) in changed_skinned_meshes + { + // Remove the skin if it existed last frame. + let skinned_mesh_entity = MainEntity::from(skinned_mesh_entity); + remove_skin(skin_uniforms, skinned_mesh_entity); + + // If the skin is invisible, we're done. + if !(*skinned_mesh_view_visibility).get() { + continue; + } + + // Initialize the skin. + add_skin( + skinned_mesh_entity, + skinned_mesh, + skin_uniforms, + skinned_mesh_inverse_bindposes, + joints, + ); + } +} + +/// Extracts the global transforms of all joints and updates the staging buffer +/// as necessary. +fn extract_joints( + skin_uniforms: &mut SkinUniforms, + skinned_meshes: &Query<(Entity, &SkinnedMesh)>, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + changed_transforms: &Query<(Entity, &GlobalTransform), Changed>, + joints: &Query<&GlobalTransform>, +) { + // If the number of entities that changed transforms exceeds a certain + // fraction (currently 25%) of the total joints in the scene, then skip + // fine-grained change detection. + // + // Note that this is a crude heuristic, for performance reasons. It doesn't + // consider the ratio of modified *joints* to total joints, only the ratio + // of modified *entities* to total joints. Thus in the worst case we might + // end up re-extracting all skins even though none of the joints changed. + // But making the heuristic finer-grained would make it slower to evaluate, + // and we don't want to lose performance. + let threshold = + (skin_uniforms.total_joints as f64 * JOINT_EXTRACTION_THRESHOLD_FACTOR).floor() as usize; + + if changed_transforms.iter().nth(threshold).is_some() { + // Go ahead and re-extract all skins in the scene. + for (skin_entity, skin) in skinned_meshes { + extract_joints_for_skin( + skin_entity.into(), + skin, + skin_uniforms, + changed_skinned_meshes, + skinned_mesh_inverse_bindposes, + joints, + ); + } + return; + } + + // Use fine-grained change detection to figure out only the skins that need + // to have their joints re-extracted. + let dirty_skins: MainEntityHashSet = changed_transforms + .iter() + .flat_map(|(joint, _)| skin_uniforms.joint_to_skins.get(&MainEntity::from(joint))) + .flat_map(|skin_joint_mappings| skin_joint_mappings.iter()) + .copied() + .collect(); + + // Re-extract the joints for only those skins. + for skin_entity in dirty_skins { + let Ok((_, skin)) = skinned_meshes.get(*skin_entity) else { + continue; + }; + extract_joints_for_skin( + skin_entity, + skin, + skin_uniforms, + changed_skinned_meshes, + skinned_mesh_inverse_bindposes, + joints, + ); + } +} + +/// Extracts all joints for a single skin and writes their transforms into the +/// CPU staging buffer. +fn extract_joints_for_skin( + skin_entity: MainEntity, + skin: &SkinnedMesh, + skin_uniforms: &mut SkinUniforms, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // If we initialized the skin this frame, we already populated all + // the joints, so there's no need to populate them again. + if changed_skinned_meshes.contains(*skin_entity) { + return; + } + + // Fetch information about the skin. + let Some(skin_uniform_info) = skin_uniforms.skin_uniform_info.get(&skin_entity) else { + return; + }; + let Some(skinned_mesh_inverse_bindposes) = + skinned_mesh_inverse_bindposes.get(&skin.inverse_bindposes) + else { + return; + }; + + // Calculate and write in the new joint matrices. + for (joint_index, (&joint, skinned_mesh_inverse_bindpose)) in skin + .joints + .iter() + .zip(skinned_mesh_inverse_bindposes.iter()) + .enumerate() + { + let Ok(joint_transform) = joints.get(joint) else { + continue; + }; + + let joint_matrix = joint_transform.affine() * *skinned_mesh_inverse_bindpose; + skin_uniforms.current_staging_buffer[skin_uniform_info.offset() as usize + joint_index] = + joint_matrix; + } +} + +/// Allocates space for a new skin in the buffers, and populates its joints. +fn add_skin( + skinned_mesh_entity: MainEntity, + skinned_mesh: &SkinnedMesh, + skin_uniforms: &mut SkinUniforms, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // Allocate space for the joints. + let Some(allocation) = skin_uniforms.allocator.allocate( + skinned_mesh + .joints + .len() + .div_ceil(JOINTS_PER_ALLOCATION_UNIT as usize) as u32, + ) else { + error!( + "Out of space for skin: {:?}. Tried to allocate space for {:?} joints.", + skinned_mesh_entity, + skinned_mesh.joints.len() + ); + return; + }; + + // Store that allocation. + let skin_uniform_info = SkinUniformInfo { + allocation, + joints: skinned_mesh + .joints + .iter() + .map(|entity| MainEntity::from(*entity)) + .collect(), + }; + + let skinned_mesh_inverse_bindposes = + skinned_mesh_inverse_bindposes.get(&skinned_mesh.inverse_bindposes); + + for (joint_index, &joint) in skinned_mesh.joints.iter().enumerate() { + // Calculate the initial joint matrix. + let skinned_mesh_inverse_bindpose = + skinned_mesh_inverse_bindposes.and_then(|skinned_mesh_inverse_bindposes| { + skinned_mesh_inverse_bindposes.get(joint_index) + }); + let joint_matrix = match (skinned_mesh_inverse_bindpose, joints.get(joint)) { + (Some(skinned_mesh_inverse_bindpose), Ok(transform)) => { + transform.affine() * *skinned_mesh_inverse_bindpose + } + _ => Mat4::IDENTITY, + }; + + // Write in the new joint matrix, growing the staging buffer if + // necessary. + let buffer_index = skin_uniform_info.offset() as usize + joint_index; + if skin_uniforms.current_staging_buffer.len() < buffer_index + 1 { + skin_uniforms + .current_staging_buffer + .resize(buffer_index + 1, Mat4::IDENTITY); + } + skin_uniforms.current_staging_buffer[buffer_index] = joint_matrix; + + // Record the inverse mapping from the joint back to the skin. We use + // this in order to perform fine-grained joint extraction. + skin_uniforms + .joint_to_skins + .entry(MainEntity::from(joint)) + .or_default() + .push(skinned_mesh_entity); + } + + // Record the number of joints. + skin_uniforms.total_joints += skinned_mesh.joints.len(); + + skin_uniforms + .skin_uniform_info + .insert(skinned_mesh_entity, skin_uniform_info); +} + +/// Deallocates a skin and removes it from the [`SkinUniforms`]. +fn remove_skin(skin_uniforms: &mut SkinUniforms, skinned_mesh_entity: MainEntity) { + let Some(old_skin_uniform_info) = skin_uniforms.skin_uniform_info.remove(&skinned_mesh_entity) + else { + return; + }; + + // Free the allocation. + skin_uniforms + .allocator + .free(old_skin_uniform_info.allocation); + + // Remove the inverse mapping from each joint back to the skin. + for &joint in &old_skin_uniform_info.joints { + if let Entry::Occupied(mut entry) = skin_uniforms.joint_to_skins.entry(joint) { + entry.get_mut().retain(|skin| *skin != skinned_mesh_entity); + if entry.get_mut().is_empty() { + entry.remove(); + } + } + } + + // Update the total number of joints. + skin_uniforms.total_joints -= old_skin_uniform_info.joints.len(); +} + +// NOTE: The skinned joints uniform buffer has to be bound at a dynamic offset per +// entity and so cannot currently be batched on WebGL 2. +pub fn no_automatic_skin_batching( + mut commands: Commands, + query: Query, Without)>, + render_device: Res, +) { + if !skins_use_uniform_buffers(&render_device.limits()) { + return; + } + + for entity in &query { + commands.entity(entity).try_insert(NoAutomaticBatching); + } +} diff --git a/vendor/bevy_pbr/src/render/skinning.wgsl b/vendor/bevy_pbr/src/render/skinning.wgsl new file mode 100644 index 00000000..6c4da075 --- /dev/null +++ b/vendor/bevy_pbr/src/render/skinning.wgsl @@ -0,0 +1,95 @@ +#define_import_path bevy_pbr::skinning + +#import bevy_pbr::mesh_types::SkinnedMesh +#import bevy_pbr::mesh_bindings::mesh + +#ifdef SKINNED + +#ifdef SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(1) var joint_matrices: SkinnedMesh; +#else // SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(1) var joint_matrices: array>; +#endif // SKINS_USE_UNIFORM_BUFFERS + +// An array of matrices specifying the joint positions from the previous frame. +// +// This is used for motion vector computation. +// +// If this is the first frame, or we're otherwise prevented from using data from +// the previous frame, this is simply the same as `joint_matrices` above. +#ifdef SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(6) var prev_joint_matrices: SkinnedMesh; +#else // SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(6) var prev_joint_matrices: array>; +#endif // SKINS_USE_UNIFORM_BUFFERS + +fn skin_model( + indexes: vec4, + weights: vec4, + instance_index: u32, +) -> mat4x4 { +#ifdef SKINS_USE_UNIFORM_BUFFERS + return weights.x * joint_matrices.data[indexes.x] + + weights.y * joint_matrices.data[indexes.y] + + weights.z * joint_matrices.data[indexes.z] + + weights.w * joint_matrices.data[indexes.w]; +#else // SKINS_USE_UNIFORM_BUFFERS + var skin_index = mesh[instance_index].current_skin_index; + return weights.x * joint_matrices[skin_index + indexes.x] + + weights.y * joint_matrices[skin_index + indexes.y] + + weights.z * joint_matrices[skin_index + indexes.z] + + weights.w * joint_matrices[skin_index + indexes.w]; +#endif // SKINS_USE_UNIFORM_BUFFERS +} + +// Returns the skinned position of a vertex with the given weights from the +// previous frame. +// +// This is used for motion vector computation. +fn skin_prev_model( + indexes: vec4, + weights: vec4, + instance_index: u32, +) -> mat4x4 { +#ifdef SKINS_USE_UNIFORM_BUFFERS + return weights.x * prev_joint_matrices.data[indexes.x] + + weights.y * prev_joint_matrices.data[indexes.y] + + weights.z * prev_joint_matrices.data[indexes.z] + + weights.w * prev_joint_matrices.data[indexes.w]; +#else // SKINS_USE_UNIFORM_BUFFERS + let skin_index = mesh[instance_index].current_skin_index; + return weights.x * prev_joint_matrices[skin_index + indexes.x] + + weights.y * prev_joint_matrices[skin_index + indexes.y] + + weights.z * prev_joint_matrices[skin_index + indexes.z] + + weights.w * prev_joint_matrices[skin_index + indexes.w]; +#endif // SKINS_USE_UNIFORM_BUFFERS +} + +fn inverse_transpose_3x3m(in: mat3x3) -> mat3x3 { + let x = cross(in[1], in[2]); + let y = cross(in[2], in[0]); + let z = cross(in[0], in[1]); + let det = dot(in[2], z); + return mat3x3( + x / det, + y / det, + z / det + ); +} + +fn skin_normals( + world_from_local: mat4x4, + normal: vec3, +) -> vec3 { + return normalize( + inverse_transpose_3x3m( + mat3x3( + world_from_local[0].xyz, + world_from_local[1].xyz, + world_from_local[2].xyz + ) + ) * normal + ); +} + +#endif diff --git a/vendor/bevy_pbr/src/render/utils.wgsl b/vendor/bevy_pbr/src/render/utils.wgsl new file mode 100644 index 00000000..2f9190c6 --- /dev/null +++ b/vendor/bevy_pbr/src/render/utils.wgsl @@ -0,0 +1,212 @@ +#define_import_path bevy_pbr::utils + +#import bevy_pbr::rgb9e5 +#import bevy_render::maths::{PI, PI_2, orthonormalize} + +// Generates a random u32 in range [0, u32::MAX]. +// +// `state` is a mutable reference to a u32 used as the seed. +// +// Values are generated via "white noise", with no correlation between values. +// In shaders, you often want spatial and/or temporal correlation. Use a different RNG method for these use cases. +// +// https://www.pcg-random.org +// https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering +fn rand_u(state: ptr) -> u32 { + *state = *state * 747796405u + 2891336453u; + let word = ((*state >> ((*state >> 28u) + 4u)) ^ *state) * 277803737u; + return (word >> 22u) ^ word; +} + +// Generates a random f32 in range [0, 1.0]. +fn rand_f(state: ptr) -> f32 { + *state = *state * 747796405u + 2891336453u; + let word = ((*state >> ((*state >> 28u) + 4u)) ^ *state) * 277803737u; + return f32((word >> 22u) ^ word) * bitcast(0x2f800004u); +} + +// Generates a random vec2 where each value is in range [0, 1.0]. +fn rand_vec2f(state: ptr) -> vec2 { + return vec2(rand_f(state), rand_f(state)); +} + +// Generates a random u32 in range [0, n). +fn rand_range_u(n: u32, state: ptr) -> u32 { + return rand_u(state) % n; +} + +// returns the (0-1, 0-1) position within the given viewport for the current buffer coords . +// buffer coords can be obtained from `@builtin(position).xy`. +// the view uniform struct contains the current camera viewport in `view.viewport`. +// topleft = 0,0 +fn coords_to_viewport_uv(position: vec2, viewport: vec4) -> vec2 { + return (position - viewport.xy) / viewport.zw; +} + +// https://jcgt.org/published/0003/02/01/paper.pdf + +// For encoding normals or unit direction vectors as octahedral coordinates. +fn octahedral_encode(v: vec3) -> vec2 { + var n = v / (abs(v.x) + abs(v.y) + abs(v.z)); + let octahedral_wrap = (1.0 - abs(n.yx)) * select(vec2(-1.0), vec2(1.0), n.xy > vec2f(0.0)); + let n_xy = select(octahedral_wrap, n.xy, n.z >= 0.0); + return n_xy * 0.5 + 0.5; +} + +// For decoding normals or unit direction vectors from octahedral coordinates. +fn octahedral_decode(v: vec2) -> vec3 { + let f = v * 2.0 - 1.0; + return octahedral_decode_signed(f); +} + +// Like octahedral_decode, but for input in [-1, 1] instead of [0, 1]. +fn octahedral_decode_signed(v: vec2) -> vec3 { + var n = vec3(v.xy, 1.0 - abs(v.x) - abs(v.y)); + let t = saturate(-n.z); + let w = select(vec2(t), vec2(-t), n.xy >= vec2(0.0)); + n = vec3(n.xy + w, n.z); + return normalize(n); +} + +// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence +fn interleaved_gradient_noise(pixel_coordinates: vec2, frame: u32) -> f32 { + let xy = pixel_coordinates + 5.588238 * f32(frame % 64u); + return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y)); +} + +// Hammersley sequence for quasi-random points +fn hammersley_2d(i: u32, n: u32) -> vec2f { + let inv_n = 1.0 / f32(n); + let vdc = f32(reverseBits(i)) * 2.3283064365386963e-10; // 1/2^32 + return vec2f(f32(i) * inv_n, vdc); +} + +// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) +// TODO: Use an array here instead of a bunch of constants, once arrays work properly under DX12. +// NOTE: The names have a final underscore to avoid the following error: +// `Composable module identifiers must not require substitution according to naga writeback rules` +const SPIRAL_OFFSET_0_ = vec2(-0.7071, 0.7071); +const SPIRAL_OFFSET_1_ = vec2(-0.0000, -0.8750); +const SPIRAL_OFFSET_2_ = vec2( 0.5303, 0.5303); +const SPIRAL_OFFSET_3_ = vec2(-0.6250, -0.0000); +const SPIRAL_OFFSET_4_ = vec2( 0.3536, -0.3536); +const SPIRAL_OFFSET_5_ = vec2(-0.0000, 0.3750); +const SPIRAL_OFFSET_6_ = vec2(-0.1768, -0.1768); +const SPIRAL_OFFSET_7_ = vec2( 0.1250, 0.0000); + +// 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 = PI_2 * 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); +} + +// https://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations#UniformlySamplingaHemisphere +fn sample_uniform_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = sin_theta * cos(phi); + let y = sin_theta * sin(phi); + let z = cos_theta; + return orthonormalize(normal) * vec3(x, y, z); +} + +fn uniform_hemisphere_inverse_pdf() -> f32 { + return PI_2; +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec19%3A294 +fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { + let ab = 2.0 * rand_vec2f(rng) - 1.0; + let a = ab.x; + var b = ab.y; + if (b == 0.0) { b = 1.0; } + + var phi: f32; + var r: f32; + if (a * a > b * b) { + r = disk_radius * a; + phi = (PI / 4.0) * (b / a); + } else { + r = disk_radius * b; + phi = (PI / 2.0) - (PI / 4.0) * (a / b); + } + + let x = r * cos(phi); + let y = r * sin(phi); + return vec2(x, y); +} + +// Convert UV and face index to direction vector +fn sample_cube_dir(uv: vec2f, face: u32) -> vec3f { + // Convert from [0,1] to [-1,1] + let uvc = 2.0 * uv - 1.0; + + // Generate direction based on the cube face + var dir: vec3f; + switch(face) { + case 0u: { dir = vec3f( 1.0, -uvc.y, -uvc.x); } // +X + case 1u: { dir = vec3f(-1.0, -uvc.y, uvc.x); } // -X + case 2u: { dir = vec3f( uvc.x, 1.0, uvc.y); } // +Y + case 3u: { dir = vec3f( uvc.x, -1.0, -uvc.y); } // -Y + case 4u: { dir = vec3f( uvc.x, -uvc.y, 1.0); } // +Z + case 5u: { dir = vec3f(-uvc.x, -uvc.y, -1.0); } // -Z + default: { dir = vec3f(0.0); } + } + return normalize(dir); +} + +// Convert direction vector to cube face UV +struct CubeUV { + uv: vec2f, + face: u32, +} +fn dir_to_cube_uv(dir: vec3f) -> CubeUV { + let abs_dir = abs(dir); + var face: u32 = 0u; + var uv: vec2f = vec2f(0.0); + + // Find the dominant axis to determine face + if (abs_dir.x >= abs_dir.y && abs_dir.x >= abs_dir.z) { + // X axis is dominant + if (dir.x > 0.0) { + face = 0u; // +X + uv = vec2f(-dir.z, -dir.y) / dir.x; + } else { + face = 1u; // -X + uv = vec2f(dir.z, -dir.y) / abs_dir.x; + } + } else if (abs_dir.y >= abs_dir.x && abs_dir.y >= abs_dir.z) { + // Y axis is dominant + if (dir.y > 0.0) { + face = 2u; // +Y + uv = vec2f(dir.x, dir.z) / dir.y; + } else { + face = 3u; // -Y + uv = vec2f(dir.x, -dir.z) / abs_dir.y; + } + } else { + // Z axis is dominant + if (dir.z > 0.0) { + face = 4u; // +Z + uv = vec2f(dir.x, -dir.y) / dir.z; + } else { + face = 5u; // -Z + uv = vec2f(-dir.x, -dir.y) / abs_dir.z; + } + } + + // Convert from [-1,1] to [0,1] + return CubeUV(uv * 0.5 + 0.5, face); +} + +// The Porter-Duff OVER operator on RGBA, correctly computing alpha of the +// result. +fn porter_duff_over(bg: vec4, fg: vec4) -> vec4 { + return vec4(mix(bg.rgb * bg.a, fg.rgb, fg.a), bg.a + fg.a * (1.0 - bg.a)); +} diff --git a/vendor/bevy_pbr/src/render/view_transformations.wgsl b/vendor/bevy_pbr/src/render/view_transformations.wgsl new file mode 100644 index 00000000..dfb4d6e9 --- /dev/null +++ b/vendor/bevy_pbr/src/render/view_transformations.wgsl @@ -0,0 +1,238 @@ +#define_import_path bevy_pbr::view_transformations + +#import bevy_pbr::mesh_view_bindings as view_bindings +#import bevy_pbr::prepass_bindings + +/// World space: +/// +y is up + +/// View space: +/// -z is forward, +x is right, +y is up +/// Forward is from the camera position into the scene. +/// (0.0, 0.0, -1.0) is linear distance of 1.0 in front of the camera's view relative to the camera's rotation +/// (0.0, 1.0, 0.0) is linear distance of 1.0 above the camera's view relative to the camera's rotation + +/// NDC (normalized device coordinate): +/// https://www.w3.org/TR/webgpu/#coordinate-systems +/// (-1.0, -1.0) in NDC is located at the bottom-left corner of NDC +/// (1.0, 1.0) in NDC is located at the top-right corner of NDC +/// Z is depth where: +/// 1.0 is near clipping plane +/// Perspective projection: 0.0 is inf far away +/// Orthographic projection: 0.0 is far clipping plane + +/// Clip space: +/// This is NDC before the perspective divide, still in homogenous coordinate space. +/// Dividing a clip space point by its w component yields a point in NDC space. + +/// UV space: +/// 0.0, 0.0 is the top left +/// 1.0, 1.0 is the bottom right + + +// ----------------- +// TO WORLD -------- +// ----------------- + +/// Convert a view space position to world space +fn position_view_to_world(view_pos: vec3) -> vec3 { + let world_pos = view_bindings::view.world_from_view * vec4(view_pos, 1.0); + return world_pos.xyz; +} + +/// Convert a clip space position to world space +fn position_clip_to_world(clip_pos: vec4) -> vec3 { + let world_pos = view_bindings::view.world_from_clip * clip_pos; + return world_pos.xyz; +} + +/// Convert a ndc space position to world space +fn position_ndc_to_world(ndc_pos: vec3) -> vec3 { + let world_pos = view_bindings::view.world_from_clip * vec4(ndc_pos, 1.0); + return world_pos.xyz / world_pos.w; +} + +/// Convert a view space direction to world space +fn direction_view_to_world(view_dir: vec3) -> vec3 { + let world_dir = view_bindings::view.world_from_view * vec4(view_dir, 0.0); + return world_dir.xyz; +} + +/// Convert a clip space direction to world space +fn direction_clip_to_world(clip_dir: vec4) -> vec3 { + let world_dir = view_bindings::view.world_from_clip * clip_dir; + return world_dir.xyz; +} + +// ----------------- +// TO VIEW --------- +// ----------------- + +/// Convert a world space position to view space +fn position_world_to_view(world_pos: vec3) -> vec3 { + let view_pos = view_bindings::view.view_from_world * vec4(world_pos, 1.0); + return view_pos.xyz; +} + +/// Convert a clip space position to view space +fn position_clip_to_view(clip_pos: vec4) -> vec3 { + let view_pos = view_bindings::view.view_from_clip * clip_pos; + return view_pos.xyz; +} + +/// Convert a ndc space position to view space +fn position_ndc_to_view(ndc_pos: vec3) -> vec3 { + let view_pos = view_bindings::view.view_from_clip * vec4(ndc_pos, 1.0); + return view_pos.xyz / view_pos.w; +} + +/// Convert a world space direction to view space +fn direction_world_to_view(world_dir: vec3) -> vec3 { + let view_dir = view_bindings::view.view_from_world * vec4(world_dir, 0.0); + return view_dir.xyz; +} + +/// Convert a clip space direction to view space +fn direction_clip_to_view(clip_dir: vec4) -> vec3 { + let view_dir = view_bindings::view.view_from_clip * clip_dir; + return view_dir.xyz; +} + +// ----------------- +// TO PREV. VIEW --- +// ----------------- + +fn position_world_to_prev_view(world_pos: vec3) -> vec3 { + let view_pos = prepass_bindings::previous_view_uniforms.view_from_world * + vec4(world_pos, 1.0); + return view_pos.xyz; +} + +fn position_world_to_prev_ndc(world_pos: vec3) -> vec3 { + let ndc_pos = prepass_bindings::previous_view_uniforms.clip_from_world * + vec4(world_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +// ----------------- +// TO CLIP --------- +// ----------------- + +/// Convert a world space position to clip space +fn position_world_to_clip(world_pos: vec3) -> vec4 { + let clip_pos = view_bindings::view.clip_from_world * vec4(world_pos, 1.0); + return clip_pos; +} + +/// Convert a view space position to clip space +fn position_view_to_clip(view_pos: vec3) -> vec4 { + let clip_pos = view_bindings::view.clip_from_view * vec4(view_pos, 1.0); + return clip_pos; +} + +/// Convert a world space direction to clip space +fn direction_world_to_clip(world_dir: vec3) -> vec4 { + let clip_dir = view_bindings::view.clip_from_world * vec4(world_dir, 0.0); + return clip_dir; +} + +/// Convert a view space direction to clip space +fn direction_view_to_clip(view_dir: vec3) -> vec4 { + let clip_dir = view_bindings::view.clip_from_view * vec4(view_dir, 0.0); + return clip_dir; +} + +// ----------------- +// TO NDC ---------- +// ----------------- + +/// Convert a world space position to ndc space +fn position_world_to_ndc(world_pos: vec3) -> vec3 { + let ndc_pos = view_bindings::view.clip_from_world * vec4(world_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +/// Convert a view space position to ndc space +fn position_view_to_ndc(view_pos: vec3) -> vec3 { + let ndc_pos = view_bindings::view.clip_from_view * vec4(view_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +// ----------------- +// DEPTH ----------- +// ----------------- + +/// Retrieve the perspective camera near clipping plane +fn perspective_camera_near() -> f32 { + return view_bindings::view.clip_from_view[3][2]; +} + +/// Convert ndc depth to linear view z. +/// Note: Depth values in front of the camera will be negative as -z is forward +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(view_bindings::view.clip_from_view[3][2] - ndc_depth) / view_bindings::view.clip_from_view[2][2]; +#else + let view_pos = view_bindings::view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +/// Convert linear view z to ndc depth. +/// Note: View z input should be negative for values in front of the camera as -z is forward +fn view_z_to_depth_ndc(view_z: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / view_z; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return view_bindings::view.clip_from_view[3][2] + view_z * view_bindings::view.clip_from_view[2][2]; +#else + let ndc_pos = view_bindings::view.clip_from_view * vec4(0.0, 0.0, view_z, 1.0); + return ndc_pos.z / ndc_pos.w; +#endif +} + +fn prev_view_z_to_depth_ndc(view_z: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / view_z; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return prepass_bindings::previous_view_uniforms.clip_from_view[3][2] + + view_z * prepass_bindings::previous_view_uniforms.clip_from_view[2][2]; +#else + let ndc_pos = prepass_bindings::previous_view_uniforms.clip_from_view * + vec4(0.0, 0.0, view_z, 1.0); + return ndc_pos.z / ndc_pos.w; +#endif +} + +// ----------------- +// UV -------------- +// ----------------- + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// returns the (0.0, 0.0) .. (1.0, 1.0) position within the viewport for the current render target +/// [0 .. render target viewport size] eg. [(0.0, 0.0) .. (1280.0, 720.0)] to [(0.0, 0.0) .. (1.0, 1.0)] +fn frag_coord_to_uv(frag_coord: vec2) -> vec2 { + return (frag_coord - view_bindings::view.viewport.xy) / view_bindings::view.viewport.zw; +} + +/// Convert frag coord to ndc +fn frag_coord_to_ndc(frag_coord: vec4) -> vec3 { + return vec3(uv_to_ndc(frag_coord_to_uv(frag_coord.xy)), frag_coord.z); +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to [0 .. render target +/// viewport size] +fn ndc_to_frag_coord(ndc: vec2) -> vec2 { + return ndc_to_uv(ndc) * view_bindings::view.viewport.zw; +} diff --git a/vendor/bevy_pbr/src/render/wireframe.wgsl b/vendor/bevy_pbr/src/render/wireframe.wgsl new file mode 100644 index 00000000..3873ffa3 --- /dev/null +++ b/vendor/bevy_pbr/src/render/wireframe.wgsl @@ -0,0 +1,12 @@ +#import bevy_pbr::forward_io::VertexOutput + +struct PushConstants { + color: vec4 +} + +var push_constants: PushConstants; + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + return push_constants.color; +} diff --git a/vendor/bevy_pbr/src/ssao/mod.rs b/vendor/bevy_pbr/src/ssao/mod.rs new file mode 100644 index 00000000..81aa5aa4 --- /dev/null +++ b/vendor/bevy_pbr/src/ssao/mod.rs @@ -0,0 +1,773 @@ +use crate::NodePbr; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_camera::{Camera, Camera3d}; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DepthPrepass, NormalPrepass, ViewPrepassTextures}, +}; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::{Has, QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::ToExtents; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + camera::{ExtractedCamera, TemporalJitter}, + diagnostic::RecordDiagnostics, + extract_component::ExtractComponent, + globals::{GlobalsBuffer, GlobalsUniform}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + }, + *, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{CachedTexture, TextureCache}, + view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bevy_utils::prelude::default; +use core::mem; +use tracing::{error, warn}; + +/// Plugin for screen space ambient occlusion. +pub struct ScreenSpaceAmbientOcclusionPlugin; + +impl Plugin for ScreenSpaceAmbientOcclusionPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "ssao_utils.wgsl"); + + embedded_asset!(app, "preprocess_depth.wgsl"); + embedded_asset!(app, "ssao.wgsl"); + embedded_asset!(app, "spatial_denoise.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + if render_app + .world() + .resource::() + .limits() + .max_storage_textures_per_shader_stage + < 5 + { + warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: Limits::max_storage_textures_per_shader_stage is less than 5."); + return; + } + + render_app + .init_resource::() + .init_resource::>() + .add_systems(ExtractSchedule, extract_ssao_settings) + .add_systems( + Render, + ( + prepare_ssao_pipelines.in_set(RenderSystems::Prepare), + prepare_ssao_textures.in_set(RenderSystems::PrepareResources), + prepare_ssao_bind_groups.in_set(RenderSystems::PrepareBindGroups), + ), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::ScreenSpaceAmbientOcclusion, + ) + .add_render_graph_edges( + Core3d, + ( + // END_PRE_PASSES -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS + Node3d::EndPrepasses, + NodePbr::ScreenSpaceAmbientOcclusion, + Node3d::StartMainPass, + ), + ); + } +} + +/// Component to apply screen space ambient occlusion to a 3d camera. +/// +/// Screen space ambient occlusion (SSAO) approximates small-scale, +/// local occlusion of _indirect_ diffuse light between objects, based on what's visible on-screen. +/// SSAO does not apply to direct lighting, such as point or directional lights. +/// +/// This darkens creases, e.g. on staircases, and gives nice contact shadows +/// where objects meet, giving entities a more "grounded" feel. +/// +/// # Usage Notes +/// +/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app. +/// +/// It strongly recommended that you use SSAO in conjunction with +/// TAA (`TemporalAntiAliasing`). +/// Doing so greatly reduces SSAO noise. +/// +/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`. +#[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)] +#[reflect(Component, Debug, Default, PartialEq, Clone)] +#[require(DepthPrepass, NormalPrepass)] +#[doc(alias = "Ssao")] +pub struct ScreenSpaceAmbientOcclusion { + /// Quality of the SSAO effect. + pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + /// A constant estimated thickness of objects. + /// + /// This value is used to decide how far behind an object a ray of light needs to be in order + /// to pass behind it. Any ray closer than that will be occluded. + pub constant_object_thickness: f32, +} + +impl Default for ScreenSpaceAmbientOcclusion { + fn default() -> Self { + Self { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel::default(), + constant_object_thickness: 0.25, + } + } +} + +#[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] +#[reflect(PartialEq, Hash, Clone, Default)] +pub enum ScreenSpaceAmbientOcclusionQualityLevel { + Low, + Medium, + #[default] + High, + Ultra, + Custom { + /// Higher slice count means less noise, but worse performance. + slice_count: u32, + /// Samples per slice side is also tweakable, but recommended to be left at 2 or 3. + samples_per_slice_side: u32, + }, +} + +impl ScreenSpaceAmbientOcclusionQualityLevel { + fn sample_counts(&self) -> (u32, u32) { + match self { + Self::Low => (1, 2), // 4 spp (1 * (2 * 2)), plus optional temporal samples + Self::Medium => (2, 2), // 8 spp (2 * (2 * 2)), plus optional temporal samples + Self::High => (3, 3), // 18 spp (3 * (3 * 2)), plus optional temporal samples + Self::Ultra => (9, 3), // 54 spp (9 * (3 * 2)), plus optional temporal samples + Self::Custom { + slice_count: slices, + samples_per_slice_side, + } => (*slices, *samples_per_slice_side), + } + } +} + +#[derive(Default)] +struct SsaoNode {} + +impl ViewNode for SsaoNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static SsaoPipelineId, + &'static SsaoBindGroups, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (camera, pipeline_id, bind_groups, view_uniform_offset): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let ( + Some(camera_size), + Some(preprocess_depth_pipeline), + Some(spatial_denoise_pipeline), + Some(ssao_pipeline), + ) = ( + camera.physical_viewport_size, + pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), + pipeline_cache.get_compute_pipeline(pipelines.spatial_denoise_pipeline), + pipeline_cache.get_compute_pipeline(pipeline_id.0), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let command_encoder = render_context.command_encoder(); + command_encoder.push_debug_group("ssao"); + let time_span = diagnostics.time_span(command_encoder, "ssao"); + + { + let mut preprocess_depth_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao_preprocess_depth"), + timestamp_writes: None, + }); + preprocess_depth_pass.set_pipeline(preprocess_depth_pipeline); + preprocess_depth_pass.set_bind_group(0, &bind_groups.preprocess_depth_bind_group, &[]); + preprocess_depth_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + preprocess_depth_pass.dispatch_workgroups( + camera_size.x.div_ceil(16), + camera_size.y.div_ceil(16), + 1, + ); + } + + { + let mut ssao_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao"), + timestamp_writes: None, + }); + ssao_pass.set_pipeline(ssao_pipeline); + ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]); + ssao_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + ssao_pass.dispatch_workgroups(camera_size.x.div_ceil(8), camera_size.y.div_ceil(8), 1); + } + + { + let mut spatial_denoise_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao_spatial_denoise"), + timestamp_writes: None, + }); + spatial_denoise_pass.set_pipeline(spatial_denoise_pipeline); + spatial_denoise_pass.set_bind_group(0, &bind_groups.spatial_denoise_bind_group, &[]); + spatial_denoise_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + spatial_denoise_pass.dispatch_workgroups( + camera_size.x.div_ceil(8), + camera_size.y.div_ceil(8), + 1, + ); + } + + time_span.end(command_encoder); + command_encoder.pop_debug_group(); + Ok(()) + } +} + +#[derive(Resource)] +struct SsaoPipelines { + preprocess_depth_pipeline: CachedComputePipelineId, + spatial_denoise_pipeline: CachedComputePipelineId, + + common_bind_group_layout: BindGroupLayoutDescriptor, + preprocess_depth_bind_group_layout: BindGroupLayoutDescriptor, + ssao_bind_group_layout: BindGroupLayoutDescriptor, + spatial_denoise_bind_group_layout: BindGroupLayoutDescriptor, + + hilbert_index_lut: TextureView, + point_clamp_sampler: Sampler, + linear_clamp_sampler: Sampler, + + shader: Handle, + depth_format: TextureFormat, +} + +impl FromWorld for SsaoPipelines { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let pipeline_cache = world.resource::(); + + // Detect the depth format support + let render_adapter = world.resource::(); + let depth_format = if render_adapter + .get_texture_format_features(TextureFormat::R16Float) + .allowed_usages + .contains(TextureUsages::STORAGE_BINDING) + { + TextureFormat::R16Float + } else { + TextureFormat::R32Float + }; + + let hilbert_index_lut = render_device + .create_texture_with_data( + render_queue, + &(TextureDescriptor { + label: Some("ssao_hilbert_index_lut"), + size: Extent3d { + width: HILBERT_WIDTH as u32, + height: HILBERT_WIDTH as u32, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }), + TextureDataOrder::default(), + bytemuck::cast_slice(&generate_hilbert_index_lut()), + ) + .create_view(&TextureViewDescriptor::default()); + + let point_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Nearest, + mag_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); + let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); + + let common_bind_group_layout = BindGroupLayoutDescriptor::new( + "ssao_common_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::NonFiltering), + sampler(SamplerBindingType::Filtering), + uniform_buffer::(true), + ), + ), + ); + + let preprocess_depth_bind_group_layout = BindGroupLayoutDescriptor::new( + "ssao_preprocess_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_depth_2d(), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + ), + ), + ); + + let ssao_bind_group_layout = BindGroupLayoutDescriptor::new( + "ssao_ssao_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Uint), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), + uniform_buffer::(false), + uniform_buffer::(false), + ), + ), + ); + + let spatial_denoise_bind_group_layout = BindGroupLayoutDescriptor::new( + "ssao_spatial_denoise_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Uint), + texture_storage_2d(depth_format, StorageTextureAccess::WriteOnly), + ), + ), + ); + + let mut shader_defs = Vec::new(); + if depth_format == TextureFormat::R16Float { + shader_defs.push("USE_R16FLOAT".into()); + } + + let preprocess_depth_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("ssao_preprocess_depth_pipeline".into()), + layout: vec![ + preprocess_depth_bind_group_layout.clone(), + common_bind_group_layout.clone(), + ], + shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), + shader_defs: shader_defs.clone(), + ..default() + }); + + let spatial_denoise_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("ssao_spatial_denoise_pipeline".into()), + layout: vec![ + spatial_denoise_bind_group_layout.clone(), + common_bind_group_layout.clone(), + ], + shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), + shader_defs, + ..default() + }); + + Self { + preprocess_depth_pipeline, + spatial_denoise_pipeline, + + common_bind_group_layout, + preprocess_depth_bind_group_layout, + ssao_bind_group_layout, + spatial_denoise_bind_group_layout, + + hilbert_index_lut, + point_clamp_sampler, + linear_clamp_sampler, + + shader: load_embedded_asset!(world, "ssao.wgsl"), + depth_format, + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct SsaoPipelineKey { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + temporal_jitter: bool, +} + +impl SpecializedComputePipeline for SsaoPipelines { + type Key = SsaoPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts(); + + let mut shader_defs = vec![ + ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), + ShaderDefVal::Int( + "SAMPLES_PER_SLICE_SIDE".to_string(), + samples_per_slice_side as i32, + ), + ]; + + if key.temporal_jitter { + shader_defs.push("TEMPORAL_JITTER".into()); + } + + if self.depth_format == TextureFormat::R16Float { + shader_defs.push("USE_R16FLOAT".into()); + } + + ComputePipelineDescriptor { + label: Some("ssao_ssao_pipeline".into()), + layout: vec![ + self.ssao_bind_group_layout.clone(), + self.common_bind_group_layout.clone(), + ], + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +fn extract_ssao_settings( + mut commands: Commands, + cameras: Extract< + Query< + (RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), + (With, With, With), + >, + >, +) { + for (entity, camera, ssao_settings, msaa) in &cameras { + if *msaa != Msaa::Off { + error!( + "SSAO is being used which requires Msaa::Off, but Msaa is currently set to Msaa::{:?}", + *msaa + ); + return; + } + let mut entity_commands = commands + .get_entity(entity) + .expect("SSAO entity wasn't synced."); + if camera.is_active { + entity_commands.insert(ssao_settings.clone()); + } else { + entity_commands.remove::(); + } + } +} + +#[derive(Component)] +pub struct ScreenSpaceAmbientOcclusionResources { + preprocessed_depth_texture: CachedTexture, + ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture + pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture + depth_differences_texture: CachedTexture, + thickness_buffer: Buffer, +} + +fn prepare_ssao_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + pipelines: Res, + views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>, +) { + for (entity, camera, ssao_settings) in &views { + let Some(physical_viewport_size) = camera.physical_viewport_size else { + continue; + }; + let size = physical_viewport_size.to_extents(); + + let preprocessed_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_preprocessed_depth_texture"), + size, + mip_level_count: 5, + sample_count: 1, + dimension: TextureDimension::D2, + format: pipelines.depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let ssao_noisy_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_noisy_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: pipelines.depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let ssao_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: pipelines.depth_format, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let depth_differences_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_depth_differences_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Uint, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("thickness_buffer"), + contents: &ssao_settings.constant_object_thickness.to_le_bytes(), + usage: BufferUsages::UNIFORM, + }); + + commands + .entity(entity) + .insert(ScreenSpaceAmbientOcclusionResources { + preprocessed_depth_texture, + ssao_noisy_texture, + screen_space_ambient_occlusion_texture: ssao_texture, + depth_differences_texture, + thickness_buffer, + }); + } +} + +#[derive(Component)] +struct SsaoPipelineId(CachedComputePipelineId); + +fn prepare_ssao_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + views: Query<(Entity, &ScreenSpaceAmbientOcclusion, Has)>, +) { + for (entity, ssao_settings, temporal_jitter) in &views { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &pipeline, + SsaoPipelineKey { + quality_level: ssao_settings.quality_level, + temporal_jitter, + }, + ); + + commands.entity(entity).insert(SsaoPipelineId(pipeline_id)); + } +} + +#[derive(Component)] +struct SsaoBindGroups { + common_bind_group: BindGroup, + preprocess_depth_bind_group: BindGroup, + ssao_bind_group: BindGroup, + spatial_denoise_bind_group: BindGroup, +} + +fn prepare_ssao_bind_groups( + mut commands: Commands, + render_device: Res, + pipelines: Res, + view_uniforms: Res, + global_uniforms: Res, + pipeline_cache: Res, + views: Query<( + Entity, + &ScreenSpaceAmbientOcclusionResources, + &ViewPrepassTextures, + )>, +) { + let (Some(view_uniforms), Some(globals_uniforms)) = ( + view_uniforms.uniforms.binding(), + global_uniforms.buffer.binding(), + ) else { + return; + }; + + for (entity, ssao_resources, prepass_textures) in &views { + let common_bind_group = render_device.create_bind_group( + "ssao_common_bind_group", + &pipeline_cache.get_bind_group_layout(&pipelines.common_bind_group_layout), + &BindGroupEntries::sequential(( + &pipelines.point_clamp_sampler, + &pipelines.linear_clamp_sampler, + view_uniforms.clone(), + )), + ); + + let create_depth_view = |mip_level| { + ssao_resources + .preprocessed_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("ssao_preprocessed_depth_texture_mip_view"), + base_mip_level: mip_level, + format: Some(pipelines.depth_format), + dimension: Some(TextureViewDimension::D2), + mip_level_count: Some(1), + ..default() + }) + }; + + let preprocess_depth_bind_group = render_device.create_bind_group( + "ssao_preprocess_depth_bind_group", + &pipeline_cache.get_bind_group_layout(&pipelines.preprocess_depth_bind_group_layout), + &BindGroupEntries::sequential(( + prepass_textures.depth_view().unwrap(), + &create_depth_view(0), + &create_depth_view(1), + &create_depth_view(2), + &create_depth_view(3), + &create_depth_view(4), + )), + ); + + let ssao_bind_group = render_device.create_bind_group( + "ssao_ssao_bind_group", + &pipeline_cache.get_bind_group_layout(&pipelines.ssao_bind_group_layout), + &BindGroupEntries::sequential(( + &ssao_resources.preprocessed_depth_texture.default_view, + prepass_textures.normal_view().unwrap(), + &pipelines.hilbert_index_lut, + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + globals_uniforms.clone(), + ssao_resources.thickness_buffer.as_entire_binding(), + )), + ); + + let spatial_denoise_bind_group = render_device.create_bind_group( + "ssao_spatial_denoise_bind_group", + &pipeline_cache.get_bind_group_layout(&pipelines.spatial_denoise_bind_group_layout), + &BindGroupEntries::sequential(( + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + &ssao_resources + .screen_space_ambient_occlusion_texture + .default_view, + )), + ); + + commands.entity(entity).insert(SsaoBindGroups { + common_bind_group, + preprocess_depth_bind_group, + ssao_bind_group, + spatial_denoise_bind_group, + }); + } +} + +fn generate_hilbert_index_lut() -> [[u16; 64]; 64] { + use core::array::from_fn; + from_fn(|x| from_fn(|y| hilbert_index(x as u16, y as u16))) +} + +// https://www.shadertoy.com/view/3tB3z3 +const HILBERT_WIDTH: u16 = 64; +fn hilbert_index(mut x: u16, mut y: u16) -> u16 { + let mut index = 0; + + let mut level: u16 = HILBERT_WIDTH / 2; + while level > 0 { + let region_x = (x & level > 0) as u16; + let region_y = (y & level > 0) as u16; + index += level * level * ((3 * region_x) ^ region_y); + + if region_y == 0 { + if region_x == 1 { + x = HILBERT_WIDTH - 1 - x; + y = HILBERT_WIDTH - 1 - y; + } + + mem::swap(&mut x, &mut y); + } + + level /= 2; + } + + index +} diff --git a/vendor/bevy_pbr/src/ssao/preprocess_depth.wgsl b/vendor/bevy_pbr/src/ssao/preprocess_depth.wgsl new file mode 100644 index 00000000..bce9e56f --- /dev/null +++ b/vendor/bevy_pbr/src/ssao/preprocess_depth.wgsl @@ -0,0 +1,110 @@ +// Inputs a depth texture and outputs a MIP-chain of depths. +// +// Because SSAO's performance is bound by texture reads, this increases +// performance over using the full resolution depth for every sample. + +// Reference: https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf, section 2.2 + +#import bevy_render::view::View + +@group(0) @binding(0) var input_depth: texture_depth_2d; +#ifdef USE_R16FLOAT +@group(0) @binding(1) var preprocessed_depth_mip0: texture_storage_2d; +@group(0) @binding(2) var preprocessed_depth_mip1: texture_storage_2d; +@group(0) @binding(3) var preprocessed_depth_mip2: texture_storage_2d; +@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d; +@group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d; +#else +@group(0) @binding(1) var preprocessed_depth_mip0: texture_storage_2d; +@group(0) @binding(2) var preprocessed_depth_mip1: texture_storage_2d; +@group(0) @binding(3) var preprocessed_depth_mip2: texture_storage_2d; +@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d; +@group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d; +#endif +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + + +// Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP +fn weighted_average(depth0: f32, depth1: f32, depth2: f32, depth3: f32) -> f32 { + let depth_range_scale_factor = 0.75; + let effect_radius = depth_range_scale_factor * 0.5 * 1.457; + let falloff_range = 0.615 * effect_radius; + let falloff_from = effect_radius * (1.0 - 0.615); + let falloff_mul = -1.0 / falloff_range; + let falloff_add = falloff_from / falloff_range + 1.0; + + let min_depth = min(min(depth0, depth1), min(depth2, depth3)); + let weight0 = saturate((depth0 - min_depth) * falloff_mul + falloff_add); + let weight1 = saturate((depth1 - min_depth) * falloff_mul + falloff_add); + let weight2 = saturate((depth2 - min_depth) * falloff_mul + falloff_add); + let weight3 = saturate((depth3 - min_depth) * falloff_mul + falloff_add); + let weight_total = weight0 + weight1 + weight2 + weight3; + + return ((weight0 * depth0) + (weight1 * depth1) + (weight2 * depth2) + (weight3 * depth3)) / weight_total; +} + +// Used to share the depths from the previous MIP level between all invocations in a workgroup +var previous_mip_depth: array, 8>; + +@compute +@workgroup_size(8, 8, 1) +fn preprocess_depth(@builtin(global_invocation_id) global_id: vec3, @builtin(local_invocation_id) local_id: vec3) { + let base_coordinates = vec2(global_id.xy); + + // MIP 0 - Copy 4 texels from the input depth (per invocation, 8x8 invocations per workgroup) + let pixel_coordinates0 = base_coordinates * 2i; + let pixel_coordinates1 = pixel_coordinates0 + vec2(1i, 0i); + let pixel_coordinates2 = pixel_coordinates0 + vec2(0i, 1i); + let pixel_coordinates3 = pixel_coordinates0 + vec2(1i, 1i); + let depths_uv = vec2(pixel_coordinates0) / view.viewport.zw; + let depths = textureGather(0, input_depth, point_clamp_sampler, depths_uv, vec2(1i, 1i)); + textureStore(preprocessed_depth_mip0, pixel_coordinates0, vec4(depths.w, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates1, vec4(depths.z, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates2, vec4(depths.x, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates3, vec4(depths.y, 0.0, 0.0, 0.0)); + + // MIP 1 - Weighted average of MIP 0's depth values (per invocation, 8x8 invocations per workgroup) + let depth_mip1 = weighted_average(depths.w, depths.z, depths.x, depths.y); + textureStore(preprocessed_depth_mip1, base_coordinates, vec4(depth_mip1, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip1; + + workgroupBarrier(); + + // MIP 2 - Weighted average of MIP 1's depth values (per invocation, 4x4 invocations per workgroup) + if all(local_id.xy % vec2(2u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 1u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 1u]; + let depth3 = previous_mip_depth[local_id.x + 1u][local_id.y + 1u]; + let depth_mip2 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip2, base_coordinates / 2i, vec4(depth_mip2, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip2; + } + + workgroupBarrier(); + + // MIP 3 - Weighted average of MIP 2's depth values (per invocation, 2x2 invocations per workgroup) + if all(local_id.xy % vec2(4u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 2u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 2u]; + let depth3 = previous_mip_depth[local_id.x + 2u][local_id.y + 2u]; + let depth_mip3 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip3, base_coordinates / 4i, vec4(depth_mip3, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip3; + } + + workgroupBarrier(); + + // MIP 4 - Weighted average of MIP 3's depth values (per invocation, 1 invocation per workgroup) + if all(local_id.xy % vec2(8u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 4u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 4u]; + let depth3 = previous_mip_depth[local_id.x + 4u][local_id.y + 4u]; + let depth_mip4 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip4, base_coordinates / 8i, vec4(depth_mip4, 0.0, 0.0, 0.0)); + } +} diff --git a/vendor/bevy_pbr/src/ssao/spatial_denoise.wgsl b/vendor/bevy_pbr/src/ssao/spatial_denoise.wgsl new file mode 100644 index 00000000..f61934fb --- /dev/null +++ b/vendor/bevy_pbr/src/ssao/spatial_denoise.wgsl @@ -0,0 +1,89 @@ +// 3x3 bilaterial filter (edge-preserving blur) +// https://people.csail.mit.edu/sparis/bf_course/course_notes.pdf + +// Note: Does not use the Gaussian kernel part of a typical bilateral blur +// From the paper: "use the information gathered on a neighborhood of 4 × 4 using a bilateral filter for +// reconstruction, using _uniform_ convolution weights" + +// Note: The paper does a 4x4 (not quite centered) filter, offset by +/- 1 pixel every other frame +// XeGTAO does a 3x3 filter, on two pixels at a time per compute thread, applied twice +// We do a 3x3 filter, on 1 pixel per compute thread, applied once + +#import bevy_render::view::View + +@group(0) @binding(0) var ambient_occlusion_noisy: texture_2d; +@group(0) @binding(1) var depth_differences: texture_2d; +#ifdef USE_R16FLOAT +@group(0) @binding(2) var ambient_occlusion: texture_storage_2d; +#else +@group(0) @binding(2) var ambient_occlusion: texture_storage_2d; +#endif +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + +@compute +@workgroup_size(8, 8, 1) +fn spatial_denoise(@builtin(global_invocation_id) global_id: vec3) { + let pixel_coordinates = vec2(global_id.xy); + let uv = vec2(pixel_coordinates) / view.viewport.zw; + + let edges0 = textureGather(0, depth_differences, point_clamp_sampler, uv); + let edges1 = textureGather(0, depth_differences, point_clamp_sampler, uv, vec2(2i, 0i)); + let edges2 = textureGather(0, depth_differences, point_clamp_sampler, uv, vec2(1i, 2i)); + let visibility0 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv); + let visibility1 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(2i, 0i)); + let visibility2 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(0i, 2i)); + let visibility3 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(2i, 2i)); + + let left_edges = unpack4x8unorm(edges0.x); + let right_edges = unpack4x8unorm(edges1.x); + let top_edges = unpack4x8unorm(edges0.z); + let bottom_edges = unpack4x8unorm(edges2.w); + var center_edges = unpack4x8unorm(edges0.y); + center_edges *= vec4(left_edges.y, right_edges.x, top_edges.w, bottom_edges.z); + + let center_weight = 1.2; + let left_weight = center_edges.x; + let right_weight = center_edges.y; + let top_weight = center_edges.z; + let bottom_weight = center_edges.w; + let top_left_weight = 0.425 * (top_weight * top_edges.x + left_weight * left_edges.z); + let top_right_weight = 0.425 * (top_weight * top_edges.y + right_weight * right_edges.z); + let bottom_left_weight = 0.425 * (bottom_weight * bottom_edges.x + left_weight * left_edges.w); + let bottom_right_weight = 0.425 * (bottom_weight * bottom_edges.y + right_weight * right_edges.w); + + let center_visibility = visibility0.y; + let left_visibility = visibility0.x; + let right_visibility = visibility0.z; + let top_visibility = visibility1.x; + let bottom_visibility = visibility2.z; + let top_left_visibility = visibility0.w; + let top_right_visibility = visibility1.w; + let bottom_left_visibility = visibility2.w; + let bottom_right_visibility = visibility3.w; + + var sum = center_visibility; + sum += left_visibility * left_weight; + sum += right_visibility * right_weight; + sum += top_visibility * top_weight; + sum += bottom_visibility * bottom_weight; + sum += top_left_visibility * top_left_weight; + sum += top_right_visibility * top_right_weight; + sum += bottom_left_visibility * bottom_left_weight; + sum += bottom_right_visibility * bottom_right_weight; + + var sum_weight = center_weight; + sum_weight += left_weight; + sum_weight += right_weight; + sum_weight += top_weight; + sum_weight += bottom_weight; + sum_weight += top_left_weight; + sum_weight += top_right_weight; + sum_weight += bottom_left_weight; + sum_weight += bottom_right_weight; + + let denoised_visibility = sum / sum_weight; + + textureStore(ambient_occlusion, pixel_coordinates, vec4(denoised_visibility, 0.0, 0.0, 0.0)); +} diff --git a/vendor/bevy_pbr/src/ssao/ssao.wgsl b/vendor/bevy_pbr/src/ssao/ssao.wgsl new file mode 100644 index 00000000..aa715be0 --- /dev/null +++ b/vendor/bevy_pbr/src/ssao/ssao.wgsl @@ -0,0 +1,204 @@ +// Visibility Bitmask Ambient Occlusion (VBAO) +// Paper: ttps://ar5iv.labs.arxiv.org/html/2301.11376 + +// Source code heavily based on XeGTAO v1.30 from Intel +// https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli + +// Source code based on the existing XeGTAO implementation and +// https://cdrinmatane.github.io/posts/ssaovb-code/ + +// Source code base on SSRT3 implementation +// https://github.com/cdrinmatane/SSRT3 + +#import bevy_render::maths::fast_acos + +#import bevy_render::{ + view::View, + globals::Globals, + maths::{PI, HALF_PI}, +} + +@group(0) @binding(0) var preprocessed_depth: texture_2d; +@group(0) @binding(1) var normals: texture_2d; +@group(0) @binding(2) var hilbert_index_lut: texture_2d; +#ifdef USE_R16FLOAT +@group(0) @binding(3) var ambient_occlusion: texture_storage_2d; +#else +@group(0) @binding(3) var ambient_occlusion: texture_storage_2d; +#endif +@group(0) @binding(4) var depth_differences: texture_storage_2d; +@group(0) @binding(5) var globals: Globals; +@group(0) @binding(6) var thickness: f32; +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + +fn load_noise(pixel_coordinates: vec2) -> vec2 { + var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; + +#ifdef TEMPORAL_JITTER + index += 288u * (globals.frame_count % 64u); +#endif + + // R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences + return fract(0.5 + f32(index) * vec2(0.75487766624669276005, 0.5698402909980532659114)); +} + +// Calculate differences in depth between neighbor pixels (later used by the spatial denoiser pass to preserve object edges) +fn calculate_neighboring_depth_differences(pixel_coordinates: vec2) -> f32 { + // Sample the pixel's depth and 4 depths around it + let uv = vec2(pixel_coordinates) / view.viewport.zw; + let depths_upper_left = textureGather(0, preprocessed_depth, point_clamp_sampler, uv); + let depths_bottom_right = textureGather(0, preprocessed_depth, point_clamp_sampler, uv, vec2(1i, 1i)); + let depth_center = depths_upper_left.y; + let depth_left = depths_upper_left.x; + let depth_top = depths_upper_left.z; + let depth_bottom = depths_bottom_right.x; + let depth_right = depths_bottom_right.z; + + // Calculate the depth differences (large differences represent object edges) + var edge_info = vec4(depth_left, depth_right, depth_top, depth_bottom) - depth_center; + let slope_left_right = (edge_info.y - edge_info.x) * 0.5; + let slope_top_bottom = (edge_info.w - edge_info.z) * 0.5; + let edge_info_slope_adjusted = edge_info + vec4(slope_left_right, -slope_left_right, slope_top_bottom, -slope_top_bottom); + edge_info = min(abs(edge_info), abs(edge_info_slope_adjusted)); + let bias = 0.25; // Using the bias and then saturating nudges the values a bit + let scale = depth_center * 0.011; // Weight the edges by their distance from the camera + edge_info = saturate((1.0 + bias) - edge_info / scale); // Apply the bias and scale, and invert edge_info so that small values become large, and vice versa + + // Pack the edge info into the texture + let edge_info_packed = vec4(pack4x8unorm(edge_info), 0u, 0u, 0u); + textureStore(depth_differences, pixel_coordinates, edge_info_packed); + + return depth_center; +} + +fn load_normal_view_space(uv: vec2) -> vec3 { + var world_normal = textureSampleLevel(normals, point_clamp_sampler, uv, 0.0).xyz; + world_normal = (world_normal * 2.0) - 1.0; + let view_from_world = mat3x3( + view.view_from_world[0].xyz, + view.view_from_world[1].xyz, + view.view_from_world[2].xyz, + ); + return view_from_world * world_normal; +} + +fn reconstruct_view_space_position(depth: f32, uv: vec2) -> vec3 { + let clip_xy = vec2(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y); + let t = view.view_from_clip * vec4(clip_xy, depth, 1.0); + let view_xyz = t.xyz / t.w; + return view_xyz; +} + +fn load_and_reconstruct_view_space_position(uv: vec2, sample_mip_level: f32) -> vec3 { + let depth = textureSampleLevel(preprocessed_depth, linear_clamp_sampler, uv, sample_mip_level).r; + return reconstruct_view_space_position(depth, uv); +} + +fn updateSectors( + min_horizon: f32, + max_horizon: f32, + samples_per_slice: f32, + bitmask: u32, +) -> u32 { + let start_horizon = u32(min_horizon * samples_per_slice); + let angle_horizon = u32(ceil((max_horizon - min_horizon) * samples_per_slice)); + + return insertBits(bitmask, 0xFFFFFFFFu, start_horizon, angle_horizon); +} + +fn processSample( + delta_position: vec3, + view_vec: vec3, + sampling_direction: f32, + n: vec2, + samples_per_slice: f32, + bitmask: ptr, +) { + let delta_position_back_face = delta_position - view_vec * thickness; + + var front_back_horizon = vec2( + fast_acos(dot(normalize(delta_position), view_vec)), + fast_acos(dot(normalize(delta_position_back_face), view_vec)), + ); + + front_back_horizon = saturate(fma(vec2(sampling_direction), -front_back_horizon, n)); + front_back_horizon = select(front_back_horizon.xy, front_back_horizon.yx, sampling_direction >= 0.0); + + *bitmask = updateSectors(front_back_horizon.x, front_back_horizon.y, samples_per_slice, *bitmask); +} + +@compute +@workgroup_size(8, 8, 1) +fn ssao(@builtin(global_invocation_id) global_id: vec3) { + let slice_count = f32(#SLICE_COUNT); + let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE); + let effect_radius = 0.5 * 1.457; + let falloff_range = 0.615 * effect_radius; + let falloff_from = effect_radius * (1.0 - 0.615); + let falloff_mul = -1.0 / falloff_range; + let falloff_add = falloff_from / falloff_range + 1.0; + + let pixel_coordinates = vec2(global_id.xy); + let uv = (vec2(pixel_coordinates) + 0.5) / view.viewport.zw; + + var pixel_depth = calculate_neighboring_depth_differences(pixel_coordinates); + pixel_depth += 0.00001; // Avoid depth precision issues + + let pixel_position = reconstruct_view_space_position(pixel_depth, uv); + let pixel_normal = load_normal_view_space(uv); + let view_vec = normalize(-pixel_position); + + let noise = load_noise(pixel_coordinates); + let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z; + + var visibility = 0.0; + var occluded_sample_count = 0u; + for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) { + let slice = slice_t + noise.x; + let phi = (PI / slice_count) * slice; + let omega = vec2(cos(phi), sin(phi)); + + let direction = vec3(omega.xy, 0.0); + let orthographic_direction = direction - (dot(direction, view_vec) * view_vec); + let axis = cross(direction, view_vec); + let projected_normal = pixel_normal - axis * dot(pixel_normal, axis); + let projected_normal_length = length(projected_normal); + + let sign_norm = sign(dot(orthographic_direction, projected_normal)); + let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length); + let n = vec2((HALF_PI - sign_norm * fast_acos(cos_norm)) * (1.0 / PI)); + + var bitmask = 0u; + + let sample_mul = vec2(omega.x, -omega.y) * sample_scale; + for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) { + var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482; + sample_noise = fract(noise.y + sample_noise); + + var s = (sample_t + sample_noise) / samples_per_slice_side; + s *= s; // https://github.com/GameTechDev/XeGTAO#sample-distribution + let sample = s * sample_mul; + + // * view.viewport.zw gets us from [0, 1] to [0, viewport_size], which is needed for this to get the correct mip levels + let sample_mip_level = clamp(log2(length(sample * view.viewport.zw)) - 3.3, 0.0, 5.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck + let sample_position_1 = load_and_reconstruct_view_space_position(uv + sample, sample_mip_level); + let sample_position_2 = load_and_reconstruct_view_space_position(uv - sample, sample_mip_level); + + let sample_difference_1 = sample_position_1 - pixel_position; + let sample_difference_2 = sample_position_2 - pixel_position; + + processSample(sample_difference_1, view_vec, -1.0, n, samples_per_slice_side * 2.0, &bitmask); + processSample(sample_difference_2, view_vec, 1.0, n, samples_per_slice_side * 2.0, &bitmask); + } + + occluded_sample_count += countOneBits(bitmask); + } + + visibility = 1.0 - f32(occluded_sample_count) / (slice_count * 2.0 * samples_per_slice_side); + + visibility = clamp(visibility, 0.03, 1.0); + + textureStore(ambient_occlusion, pixel_coordinates, vec4(visibility, 0.0, 0.0, 0.0)); +} diff --git a/vendor/bevy_pbr/src/ssao/ssao_utils.wgsl b/vendor/bevy_pbr/src/ssao/ssao_utils.wgsl new file mode 100644 index 00000000..be19fa66 --- /dev/null +++ b/vendor/bevy_pbr/src/ssao/ssao_utils.wgsl @@ -0,0 +1,13 @@ +#define_import_path bevy_pbr::ssao_utils + +#import bevy_render::maths::{PI, HALF_PI} + +// Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion +// https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78 +fn ssao_multibounce(visibility: f32, base_color: vec3) -> vec3 { + let a = 2.0404 * base_color - 0.3324; + let b = -4.7951 * base_color + 0.6417; + let c = 2.7552 * base_color + 0.6903; + let x = vec3(visibility); + return max(x, ((x * a + b) * x + c) * x); +} diff --git a/vendor/bevy_pbr/src/ssr/mod.rs b/vendor/bevy_pbr/src/ssr/mod.rs new file mode 100644 index 00000000..7bd9d02c --- /dev/null +++ b/vendor/bevy_pbr/src/ssr/mod.rs @@ -0,0 +1,592 @@ +//! Screen space reflections implemented via raymarching. + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::{ + core_3d::{ + graph::{Core3d, Node3d}, + DEPTH_TEXTURE_SAMPLING_SUPPORTED, + }, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + FullscreenShader, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::World, +}; +use bevy_image::BevyDefault as _; +use bevy_light::EnvironmentMapLight; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + diagnostic::RecordDiagnostics, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_graph::{ + NodeRunError, RenderGraph, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types, AddressMode, BindGroupEntries, BindGroupLayoutDescriptor, + BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, + DynamicUniformBuffer, FilterMode, FragmentState, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, + SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, TextureSampleType, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader}; +use bevy_utils::{once, prelude::default}; +use tracing::info; + +use crate::{ + binding_arrays_are_usable, graph::NodePbr, ExtractedAtmosphere, MeshPipelineViewLayoutKey, + MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, +}; + +/// Enables screen-space reflections for a camera. +/// +/// Screen-space reflections are currently only supported with deferred rendering. +pub struct ScreenSpaceReflectionsPlugin; + +/// Add this component to a camera to enable *screen-space reflections* (SSR). +/// +/// Screen-space reflections currently require deferred rendering in order to +/// appear. Therefore, they also need the [`DepthPrepass`] and [`DeferredPrepass`] +/// components, which are inserted automatically, +/// but deferred rendering itself is not automatically enabled. +/// +/// SSR currently performs no roughness filtering for glossy reflections, so +/// only very smooth surfaces will reflect objects in screen space. You can +/// adjust the `perceptual_roughness_threshold` in order to tune the threshold +/// below which screen-space reflections will be traced. +/// +/// As with all screen-space techniques, SSR can only reflect objects on screen. +/// When objects leave the camera, they will disappear from reflections. +/// An alternative that doesn't suffer from this problem is the combination of +/// a [`LightProbe`](bevy_light::LightProbe) and [`EnvironmentMapLight`]. The advantage of SSR is +/// that it can reflect all objects, not just static ones. +/// +/// SSR is an approximation technique and produces artifacts in some situations. +/// Hand-tuning the settings in this component will likely be useful. +/// +/// Screen-space reflections are presently unsupported on WebGL 2 because of a +/// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers, +/// which is required for screen-space raymarching. +#[derive(Clone, Copy, Component, Reflect)] +#[reflect(Component, Default, Clone)] +#[require(DepthPrepass, DeferredPrepass)] +#[doc(alias = "Ssr")] +pub struct ScreenSpaceReflections { + /// The maximum PBR roughness level that will enable screen space + /// reflections. + pub perceptual_roughness_threshold: f32, + + /// When marching the depth buffer, we only have 2.5D information and don't + /// know how thick surfaces are. We shall assume that the depth buffer + /// fragments are cuboids with a constant thickness defined by this + /// parameter. + pub thickness: f32, + + /// The number of steps to be taken at regular intervals to find an initial + /// intersection. Must not be zero. + /// + /// Higher values result in higher-quality reflections, because the + /// raymarching shader is less likely to miss objects. However, they take + /// more GPU time. + pub linear_steps: u32, + + /// Exponent to be applied in the linear part of the march. + /// + /// A value of 1.0 will result in equidistant steps, and higher values will + /// compress the earlier steps, and expand the later ones. This might be + /// desirable in order to get more detail close to objects. + /// + /// For optimal performance, this should be a small unsigned integer, such + /// as 1 or 2. + pub linear_march_exponent: f32, + + /// Number of steps in a bisection (binary search) to perform once the + /// linear search has found an intersection. Helps narrow down the hit, + /// increasing the chance of the secant method finding an accurate hit + /// point. + pub bisection_steps: u32, + + /// Approximate the root position using the secant method—by solving for + /// line-line intersection between the ray approach rate and the surface + /// gradient. + pub use_secant: bool, +} + +/// A version of [`ScreenSpaceReflections`] for upload to the GPU. +/// +/// For more information on these fields, see the corresponding documentation in +/// [`ScreenSpaceReflections`]. +#[derive(Clone, Copy, Component, ShaderType)] +pub struct ScreenSpaceReflectionsUniform { + perceptual_roughness_threshold: f32, + thickness: f32, + linear_steps: u32, + linear_march_exponent: f32, + bisection_steps: u32, + /// A boolean converted to a `u32`. + use_secant: u32, +} + +/// The node in the render graph that traces screen space reflections. +#[derive(Default)] +pub struct ScreenSpaceReflectionsNode; + +/// Identifies which screen space reflections render pipeline a view needs. +#[derive(Component, Deref, DerefMut)] +pub struct ScreenSpaceReflectionsPipelineId(pub CachedRenderPipelineId); + +/// Information relating to the render pipeline for the screen space reflections +/// shader. +#[derive(Resource)] +pub struct ScreenSpaceReflectionsPipeline { + mesh_view_layouts: MeshPipelineViewLayouts, + color_sampler: Sampler, + depth_linear_sampler: Sampler, + depth_nearest_sampler: Sampler, + bind_group_layout: BindGroupLayoutDescriptor, + binding_arrays_are_usable: bool, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, +} + +/// A GPU buffer that stores the screen space reflection settings for each view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ScreenSpaceReflectionsBuffer(pub DynamicUniformBuffer); + +/// A component that stores the offset within the +/// [`ScreenSpaceReflectionsBuffer`] for each view. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewScreenSpaceReflectionsUniformOffset(u32); + +/// Identifies a specific configuration of the SSR pipeline shader. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ScreenSpaceReflectionsPipelineKey { + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + is_hdr: bool, + has_environment_maps: bool, + has_atmosphere: bool, +} + +impl Plugin for ScreenSpaceReflectionsPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "ssr.wgsl"); + load_shader_library!(app, "raymarch.wgsl"); + + app.add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::>() + .add_systems( + RenderStartup, + ( + init_screen_space_reflections_pipeline, + add_screen_space_reflections_render_graph_edges, + ), + ) + .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare)) + .add_systems( + Render, + prepare_ssr_settings.in_set(RenderSystems::PrepareResources), + ) + // Note: we add this node here but then we add edges in + // `add_screen_space_reflections_render_graph_edges`. + .add_render_graph_node::>( + Core3d, + NodePbr::ScreenSpaceReflections, + ); + } +} + +fn add_screen_space_reflections_render_graph_edges(mut render_graph: ResMut) { + let subgraph = render_graph.sub_graph_mut(Core3d); + + subgraph.add_node_edge(NodePbr::ScreenSpaceReflections, Node3d::MainOpaquePass); + + if subgraph + .get_node_state(NodePbr::DeferredLightingPass) + .is_ok() + { + subgraph.add_node_edge( + NodePbr::DeferredLightingPass, + NodePbr::ScreenSpaceReflections, + ); + } +} + +impl Default for ScreenSpaceReflections { + // Reasonable default values. + // + // These are from + // . + fn default() -> Self { + Self { + perceptual_roughness_threshold: 0.1, + linear_steps: 16, + bisection_steps: 4, + use_secant: true, + thickness: 0.25, + linear_march_exponent: 1.0, + } + } +} + +impl ViewNode for ScreenSpaceReflectionsNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + view_bind_group, + ssr_pipeline_id, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + // Grab the render pipeline. + let pipeline_cache = world.resource::(); + let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + // Set up a standard pair of postprocessing textures. + let postprocess = view_target.post_process_write(); + + // Create the bind group for this view. + let ssr_pipeline = world.resource::(); + let ssr_bind_group = render_context.render_device().create_bind_group( + "SSR bind group", + &pipeline_cache.get_bind_group_layout(&ssr_pipeline.bind_group_layout), + &BindGroupEntries::sequential(( + postprocess.source, + &ssr_pipeline.color_sampler, + &ssr_pipeline.depth_linear_sampler, + &ssr_pipeline.depth_nearest_sampler, + )), + ); + + // Build the SSR render pass. + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("ssr"), + color_attachments: &[Some(RenderPassColorAttachment { + view: postprocess.destination, + depth_slice: None, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "ssr"); + + // Set bind groups. + render_pass.set_render_pipeline(render_pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]); + + // Perform the SSR render pass. + render_pass.set_bind_group(2, &ssr_bind_group, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +pub fn init_screen_space_reflections_pipeline( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + mesh_view_layouts: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + // Create the bind group layout. + let bind_group_layout = BindGroupLayoutDescriptor::new( + "SSR bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + binding_types::texture_2d(TextureSampleType::Float { filterable: true }), + binding_types::sampler(SamplerBindingType::Filtering), + binding_types::sampler(SamplerBindingType::Filtering), + binding_types::sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + + // Create the samplers we need. + + let color_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR color sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR depth linear sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR depth nearest sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + ..default() + }); + + commands.insert_resource(ScreenSpaceReflectionsPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + color_sampler, + depth_linear_sampler, + depth_nearest_sampler, + bind_group_layout, + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + fullscreen_shader: fullscreen_shader.clone(), + // Even though ssr was loaded using load_shader_library, we can still access it like a + // normal embedded asset (so we can use it as both a library or a kernel). + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "ssr.wgsl"), + }); +} + +/// Sets up screen space reflection pipelines for each applicable view. +pub fn prepare_ssr_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + ssr_pipeline: Res, + views: Query< + ( + Entity, + &ExtractedView, + Has>, + Has, + Has, + Has, + ), + ( + With, + With, + With, + ), + >, +) { + for ( + entity, + extracted_view, + has_environment_maps, + has_normal_prepass, + has_motion_vector_prepass, + has_atmosphere, + ) in &views + { + // SSR is only supported in the deferred pipeline, which has no MSAA + // support. Thus we can assume MSAA is off. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(Msaa::Off) + | MeshPipelineViewLayoutKey::DEPTH_PREPASS + | MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::NORMAL_PREPASS, + has_normal_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + has_motion_vector_prepass, + ); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::ATMOSPHERE, has_atmosphere); + + // Build the pipeline. + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &ssr_pipeline, + ScreenSpaceReflectionsPipelineKey { + mesh_pipeline_view_key, + is_hdr: extracted_view.hdr, + has_environment_maps, + has_atmosphere, + }, + ); + + // Note which pipeline ID was used. + commands + .entity(entity) + .insert(ScreenSpaceReflectionsPipelineId(pipeline_id)); + } +} + +/// Gathers up screen space reflection settings for each applicable view and +/// writes them into a GPU buffer. +pub fn prepare_ssr_settings( + mut commands: Commands, + views: Query<(Entity, Option<&ScreenSpaceReflectionsUniform>), With>, + mut ssr_settings_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = + ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue) + else { + return; + }; + + for (view, ssr_uniform) in views.iter() { + let uniform_offset = match ssr_uniform { + None => 0, + Some(ssr_uniform) => writer.write(ssr_uniform), + }; + commands + .entity(view) + .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset)); + } +} + +impl ExtractComponent for ScreenSpaceReflections { + type QueryData = Read; + + type QueryFilter = (); + + type Out = ScreenSpaceReflectionsUniform; + + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { + if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { + once!(info!( + "Disabling screen-space reflections on this platform because depth textures \ + aren't supported correctly" + )); + return None; + } + + Some((*settings).into()) + } +} + +impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { + type Key = ScreenSpaceReflectionsPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout.clone(), + ]; + + let mut shader_defs = vec![ + "DEPTH_PREPASS".into(), + "DEFERRED_PREPASS".into(), + "SCREEN_SPACE_REFLECTIONS".into(), + ]; + + if key.has_environment_maps { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + } + + if key.has_atmosphere { + shader_defs.push("ATMOSPHERE".into()); + } + + #[cfg(not(target_arch = "wasm32"))] + shader_defs.push("USE_DEPTH_SAMPLERS".into()); + + RenderPipelineDescriptor { + label: Some("SSR pipeline".into()), + layout, + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.is_hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +impl From for ScreenSpaceReflectionsUniform { + fn from(settings: ScreenSpaceReflections) -> Self { + Self { + perceptual_roughness_threshold: settings.perceptual_roughness_threshold, + thickness: settings.thickness, + linear_steps: settings.linear_steps, + linear_march_exponent: settings.linear_march_exponent, + bisection_steps: settings.bisection_steps, + use_secant: settings.use_secant as u32, + } + } +} diff --git a/vendor/bevy_pbr/src/ssr/raymarch.wgsl b/vendor/bevy_pbr/src/ssr/raymarch.wgsl new file mode 100644 index 00000000..20a76afa --- /dev/null +++ b/vendor/bevy_pbr/src/ssr/raymarch.wgsl @@ -0,0 +1,558 @@ +// Copyright (c) 2023 Tomasz Stachowiak +// +// This contribution is dual licensed under EITHER OF +// +// Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) +// MIT license (http://opensource.org/licenses/MIT) +// +// at your option. +// +// This is a port of the original [`raymarch.hlsl`] to WGSL. It's deliberately +// kept as close as possible so that patches to the original `raymarch.hlsl` +// have the greatest chances of applying to this version. +// +// [`raymarch.hlsl`]: +// https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db + +#define_import_path bevy_pbr::raymarch + +#import bevy_pbr::mesh_view_bindings::depth_prepass_texture +#import bevy_pbr::view_transformations::{ + direction_world_to_clip, + ndc_to_uv, + perspective_camera_near, + position_world_to_ndc, +} + +#ifdef USE_DEPTH_SAMPLERS +// Allows us to sample from the depth buffer with bilinear filtering. +@group(2) @binding(2) var depth_linear_sampler: sampler; + +// Allows us to sample from the depth buffer with nearest-neighbor filtering. +@group(2) @binding(3) var depth_nearest_sampler: sampler; +#endif + +// Manual depth fetch helpers used on WebGPU where depth + filtering sampler is invalid. +#ifndef USE_DEPTH_SAMPLERS +fn depth_texel_clamped(texel: vec2) -> f32 { + let dims = textureDimensions(depth_prepass_texture); + let max_coord = vec2(i32(dims.x) - 1, i32(dims.y) - 1); + let clamped = clamp(texel, vec2(0), max_coord); + return textureLoad(depth_prepass_texture, clamped, 0); +} + +fn depth_sample_nearest_clamped(uv: vec2, tex_size: vec2) -> f32 { + // Match nearest sampling by snapping to the closest texel center. + let coord = uv * tex_size - vec2(0.5); + return depth_texel_clamped(vec2(floor(coord + vec2(0.5)))); +} + +fn depth_sample_bilinear_clamped(uv: vec2, tex_size: vec2) -> f32 { + let coord = uv * tex_size - vec2(0.5); + let base = vec2(floor(coord)); + let frac = coord - floor(coord); + + let d00 = depth_texel_clamped(base); + let d10 = depth_texel_clamped(base + vec2(1, 0)); + let d01 = depth_texel_clamped(base + vec2(0, 1)); + let d11 = depth_texel_clamped(base + vec2(1, 1)); + + let d0 = mix(d00, d10, frac.x); + let d1 = mix(d01, d11, frac.x); + return mix(d0, d1, frac.y); +} +#endif + +fn depth_sample_linear(uv: vec2, tex_size: vec2) -> f32 { +#ifdef USE_DEPTH_SAMPLERS + return textureSampleLevel(depth_prepass_texture, depth_linear_sampler, uv, 0u); +#else + return depth_sample_bilinear_clamped(uv, tex_size); +#endif +} + +fn depth_sample_nearest(uv: vec2, tex_size: vec2) -> f32 { +#ifdef USE_DEPTH_SAMPLERS + return textureSampleLevel(depth_prepass_texture, depth_nearest_sampler, uv, 0u); +#else + return depth_sample_nearest_clamped(uv, tex_size); +#endif +} + +// Main code + +struct HybridRootFinder { + linear_steps: u32, + bisection_steps: u32, + use_secant: bool, + linear_march_exponent: f32, + + jitter: f32, + min_t: f32, + max_t: f32, +} + +fn hybrid_root_finder_new_with_linear_steps(v: u32) -> HybridRootFinder { + var res: HybridRootFinder; + res.linear_steps = v; + res.bisection_steps = 0u; + res.use_secant = false; + res.linear_march_exponent = 1.0; + res.jitter = 1.0; + res.min_t = 0.0; + res.max_t = 1.0; + return res; +} + +fn hybrid_root_finder_find_root( + root_finder: ptr, + start: vec3, + end: vec3, + distance_fn: ptr, + hit_t: ptr, + miss_t: ptr, + hit_d: ptr, +) -> bool { + let dir = end - start; + + var min_t = (*root_finder).min_t; + var max_t = (*root_finder).max_t; + + var min_d = DistanceWithPenetration(0.0, false, 0.0); + var max_d = DistanceWithPenetration(0.0, false, 0.0); + + let step_size = (max_t - min_t) / f32((*root_finder).linear_steps); + + var intersected = false; + + // + // Ray march using linear steps + + if ((*root_finder).linear_steps > 0u) { + let candidate_t = mix( + min_t, + max_t, + pow( + (*root_finder).jitter / f32((*root_finder).linear_steps), + (*root_finder).linear_march_exponent + ) + ); + + let candidate = start + dir * candidate_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + intersected = candidate_d.distance < 0.0 && candidate_d.valid; + + if (intersected) { + max_t = candidate_t; + max_d = candidate_d; + // The `[min_t .. max_t]` interval contains an intersection. End the linear search. + } else { + // No intersection yet. Carry on. + min_t = candidate_t; + min_d = candidate_d; + + for (var step = 1u; step < (*root_finder).linear_steps; step += 1u) { + let candidate_t = mix( + (*root_finder).min_t, + (*root_finder).max_t, + pow( + (f32(step) + (*root_finder).jitter) / f32((*root_finder).linear_steps), + (*root_finder).linear_march_exponent + ) + ); + + let candidate = start + dir * candidate_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + intersected = candidate_d.distance < 0.0 && candidate_d.valid; + + if (intersected) { + max_t = candidate_t; + max_d = candidate_d; + // The `[min_t .. max_t]` interval contains an intersection. + // End the linear search. + break; + } else { + // No intersection yet. Carry on. + min_t = candidate_t; + min_d = candidate_d; + } + } + } + } + + *miss_t = min_t; + *hit_t = min_t; + + // + // Refine the hit using bisection + + if (intersected) { + for (var step = 0u; step < (*root_finder).bisection_steps; step += 1u) { + let mid_t = (min_t + max_t) * 0.5; + let candidate = start + dir * mid_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + + if (candidate_d.distance < 0.0 && candidate_d.valid) { + // Intersection at the mid point. Refine the first half. + max_t = mid_t; + max_d = candidate_d; + } else { + // No intersection yet at the mid point. Refine the second half. + min_t = mid_t; + min_d = candidate_d; + } + } + + if ((*root_finder).use_secant) { + // Finish with one application of the secant method + let total_d = min_d.distance + -max_d.distance; + + let mid_t = mix(min_t, max_t, min_d.distance / total_d); + let candidate = start + dir * mid_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + + // Only accept the result of the secant method if it improves upon + // the previous result. + // + // Technically root_finder should be `abs(candidate_d.distance) < + // min(min_d.distance, -max_d.distance) * frac`, but root_finder seems + // sufficient. + if (abs(candidate_d.distance) < min_d.distance * 0.9 && candidate_d.valid) { + *hit_t = mid_t; + *hit_d = candidate_d; + } else { + *hit_t = max_t; + *hit_d = max_d; + } + + return true; + } else { + *hit_t = max_t; + *hit_d = max_d; + return true; + } + } else { + // Mark the conservative miss distance. + *hit_t = min_t; + return false; + } +} + +struct DistanceWithPenetration { + /// Distance to the surface of which a root we're trying to find + distance: f32, + + /// Whether to consider this sample valid for intersection. + /// Mostly relevant for allowing the ray marcher to travel behind surfaces, + /// as it will mark surfaces it travels under as invalid. + valid: bool, + + /// Conservative estimate of depth to which the ray penetrates the marched surface. + penetration: f32, +} + +struct DepthRaymarchDistanceFn { + depth_tex_size: vec2, + + march_behind_surfaces: bool, + depth_thickness: f32, + + use_sloppy_march: bool, +} + +fn depth_raymarch_distance_fn_evaluate( + distance_fn: ptr, + ray_point_cs: vec3, +) -> DistanceWithPenetration { + let interp_uv = ndc_to_uv(ray_point_cs.xy); + + let ray_depth = 1.0 / ray_point_cs.z; + + // We're using both point-sampled and bilinear-filtered values from the depth buffer. + // + // That's really stupid but works like magic. For samples taken near the ray origin, + // the discrete nature of the depth buffer becomes a problem. It's not a land of continuous surfaces, + // but a bunch of stacked duplo bricks. + // + // Technically we should be taking discrete steps in distance_fn duplo land, but then we're at the mercy + // of arbitrary quantization of our directions -- and sometimes we'll take a step which would + // claim that the ray is occluded -- even though the underlying smooth surface wouldn't occlude it. + // + // If we instead take linear taps from the depth buffer, we reconstruct the linear surface. + // That fixes acne, but introduces false shadowing near object boundaries, as we now pretend + // that everything is shrink-wrapped by distance_fn continuous 2.5D surface, and our depth thickness + // heuristic ends up falling apart. + // + // The fix is to consider both the smooth and the discrete surfaces, and only claim occlusion + // when the ray descends below both. + // + // The two approaches end up fixing each other's artifacts: + // * The false occlusions due to duplo land are rejected because the ray stays above the smooth surface. + // * The shrink-wrap surface is no longer continuous, so it's possible for rays to miss it. + + let linear_depth = 1.0 / depth_sample_linear(interp_uv, (*distance_fn).depth_tex_size); + let unfiltered_depth = 1.0 / depth_sample_nearest(interp_uv, (*distance_fn).depth_tex_size); + + var max_depth: f32; + var min_depth: f32; + + if ((*distance_fn).use_sloppy_march) { + max_depth = unfiltered_depth; + min_depth = unfiltered_depth; + } else { + max_depth = max(linear_depth, unfiltered_depth); + min_depth = min(linear_depth, unfiltered_depth); + } + + let bias = 0.000002; + + var res: DistanceWithPenetration; + res.distance = max_depth * (1.0 + bias) - ray_depth; + + // distance_fn will be used at the end of the ray march to potentially discard the hit. + res.penetration = ray_depth - min_depth; + + if ((*distance_fn).march_behind_surfaces) { + res.valid = res.penetration < (*distance_fn).depth_thickness; + } else { + res.valid = true; + } + + return res; +} + +struct DepthRayMarchResult { + /// True if the raymarch hit something. + hit: bool, + + /// In case of a hit, the normalized distance to it. + /// + /// In case of a miss, the furthest the ray managed to travel, which could either be + /// exceeding the max range, or getting behind a surface further than the depth thickness. + /// + /// Range: `0..=1` as a lerp factor over `ray_start_cs..=ray_end_cs`. + hit_t: f32, + + /// UV corresponding to `hit_t`. + hit_uv: vec2, + + /// The distance that the hit point penetrates into the hit surface. + /// Will normally be non-zero due to limited precision of the ray march. + /// + /// In case of a miss: undefined. + hit_penetration: f32, + + /// Ditto, within the range `0..DepthRayMarch::depth_thickness_linear_z` + /// + /// In case of a miss: undefined. + hit_penetration_frac: f32, +} + +struct DepthRayMarch { + /// Number of steps to be taken at regular intervals to find an initial intersection. + /// Must not be zero. + linear_steps: u32, + + /// Exponent to be applied in the linear part of the march. + /// + /// A value of 1.0 will result in equidistant steps, and higher values will compress + /// the earlier steps, and expand the later ones. This might be desirable in order + /// to get more detail close to objects in SSR or SSGI. + /// + /// For optimal performance, this should be a small compile-time unsigned integer, + /// such as 1 or 2. + linear_march_exponent: f32, + + /// Number of steps in a bisection (binary search) to perform once the linear search + /// has found an intersection. Helps narrow down the hit, increasing the chance of + /// the secant method finding an accurate hit point. + /// + /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows. + bisection_steps: u32, + + /// Approximate the root position using the secant method -- by solving for line-line + /// intersection between the ray approach rate and the surface gradient. + /// + /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows. + use_secant: bool, + + /// Jitter to apply to the first step of the linear search; 0..=1 range, mapping + /// to the extent of a single linear step in the first phase of the search. + /// Use 1.0 if you don't want jitter. + jitter: f32, + + /// Clip space coordinates (w=1) of the ray. + ray_start_cs: vec3, + ray_end_cs: vec3, + + /// Should be used for contact shadows, but not for any color bounce, e.g. SSR. + /// + /// For SSR etc. this can easily create leaks, but with contact shadows it allows the rays + /// to pass over invalid occlusions (due to thickness), and find potentially valid ones ahead. + /// + /// Note that this will cause the linear search to potentially miss surfaces, + /// because when the ray overshoots and ends up penetrating a surface further than + /// `depth_thickness_linear_z`, the ray marcher will just carry on. + /// + /// For this reason, this may require a lot of samples, or high depth thickness, + /// so that `depth_thickness_linear_z >= world space ray length / linear_steps`. + march_behind_surfaces: bool, + + /// If `true`, the ray marcher only performs nearest lookups of the depth buffer, + /// resulting in aliasing and false occlusion when marching tiny detail. + /// It should work fine for longer traces with fewer rays though. + use_sloppy_march: bool, + + /// When marching the depth buffer, we only have 2.5D information, and don't know how + /// thick surfaces are. We shall assume that the depth buffer fragments are little squares + /// with a constant thickness defined by this parameter. + depth_thickness_linear_z: f32, + + /// Size of the depth buffer we're marching in, in pixels. + depth_tex_size: vec2, +} + +fn depth_ray_march_new_from_depth(depth_tex_size: vec2) -> DepthRayMarch { + var res: DepthRayMarch; + res.jitter = 1.0; + res.linear_steps = 4u; + res.bisection_steps = 0u; + res.linear_march_exponent = 1.0; + res.depth_tex_size = depth_tex_size; + res.depth_thickness_linear_z = 1.0; + res.march_behind_surfaces = false; + res.use_sloppy_march = false; + return res; +} + +fn depth_ray_march_to_cs_dir_impl( + raymarch: ptr, + dir_cs: vec4, + infinite: bool, +) { + var end_cs = vec4((*raymarch).ray_start_cs, 1.0) + dir_cs; + + // Perform perspective division, but avoid dividing by zero for rays + // heading directly towards the eye. + end_cs /= select(-1.0, 1.0, end_cs.w >= 0.0) * max(1e-10, abs(end_cs.w)); + + // Clip ray start to the view frustum + var delta_cs = end_cs.xyz - (*raymarch).ray_start_cs; + let near_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs < vec3(0.0)); + let dist_to_near_edge = (near_edge - (*raymarch).ray_start_cs) / delta_cs; + let max_dist_to_near_edge = max(dist_to_near_edge.x, dist_to_near_edge.y); + (*raymarch).ray_start_cs += delta_cs * max(0.0, max_dist_to_near_edge); + + // Clip ray end to the view frustum + + delta_cs = end_cs.xyz - (*raymarch).ray_start_cs; + let far_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs >= vec3(0.0)); + let dist_to_far_edge = (far_edge - (*raymarch).ray_start_cs) / delta_cs; + let min_dist_to_far_edge = min( + min(dist_to_far_edge.x, dist_to_far_edge.y), + dist_to_far_edge.z + ); + + if (infinite) { + delta_cs *= min_dist_to_far_edge; + } else { + // If unbounded, would make the ray reach the end of the frustum + delta_cs *= min(1.0, min_dist_to_far_edge); + } + + (*raymarch).ray_end_cs = (*raymarch).ray_start_cs + delta_cs; +} + +/// March from a clip-space position (w = 1) +fn depth_ray_march_from_cs(raymarch: ptr, v: vec3) { + (*raymarch).ray_start_cs = v; +} + +/// March to a clip-space position (w = 1) +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_cs(raymarch: ptr, end_cs: vec3) { + let dir = vec4(end_cs - (*raymarch).ray_start_cs, 0.0) * sign(end_cs.z); + depth_ray_march_to_cs_dir_impl(raymarch, dir, false); +} + +/// March towards a clip-space direction. Infinite (ray is extended to cover the whole view frustum). +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_cs_dir(raymarch: ptr, dir: vec4) { + depth_ray_march_to_cs_dir_impl(raymarch, dir, true); +} + +/// March to a world-space position. +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_ws(raymarch: ptr, end: vec3) { + depth_ray_march_to_cs(raymarch, position_world_to_ndc(end)); +} + +/// March towards a world-space direction. Infinite (ray is extended to cover the whole view frustum). +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_ws_dir(raymarch: ptr, dir: vec3) { + depth_ray_march_to_cs_dir_impl(raymarch, direction_world_to_clip(dir), true); +} + +/// Perform the ray march. +fn depth_ray_march_march(raymarch: ptr) -> DepthRayMarchResult { + var res = DepthRayMarchResult(false, 0.0, vec2(0.0), 0.0, 0.0); + + let ray_start_uv = ndc_to_uv((*raymarch).ray_start_cs.xy); + let ray_end_uv = ndc_to_uv((*raymarch).ray_end_cs.xy); + + let ray_uv_delta = ray_end_uv - ray_start_uv; + let ray_len_px = ray_uv_delta * (*raymarch).depth_tex_size; + + let min_px_per_step = 1u; + let step_count = max( + 2, + min(i32((*raymarch).linear_steps), i32(floor(length(ray_len_px) / f32(min_px_per_step)))) + ); + + let linear_z_to_scaled_linear_z = 1.0 / perspective_camera_near(); + let depth_thickness = (*raymarch).depth_thickness_linear_z * linear_z_to_scaled_linear_z; + + var distance_fn: DepthRaymarchDistanceFn; + distance_fn.depth_tex_size = (*raymarch).depth_tex_size; + distance_fn.march_behind_surfaces = (*raymarch).march_behind_surfaces; + distance_fn.depth_thickness = depth_thickness; + distance_fn.use_sloppy_march = (*raymarch).use_sloppy_march; + + var hit: DistanceWithPenetration; + + var hit_t = 0.0; + var miss_t = 0.0; + var root_finder = hybrid_root_finder_new_with_linear_steps(u32(step_count)); + root_finder.bisection_steps = (*raymarch).bisection_steps; + root_finder.use_secant = (*raymarch).use_secant; + root_finder.linear_march_exponent = (*raymarch).linear_march_exponent; + root_finder.jitter = (*raymarch).jitter; + let intersected = hybrid_root_finder_find_root( + &root_finder, + (*raymarch).ray_start_cs, + (*raymarch).ray_end_cs, + &distance_fn, + &hit_t, + &miss_t, + &hit + ); + + res.hit_t = hit_t; + + if (intersected && hit.penetration < depth_thickness && hit.distance < depth_thickness) { + res.hit = true; + res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t); + res.hit_penetration = hit.penetration / linear_z_to_scaled_linear_z; + res.hit_penetration_frac = hit.penetration / depth_thickness; + return res; + } + + res.hit_t = miss_t; + res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t); + + return res; +} diff --git a/vendor/bevy_pbr/src/ssr/ssr.wgsl b/vendor/bevy_pbr/src/ssr/ssr.wgsl new file mode 100644 index 00000000..d646ac69 --- /dev/null +++ b/vendor/bevy_pbr/src/ssr/ssr.wgsl @@ -0,0 +1,194 @@ +// A postprocessing pass that performs screen-space reflections. + +#define_import_path bevy_pbr::ssr + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::{ + clustered_forward, + lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, + mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings}, + pbr_deferred_functions::pbr_input_from_deferred_gbuffer, + pbr_deferred_types, + pbr_functions, + prepass_utils, + raymarch::{ + depth_ray_march_from_cs, + depth_ray_march_march, + depth_ray_march_new_from_depth, + depth_ray_march_to_ws_dir, + }, + utils, + view_transformations::{ + depth_ndc_to_view_z, + frag_coord_to_ndc, + ndc_to_frag_coord, + ndc_to_uv, + position_view_to_ndc, + position_world_to_ndc, + position_world_to_view, + }, +} +#import bevy_render::view::View + +#ifdef ENVIRONMENT_MAP +#import bevy_pbr::environment_map +#endif + +// The texture representing the color framebuffer. +@group(2) @binding(0) var color_texture: texture_2d; + +// The sampler that lets us sample from the color framebuffer. +@group(2) @binding(1) var color_sampler: sampler; + +// Group 1, bindings 2 and 3 are in `raymarch.wgsl`. + +// Returns the reflected color in the RGB channel and the specular occlusion in +// the alpha channel. +// +// The general approach here is similar to [1]. We first project the reflection +// ray into screen space. Then we perform uniform steps along that screen-space +// reflected ray, converting each step to view space. +// +// The arguments are: +// +// * `R_world`: The reflection vector in world space. +// +// * `P_world`: The current position in world space. +// +// [1]: https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html +fn evaluate_ssr(R_world: vec3, P_world: vec3) -> vec4 { + let depth_size = vec2(textureDimensions(depth_prepass_texture)); + + var raymarch = depth_ray_march_new_from_depth(depth_size); + depth_ray_march_from_cs(&raymarch, position_world_to_ndc(P_world)); + depth_ray_march_to_ws_dir(&raymarch, normalize(R_world)); + raymarch.linear_steps = ssr_settings.linear_steps; + raymarch.bisection_steps = ssr_settings.bisection_steps; + raymarch.use_secant = ssr_settings.use_secant != 0u; + raymarch.depth_thickness_linear_z = ssr_settings.thickness; + raymarch.jitter = 1.0; // Disable jitter for now. + raymarch.march_behind_surfaces = false; + + let raymarch_result = depth_ray_march_march(&raymarch); + if (raymarch_result.hit) { + return vec4( + textureSampleLevel(color_texture, color_sampler, raymarch_result.hit_uv, 0.0).rgb, + 0.0 + ); + } + + return vec4(0.0, 0.0, 0.0, 1.0); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Sample the depth. + var frag_coord = in.position; + frag_coord.z = prepass_utils::prepass_depth(in.position, 0u); + + // Load the G-buffer data. + let fragment = textureLoad(color_texture, vec2(frag_coord.xy), 0); + let gbuffer = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, gbuffer); + + // Don't do anything if the surface is too rough, since we can't blur or do + // temporal accumulation yet. + let perceptual_roughness = pbr_input.material.perceptual_roughness; + if (perceptual_roughness > ssr_settings.perceptual_roughness_threshold) { + return fragment; + } + + // Unpack the PBR input. + var specular_occlusion = pbr_input.specular_occlusion; + let world_position = pbr_input.world_position.xyz; + let N = pbr_input.N; + let V = pbr_input.V; + + // Calculate the reflection vector. + let R = reflect(-V, N); + + // Do the raymarching. + let ssr_specular = evaluate_ssr(R, world_position); + var indirect_light = ssr_specular.rgb; + specular_occlusion *= ssr_specular.a; + + // Sample the environment map if necessary. + // + // This will take the specular part of the environment map into account if + // the ray missed. Otherwise, it only takes the diffuse part. + // + // TODO: Merge this with the duplicated code in `apply_pbr_lighting`. +#ifdef ENVIRONMENT_MAP + // Unpack values required for environment mapping. + let base_color = pbr_input.material.base_color.rgb; + let metallic = pbr_input.material.metallic; + let reflectance = pbr_input.material.reflectance; + let specular_transmission = pbr_input.material.specular_transmission; + let diffuse_transmission = pbr_input.material.diffuse_transmission; + let diffuse_occlusion = pbr_input.diffuse_occlusion; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = pbr_input.material.clearcoat; + let clearcoat_perceptual_roughness = pbr_input.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = pbr_input.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, pbr_input.V), 0.0001); + let clearcoat_R = reflect(-pbr_input.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Calculate various other values needed for environment mapping. + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let diffuse_color = pbr_functions::calculate_diffuse_color( + base_color, + metallic, + specular_transmission, + diffuse_transmission + ); + let NdotV = max(dot(N, V), 0.0001); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); + let F0 = pbr_functions::calculate_F0(base_color, metallic, reflectance); + + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = world_position.xyz; + lighting_input.V = V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Determine which cluster we're in. We'll need this to find the right + // reflection probe. + let cluster_index = clustered_forward::fragment_cluster_index( + frag_coord.xy, frag_coord.z, false); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + + // Sample the environment map. + let environment_light = environment_map::environment_map_light( + &lighting_input, &clusterable_object_index_ranges, false); + + // Accumulate the environment map light. + indirect_light += view.exposure * + (environment_light.diffuse * diffuse_occlusion + + environment_light.specular * specular_occlusion); +#endif + + // Write the results. + return vec4(fragment.rgb + indirect_light, 1.0); +} diff --git a/vendor/bevy_pbr/src/volumetric_fog/mod.rs b/vendor/bevy_pbr/src/volumetric_fog/mod.rs new file mode 100644 index 00000000..57cbd7bb --- /dev/null +++ b/vendor/bevy_pbr/src/volumetric_fog/mod.rs @@ -0,0 +1,114 @@ +//! Volumetric fog and volumetric lighting, also known as light shafts or god +//! rays. +//! +//! This module implements a more physically-accurate, but slower, form of fog +//! than the [`crate::fog`] module does. Notably, this *volumetric fog* allows +//! for light beams from directional lights to shine through, creating what is +//! known as *light shafts* or *god rays*. +//! +//! To add volumetric fog to a scene, add [`bevy_light::VolumetricFog`] to the +//! camera, and add [`bevy_light::VolumetricLight`] to directional lights that you wish to +//! be volumetric. [`bevy_light::VolumetricFog`] feature numerous settings that +//! allow you to define the accuracy of the simulation, as well as the look of +//! the fog. Currently, only interaction with directional lights that have +//! shadow maps is supported. Note that the overhead of the effect scales +//! directly with the number of directional lights in use, so apply +//! [`bevy_light::VolumetricLight`] sparingly for the best results. +//! +//! The overall algorithm, which is implemented as a postprocessing effect, is a +//! combination of the techniques described in [Scratchapixel] and [this blog +//! post]. It uses raymarching in screen space, transformed into shadow map +//! space for sampling and combined with physically-based modeling of absorption +//! and scattering. Bevy employs the widely-used [Henyey-Greenstein phase +//! function] to model asymmetry; this essentially allows light shafts to fade +//! into and out of existence as the user views them. +//! +//! [Scratchapixel]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +//! +//! [this blog post]: https://www.alexandre-pestana.com/volumetric-lights/ +//! +//! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction + +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, Assets, Handle}; +use bevy_core_pipeline::core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, +}; +use bevy_ecs::{resource::Resource, schedule::IntoScheduleConfigs as _}; +use bevy_light::FogVolume; +use bevy_math::{ + primitives::{Cuboid, Plane3d}, + Vec2, Vec3, +}; +use bevy_mesh::{Mesh, Meshable}; +use bevy_render::{ + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::SpecializedRenderPipelines, + sync_component::SyncComponentPlugin, + ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, +}; +use render::{VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer}; + +use crate::{graph::NodePbr, volumetric_fog::render::init_volumetric_fog_pipeline}; + +pub mod render; + +/// A plugin that implements volumetric fog. +pub struct VolumetricFogPlugin; + +#[derive(Resource)] +pub struct FogAssets { + plane_mesh: Handle, + cube_mesh: Handle, +} + +impl Plugin for VolumetricFogPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "volumetric_fog.wgsl"); + + let mut meshes = app.world_mut().resource_mut::>(); + let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh()); + let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()); + + app.add_plugins(SyncComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .insert_resource(FogAssets { + plane_mesh, + cube_mesh, + }) + .init_resource::>() + .init_resource::() + .add_systems(RenderStartup, init_volumetric_fog_pipeline) + .add_systems(ExtractSchedule, render::extract_volumetric_fog) + .add_systems( + Render, + ( + render::prepare_volumetric_fog_pipelines.in_set(RenderSystems::Prepare), + render::prepare_volumetric_fog_uniforms.in_set(RenderSystems::Prepare), + render::prepare_view_depth_textures_for_volumetric_fog + .in_set(RenderSystems::Prepare) + .before(prepare_core_3d_depth_textures), + ), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::VolumetricFog, + ) + .add_render_graph_edges( + Core3d, + // Volumetric fog should run after the main pass but before bloom, so + // we order if at the start of post processing. + ( + Node3d::EndMainPass, + NodePbr::VolumetricFog, + Node3d::StartMainPassPostProcessing, + ), + ); + } +} diff --git a/vendor/bevy_pbr/src/volumetric_fog/render.rs b/vendor/bevy_pbr/src/volumetric_fog/render.rs new file mode 100644 index 00000000..e0ddc3cc --- /dev/null +++ b/vendor/bevy_pbr/src/volumetric_fog/render.rs @@ -0,0 +1,892 @@ +//! Rendering of fog volumes. + +use core::array; + +use bevy_asset::{load_embedded_asset, AssetId, AssetServer, Handle}; +use bevy_camera::Camera3d; +use bevy_color::ColorToComponents as _; +use bevy_core_pipeline::prepass::{ + DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + resource::Resource, + system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut}, + world::World, +}; +use bevy_image::{BevyDefault, Image}; +use bevy_light::{FogVolume, VolumetricFog, VolumetricLight}; +use bevy_math::{vec4, Affine3A, Mat4, Vec3, Vec3A, Vec4}; +use bevy_mesh::{Mesh, MeshVertexBufferLayoutRef}; +use bevy_render::{ + diagnostic::RecordDiagnostics, + mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo}, + render_asset::RenderAssets, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + sampler, texture_3d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroupLayoutDescriptor, BindGroupLayoutEntries, BindingResource, BlendComponent, + BlendFactor, BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, + ColorWrites, DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, + Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, SamplerBindingType, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, + TextureSampleType, TextureUsages, VertexState, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + texture::GpuImage, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + Extract, +}; +use bevy_shader::Shader; +use bevy_transform::components::GlobalTransform; +use bevy_utils::prelude::default; +use bitflags::bitflags; + +use crate::{ + ExtractedAtmosphere, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, +}; + +use super::FogAssets; + +bitflags! { + /// Flags that describe the bind group layout used to render volumetric fog. + #[derive(Clone, Copy, PartialEq)] + struct VolumetricFogBindGroupLayoutKey: u8 { + /// The framebuffer is multisampled. + const MULTISAMPLED = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +bitflags! { + /// Flags that describe the rasterization pipeline used to render volumetric + /// fog. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + struct VolumetricFogPipelineKeyFlags: u8 { + /// The view's color format has high dynamic range. + const HDR = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +/// The total number of bind group layouts. +/// +/// This is the total number of combinations of all +/// [`VolumetricFogBindGroupLayoutKey`] flags. +const VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT: usize = + VolumetricFogBindGroupLayoutKey::all().bits() as usize + 1; + +/// A matrix that converts from local 1×1×1 space to UVW 3D density texture +/// space. +static UVW_FROM_LOCAL: Mat4 = Mat4::from_cols( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.5, 0.5, 0.5, 1.0), +); + +/// The GPU pipeline for the volumetric fog postprocessing effect. +#[derive(Resource)] +pub struct VolumetricFogPipeline { + /// A reference to the shared set of mesh pipeline view layouts. + mesh_view_layouts: MeshPipelineViewLayouts, + + /// All bind group layouts. + /// + /// Since there aren't too many of these, we precompile them all. + volumetric_view_bind_group_layouts: + [BindGroupLayoutDescriptor; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT], + + // The shader asset handle. + shader: Handle, +} + +/// The two render pipelines that we use for fog volumes: one for when a 3D +/// density texture is present and one for when it isn't. +#[derive(Component)] +pub struct ViewVolumetricFogPipelines { + /// The render pipeline that we use when no density texture is present, and + /// the density distribution is uniform. + pub textureless: CachedRenderPipelineId, + /// The render pipeline that we use when a density texture is present. + pub textured: CachedRenderPipelineId, +} + +/// The node in the render graph, part of the postprocessing stack, that +/// implements volumetric fog. +#[derive(Default)] +pub struct VolumetricFogNode; + +/// Identifies a single specialization of the volumetric fog shader. +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct VolumetricFogPipelineKey { + /// The layout of the view, which is needed for the raymarching. + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + + /// The vertex buffer layout of the primitive. + /// + /// Both planes (used when the camera is inside the fog volume) and cubes + /// (used when the camera is outside the fog volume) use identical vertex + /// buffer layouts, so we only need one of them. + vertex_buffer_layout: MeshVertexBufferLayoutRef, + + /// Flags that specify features on the pipeline key. + flags: VolumetricFogPipelineKeyFlags, +} + +/// The same as [`VolumetricFog`] and [`FogVolume`], but formatted for +/// the GPU. +/// +/// See the documentation of those structures for more information on these +/// fields. +#[derive(ShaderType)] +pub struct VolumetricFogUniform { + clip_from_local: Mat4, + + /// The transform from world space to 3D density texture UVW space. + uvw_from_world: Mat4, + + /// View-space plane equations of the far faces of the fog volume cuboid. + /// + /// The vector takes the form V = (N, -N⋅Q), where N is the normal of the + /// plane and Q is any point in it, in view space. The equation of the plane + /// for homogeneous point P = (Px, Py, Pz, Pw) is V⋅P = 0. + far_planes: [Vec4; 3], + + fog_color: Vec3, + light_tint: Vec3, + ambient_color: Vec3, + ambient_intensity: f32, + step_count: u32, + + /// The radius of a sphere that bounds the fog volume in view space. + bounding_radius: f32, + + absorption: f32, + scattering: f32, + density: f32, + density_texture_offset: Vec3, + scattering_asymmetry: f32, + light_intensity: f32, + jitter_strength: f32, +} + +/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the +/// [`VolumetricFogUniform`] for a specific view. +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFog(Vec); + +/// Information that the render world needs to maintain about each fog volume. +pub struct ViewFogVolume { + /// The 3D voxel density texture for this volume, if present. + density_texture: Option>, + /// The offset of this view's [`VolumetricFogUniform`] structure within the + /// [`VolumetricFogUniformBuffer`]. + uniform_buffer_offset: u32, + /// True if the camera is outside the fog volume; false if it's inside the + /// fog volume. + exterior: bool, +} + +/// The GPU buffer that stores the [`VolumetricFogUniform`] data. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); + +pub fn init_volumetric_fog_pipeline( + mut commands: Commands, + mesh_view_layouts: Res, + asset_server: Res, +) { + // Create the bind group layout entries common to all bind group + // layouts. + let base_bind_group_layout_entries = &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + // `volumetric_fog` + uniform_buffer::(true), + ); + + // For every combination of `VolumetricFogBindGroupLayoutKey` bits, + // create a bind group layout. + let bind_group_layouts = array::from_fn(|bits| { + let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8); + + let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec(); + + // `depth_texture` + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + (( + 1, + if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + ),), + )); + + // `density_texture` and `density_sampler` + if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) { + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (2, texture_3d(TextureSampleType::Float { filterable: true })), + (3, sampler(SamplerBindingType::Filtering)), + ), + )); + } + + // Create the bind group layout. + let description = flags.bind_group_layout_description(); + BindGroupLayoutDescriptor::new(description, &bind_group_layout_entries) + }); + + commands.insert_resource(VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layouts: bind_group_layouts, + shader: load_embedded_asset!(asset_server.as_ref(), "volumetric_fog.wgsl"), + }); +} + +/// Extracts [`VolumetricFog`], [`FogVolume`], and [`VolumetricLight`]s +/// from the main world to the render world. +pub fn extract_volumetric_fog( + mut commands: Commands, + view_targets: Extract>, + fog_volumes: Extract>, + volumetric_lights: Extract>, +) { + if volumetric_lights.is_empty() { + // TODO: needs better way to handle clean up in render world + for (entity, ..) in view_targets.iter() { + commands + .entity(entity) + .remove::<(VolumetricFog, ViewVolumetricFogPipelines, ViewVolumetricFog)>(); + } + for (entity, ..) in fog_volumes.iter() { + commands.entity(entity).remove::(); + } + return; + } + + for (entity, volumetric_fog) in view_targets.iter() { + commands + .get_entity(entity) + .expect("Volumetric fog entity wasn't synced.") + .insert(*volumetric_fog); + } + + for (entity, fog_volume, fog_transform) in fog_volumes.iter() { + commands + .get_entity(entity) + .expect("Fog volume entity wasn't synced.") + .insert((*fog_volume).clone()) + .insert(*fog_transform); + } + + for (entity, volumetric_light) in volumetric_lights.iter() { + commands + .get_entity(entity) + .expect("Volumetric light entity wasn't synced.") + .insert(*volumetric_light); + } +} + +impl ViewNode for VolumetricFogNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_depth_texture, + view_volumetric_lighting_pipelines, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_fog_volumes, + view_bind_group, + view_ssr_offset, + msaa, + view_environment_map_offset, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let volumetric_lighting_pipeline = world.resource::(); + let volumetric_lighting_uniform_buffers = world.resource::(); + let image_assets = world.resource::>(); + let mesh_allocator = world.resource::(); + + // Fetch the uniform buffer and binding. + let ( + Some(textureless_pipeline), + Some(textured_pipeline), + Some(volumetric_lighting_uniform_buffer_binding), + ) = ( + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textureless), + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textured), + volumetric_lighting_uniform_buffers.binding(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + render_context + .command_encoder() + .push_debug_group("volumetric_lighting"); + let time_span = + diagnostics.time_span(render_context.command_encoder(), "volumetric_lighting"); + + let fog_assets = world.resource::(); + let render_meshes = world.resource::>(); + + for view_fog_volume in view_fog_volumes.iter() { + // If the camera is outside the fog volume, pick the cube mesh; + // otherwise, pick the plane mesh. In the latter case we'll be + // effectively rendering a full-screen quad. + let mesh_handle = if view_fog_volume.exterior { + fog_assets.cube_mesh.clone() + } else { + fog_assets.plane_mesh.clone() + }; + + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id()) + else { + continue; + }; + + let density_image = view_fog_volume + .density_texture + .and_then(|density_texture| image_assets.get(density_texture)); + + // Pick the right pipeline, depending on whether a density texture + // is present or not. + let pipeline = if density_image.is_some() { + textured_pipeline + } else { + textureless_pipeline + }; + + // This should always succeed, but if the asset was unloaded don't + // panic. + let Some(render_mesh) = render_meshes.get(&mesh_handle) else { + return Ok(()); + }; + + // Create the bind group for the view. + // + // TODO: Cache this. + + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + !matches!(*msaa, Msaa::Off), + ); + + // Create the bind group entries. The ones relating to the density + // texture will only be filled in if that texture is present. + let mut bind_group_entries = DynamicBindGroupEntries::sequential(( + volumetric_lighting_uniform_buffer_binding.clone(), + BindingResource::TextureView(view_depth_texture.view()), + )); + if let Some(density_image) = density_image { + bind_group_layout_key.insert(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE); + bind_group_entries = bind_group_entries.extend_sequential(( + BindingResource::TextureView(&density_image.texture_view), + BindingResource::Sampler(&density_image.sampler), + )); + } + + let volumetric_view_bind_group_layout = &volumetric_lighting_pipeline + .volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize]; + + let volumetric_view_bind_group = render_context.render_device().create_bind_group( + None, + &pipeline_cache.get_bind_group_layout(volumetric_view_bind_group_layout), + &bind_group_entries, + ); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("volumetric lighting pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: view_target.main_texture_view(), + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + + render_pass.set_vertex_buffer(0, *vertex_buffer_slice.buffer.slice(..)); + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group( + 1, + &volumetric_view_bind_group, + &[view_fog_volume.uniform_buffer_offset], + ); + + // Draw elements or arrays, as appropriate. + match &render_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = + mesh_allocator.mesh_index_slice(&mesh_handle.id()) + else { + continue; + }; + + render_pass + .set_index_buffer(*index_buffer_slice.buffer.slice(..), *index_format); + render_pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + 0..1, + ); + } + RenderMeshBufferInfo::NonIndexed => { + render_pass.draw(vertex_buffer_slice.range, 0..1); + } + } + } + + time_span.end(render_context.command_encoder()); + render_context.command_encoder().pop_debug_group(); + + Ok(()) + } +} + +impl SpecializedRenderPipeline for VolumetricFogPipeline { + type Key = VolumetricFogPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + // We always use hardware 2x2 filtering for sampling the shadow map; the + // more accurate versions with percentage-closer filtering aren't worth + // the overhead. + let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; + + // We need a separate layout for MSAA and non-MSAA, as well as one for + // the presence or absence of the density texture. + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + key.mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED), + ); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE, + key.flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE), + ); + + let volumetric_view_bind_group_layout = + self.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize].clone(); + + // Both the cube and plane have the same vertex layout, so we don't need + // to distinguish between the two. + let vertex_format = key + .vertex_buffer_layout + .0 + .get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)]) + .expect("Failed to get vertex layout for volumetric fog hull"); + + if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + } + + if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::ATMOSPHERE) + { + shader_defs.push("ATMOSPHERE".into()); + } + + if key + .flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE) + { + shader_defs.push("DENSITY_TEXTURE".into()); + } + + let layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + volumetric_view_bind_group_layout.clone(), + ]; + + RenderPipelineDescriptor { + label: Some("volumetric lighting pipeline".into()), + layout, + vertex: VertexState { + shader: self.shader.clone(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_format], + ..default() + }, + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..default() + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + // Blend on top of what's already in the framebuffer. Doing + // the alpha blending with the hardware blender allows us to + // avoid having to use intermediate render targets. + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::Zero, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +/// Specializes volumetric fog pipelines for all views with that effect enabled. +pub fn prepare_volumetric_fog_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + volumetric_lighting_pipeline: Res, + fog_assets: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + &Msaa, + Has, + Has, + Has, + Has, + Has, + ), + With, + >, + meshes: Res>, +) { + let Some(plane_mesh) = meshes.get(&fog_assets.plane_mesh) else { + // There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use. + return; + }; + + for ( + entity, + view, + msaa, + normal_prepass, + depth_prepass, + motion_vector_prepass, + deferred_prepass, + atmosphere, + ) in view_targets.iter() + { + // Create a mesh pipeline view layout key corresponding to the view. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + motion_vector_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::DEFERRED_PREPASS, + deferred_prepass, + ); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::ATMOSPHERE, atmosphere); + + let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty(); + textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr); + + // Specialize the pipeline. + let textureless_pipeline_key = VolumetricFogPipelineKey { + mesh_pipeline_view_key, + vertex_buffer_layout: plane_mesh.layout.clone(), + flags: textureless_flags, + }; + let textureless_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + textureless_pipeline_key.clone(), + ); + let textured_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + VolumetricFogPipelineKey { + flags: textureless_pipeline_key.flags + | VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE, + ..textureless_pipeline_key + }, + ); + + commands.entity(entity).insert(ViewVolumetricFogPipelines { + textureless: textureless_pipeline_id, + textured: textured_pipeline_id, + }); + } +} + +/// A system that converts [`VolumetricFog`] into [`VolumetricFogUniform`]s. +pub fn prepare_volumetric_fog_uniforms( + mut commands: Commands, + mut volumetric_lighting_uniform_buffer: ResMut, + view_targets: Query<(Entity, &ExtractedView, &VolumetricFog)>, + fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>, + render_device: Res, + render_queue: Res, + mut local_from_world_matrices: Local>, +) { + // Do this up front to avoid O(n^2) matrix inversion. + local_from_world_matrices.clear(); + for (_, _, fog_transform) in fog_volumes.iter() { + local_from_world_matrices.push(fog_transform.affine().inverse()); + } + + let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); + + let Some(mut writer) = + volumetric_lighting_uniform_buffer.get_writer(uniform_count, &render_device, &render_queue) + else { + return; + }; + + for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { + let world_from_view = extracted_view.world_from_view.affine(); + + let mut view_fog_volumes = vec![]; + + for ((_, fog_volume, _), local_from_world) in + fog_volumes.iter().zip(local_from_world_matrices.iter()) + { + // Calculate the transforms to and from 1×1×1 local space. + let local_from_view = *local_from_world * world_from_view; + let view_from_local = local_from_view.inverse(); + + // Determine whether the camera is inside or outside the volume, and + // calculate the clip space transform. + let interior = camera_is_inside_fog_volume(&local_from_view); + let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms( + interior, + &extracted_view.clip_from_view, + &view_from_local, + ); + + // Calculate the radius of the sphere that bounds the fog volume. + let bounding_radius = view_from_local + .transform_vector3a(Vec3A::splat(0.5)) + .length(); + + // Write out our uniform. + let uniform_buffer_offset = writer.write(&VolumetricFogUniform { + clip_from_local: hull_clip_from_local, + uvw_from_world: UVW_FROM_LOCAL * *local_from_world, + far_planes: get_far_planes(&view_from_local), + fog_color: fog_volume.fog_color.to_linear().to_vec3(), + light_tint: fog_volume.light_tint.to_linear().to_vec3(), + ambient_color: volumetric_fog.ambient_color.to_linear().to_vec3(), + ambient_intensity: volumetric_fog.ambient_intensity, + step_count: volumetric_fog.step_count, + bounding_radius, + absorption: fog_volume.absorption, + scattering: fog_volume.scattering, + density: fog_volume.density_factor, + density_texture_offset: fog_volume.density_texture_offset, + scattering_asymmetry: fog_volume.scattering_asymmetry, + light_intensity: fog_volume.light_intensity, + jitter_strength: volumetric_fog.jitter, + }); + + view_fog_volumes.push(ViewFogVolume { + uniform_buffer_offset, + exterior: !interior, + density_texture: fog_volume.density_texture.as_ref().map(Handle::id), + }); + } + + commands + .entity(view_entity) + .insert(ViewVolumetricFog(view_fog_volumes)); + } +} + +/// A system that marks all view depth textures as readable in shaders. +/// +/// The volumetric lighting pass needs to do this, and it doesn't happen by +/// default. +pub fn prepare_view_depth_textures_for_volumetric_fog( + mut view_targets: Query<&mut Camera3d>, + fog_volumes: Query<&VolumetricFog>, +) { + if fog_volumes.is_empty() { + return; + } + + for mut camera in view_targets.iter_mut() { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} + +fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] { + let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0); + + for &local_normal in &[ + Vec3A::X, + Vec3A::NEG_X, + Vec3A::Y, + Vec3A::NEG_Y, + Vec3A::Z, + Vec3A::NEG_Z, + ] { + let view_normal = view_from_local + .transform_vector3a(local_normal) + .normalize_or_zero(); + if view_normal.z <= 0.0 { + continue; + } + + let view_position = view_from_local.transform_point3a(-local_normal * 0.5); + let plane_coords = view_normal.extend(-view_normal.dot(view_position)); + + far_planes[next_index] = plane_coords; + next_index += 1; + if next_index == far_planes.len() { + continue; + } + } + + far_planes +} + +impl VolumetricFogBindGroupLayoutKey { + /// Creates an appropriate debug description for the bind group layout with + /// these flags. + fn bind_group_layout_description(&self) -> String { + if self.is_empty() { + return "volumetric lighting view bind group layout".to_owned(); + } + + format!( + "volumetric lighting view bind group layout ({})", + self.iter() + .filter_map(|flag| { + if flag == VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE { + Some("density texture") + } else if flag == VolumetricFogBindGroupLayoutKey::MULTISAMPLED { + Some("multisampled") + } else { + None + } + }) + .collect::>() + .join(", ") + ) + } +} + +/// Given the transform from the view to the 1×1×1 cube in local fog volume +/// space, returns true if the camera is inside the volume. +fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool { + local_from_view + .translation + .abs() + .cmple(Vec3A::splat(0.5)) + .all() +} + +/// Given the local transforms, returns the matrix that transforms model space +/// to clip space. +fn calculate_fog_volume_clip_from_local_transforms( + interior: bool, + clip_from_view: &Mat4, + view_from_local: &Affine3A, +) -> Mat4 { + if !interior { + return *clip_from_view * Mat4::from(*view_from_local); + } + + // If the camera is inside the fog volume, then we'll be rendering a full + // screen quad. The shader will start its raymarch at the fragment depth + // value, however, so we need to make sure that the depth of the full screen + // quad is at the near clip plane `z_near`. + let z_near = clip_from_view.w_axis[2]; + Mat4::from_cols( + vec4(z_near, 0.0, 0.0, 0.0), + vec4(0.0, z_near, 0.0, 0.0), + vec4(0.0, 0.0, 0.0, 0.0), + vec4(0.0, 0.0, z_near, z_near), + ) +} diff --git a/vendor/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/vendor/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl new file mode 100644 index 00000000..e59334b1 --- /dev/null +++ b/vendor/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -0,0 +1,525 @@ +// A postprocessing shader that implements volumetric fog via raymarching and +// sampling directional light shadow maps. +// +// The overall approach is a combination of the volumetric rendering in [1] and +// the shadow map raymarching in [2]. First, we raytrace the AABB of the fog +// volume in order to determine how long our ray is. Then we do a raymarch, with +// physically-based calculations at each step to determine how much light was +// absorbed, scattered out, and scattered in. To determine in-scattering, we +// sample the shadow map for the light to determine whether the point was in +// shadow or not. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +// +// [2]: http://www.alexandre-pestana.com/volumetric-lights/ + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::atmosphere::{ + functions::calculate_visible_sun_ratio, + bruneton_functions::transmittance_lut_r_mu_to_uv, +} +#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip} +#import bevy_pbr::mesh_view_bindings::{ + globals, lights, view, clusterable_objects, + atmosphere_data, atmosphere_transmittance_texture, atmosphere_transmittance_sampler +} +#import bevy_pbr::mesh_view_types::{ + DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT, + POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT, + POINT_LIGHT_FLAGS_VOLUMETRIC_BIT, + POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + ClusterableObject +} +#import bevy_pbr::shadow_sampling::{ + sample_shadow_map_hardware, + sample_shadow_cubemap, + sample_shadow_map, + SPOT_SHADOW_TEXEL_SIZE +} +#import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local} +#import bevy_pbr::utils::interleaved_gradient_noise +#import bevy_pbr::view_transformations::{ + depth_ndc_to_view_z, + frag_coord_to_ndc, + position_ndc_to_view, + position_ndc_to_world, + position_view_to_world +} +#import bevy_pbr::clustered_forward as clustering +#import bevy_pbr::lighting::getDistanceAttenuation; + +// The GPU version of [`VolumetricFog`]. See the comments in +// `volumetric_fog/mod.rs` for descriptions of the fields here. +struct VolumetricFog { + clip_from_local: mat4x4, + uvw_from_world: mat4x4, + far_planes: array, 3>, + fog_color: vec3, + light_tint: vec3, + ambient_color: vec3, + ambient_intensity: f32, + step_count: u32, + bounding_radius: f32, + absorption: f32, + scattering: f32, + density_factor: f32, + density_texture_offset: vec3, + scattering_asymmetry: f32, + light_intensity: f32, + jitter_strength: f32, +} + +@group(1) @binding(0) var volumetric_fog: VolumetricFog; + +#ifdef MULTISAMPLED +@group(1) @binding(1) var depth_texture: texture_depth_multisampled_2d; +#else +@group(1) @binding(1) var depth_texture: texture_depth_2d; +#endif + +#ifdef DENSITY_TEXTURE +@group(1) @binding(2) var density_texture: texture_3d; +@group(1) @binding(3) var density_sampler: sampler; +#endif // DENSITY_TEXTURE + +// 1 / (4π) +const FRAC_4_PI: f32 = 0.07957747154594767; + +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, +} + +@vertex +fn vertex(vertex: Vertex) -> @builtin(position) vec4 { + return volumetric_fog.clip_from_local * vec4(vertex.position, 1.0); +} + +// The common Henyey-Greenstein asymmetric phase function [1] [2]. +// +// This determines how much light goes toward the viewer as opposed to away from +// the viewer. From a visual point of view, it controls how the light shafts +// appear and disappear as the camera looks at the light source. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/ray-marching-get-it-right.html +// +// [2]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction +fn henyey_greenstein(neg_LdotV: f32) -> f32 { + let g = volumetric_fog.scattering_asymmetry; + let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; + return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); +} + +@fragment +fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { + // Unpack the `volumetric_fog` settings. + let uvw_from_world = volumetric_fog.uvw_from_world; + let fog_color = volumetric_fog.fog_color; + let ambient_color = volumetric_fog.ambient_color; + let ambient_intensity = volumetric_fog.ambient_intensity; + let step_count = volumetric_fog.step_count; + let bounding_radius = volumetric_fog.bounding_radius; + let absorption = volumetric_fog.absorption; + let scattering = volumetric_fog.scattering; + let density_factor = volumetric_fog.density_factor; + let density_texture_offset = volumetric_fog.density_texture_offset; + let light_tint = volumetric_fog.light_tint; + let light_intensity = volumetric_fog.light_intensity; + let jitter_strength = volumetric_fog.jitter_strength; + + // Unpack the view. + let exposure = view.exposure; + + // Sample the depth to put an upper bound on the length of the ray (as we + // shouldn't trace through solid objects). If this is multisample, just use + // sample 0; this is approximate but good enough. + let frag_coord = position; + let ndc_end_depth_from_buffer = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + let view_end_depth_from_buffer = -position_ndc_to_view( + frag_coord_to_ndc(vec4(position.xy, ndc_end_depth_from_buffer, 1.0))).z; + + // Calculate the start position of the ray. Since we're only rendering front + // faces of the AABB, this is the current fragment's depth. + let view_start_pos = position_ndc_to_view(frag_coord_to_ndc(frag_coord)); + + // Calculate the end position of the ray. This requires us to raytrace the + // three back faces of the AABB to find the one that our ray intersects. + var end_depth_view = 0.0; + for (var plane_index = 0; plane_index < 3; plane_index += 1) { + let plane = volumetric_fog.far_planes[plane_index]; + let other_plane_a = volumetric_fog.far_planes[(plane_index + 1) % 3]; + let other_plane_b = volumetric_fog.far_planes[(plane_index + 2) % 3]; + + // Calculate the intersection of the ray and the plane. The ray must + // intersect in front of us (t > 0). + let t = -plane.w / dot(plane.xyz, view_start_pos.xyz); + if (t < 0.0) { + continue; + } + let hit_pos = view_start_pos.xyz * t; + + // The intersection point must be in front of the other backfaces. + let other_sides = vec2( + dot(vec4(hit_pos, 1.0), other_plane_a) >= 0.0, + dot(vec4(hit_pos, 1.0), other_plane_b) >= 0.0 + ); + + // If those tests pass, we found our backface. + if (all(other_sides)) { + end_depth_view = -hit_pos.z; + break; + } + } + + // Starting at the end depth, which we got above, figure out how long the + // ray we want to trace is and the length of each increment. + end_depth_view = min(end_depth_view, view_end_depth_from_buffer); + + // We assume world and view have the same scale here. + let start_depth_view = -depth_ndc_to_view_z(frag_coord.z); + + let ray_length_view = max(0.0, end_depth_view - start_depth_view); + // If the end is behind the start of the first opaque pixel, then we know it + // is occluded, and we don't need to render it + if (ray_length_view == 0.0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + let inv_step_count = 1.0 / f32(step_count); + let step_size_world = ray_length_view * inv_step_count; + + let directional_light_count = lights.n_directional_lights; + + // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC, + // view, and world coordinates. + let Rd_ndc = vec3(frag_coord_to_ndc(position).xy, 1.0); + let Rd_view = normalize(position_ndc_to_view(Rd_ndc)); + var Ro_world = position_view_to_world(view_start_pos.xyz); + let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - view.world_position); + + // Offset by jitter. + let jitter = interleaved_gradient_noise(position.xy, globals.frame_count) * jitter_strength; + Ro_world += Rd_world * jitter; + + // Use Beer's law [1] [2] to calculate the maximum amount of light that each + // directional light could contribute, and modulate that value by the light + // tint and fog color. (The actual value will in turn be modulated by the + // phase according to the Henyey-Greenstein formula.) + // + // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html + // + // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law + + // Use Beer's law again to accumulate the ambient light all along the path. + var accumulated_color = exp(-ray_length_view * (absorption + scattering)) * ambient_color * + ambient_intensity; + + // This is the amount of the background that shows through. We're actually + // going to recompute this over and over again for each directional light, + // coming up with the same values each time. + var background_alpha = 1.0; + + // If we have a density texture, transform to its local space. +#ifdef DENSITY_TEXTURE + let Ro_uvw = (uvw_from_world * vec4(Ro_world, 1.0)).xyz; + let Rd_step_uvw = mat3x3(uvw_from_world[0].xyz, uvw_from_world[1].xyz, uvw_from_world[2].xyz) * + (Rd_world * step_size_world); +#endif // DENSITY_TEXTURE + + for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) { + // Volumetric lights are all sorted first, so the first time we come to + // a non-volumetric light, we know we've seen them all. + let light = &lights.directional_lights[light_index]; + if (((*light).flags & DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + break; + } + + let L = (*light).direction_to_light.xyz; + + // Offset the depth value by the bias. + let depth_offset = (*light).shadow_depth_bias * L; + + // Compute phase, which determines the fraction of light that's + // scattered toward the camera instead of away from it. + let neg_LdotV = dot(normalize(L), Rd_world); + let phase = henyey_greenstein(neg_LdotV); + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size_world; + let P_view = Rd_view * f32(step) * step_size_world; + + var density = density_factor; +#ifdef DENSITY_TEXTURE + // Take the density texture into account, if there is one. + // + // The uvs should never go outside the (0, 0, 0) to (1, 1, 1) box, + // but sometimes due to floating point error they can. Handle this + // case. + let P_uvw = Ro_uvw + Rd_step_uvw * f32(step); + if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) { + density *= textureSampleLevel(density_texture, density_sampler, P_uvw + density_texture_offset, 0.0).r; + } else { + density = 0.0; + } +#endif // DENSITY_TEXTURE + + // Calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). + let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + // Compute in-scattering (amount of light other fog particles + // scattered into this ray). This is where any directional light is + // scattered in. + + // Prepare to sample the shadow map. + let cascade_index = get_cascade_index(light_index, P_view.z); + let light_local = world_to_directional_light_local( + light_index, + cascade_index, + vec4(P_world + depth_offset, 1.0) + ); + + // If we're outside the shadow map entirely, local light attenuation + // is zero. + var local_light_attenuation = f32(light_local.w != 0.0); + + // Otherwise, sample the shadow map to determine whether, and by how + // much, this sample is in the light. + if (local_light_attenuation != 0.0) { + let cascade = &(*light).cascades[cascade_index]; + let array_index = i32((*light).depth_texture_base_index + cascade_index); + local_light_attenuation = + sample_shadow_map_hardware(light_local.xy, light_local.z, array_index); + } + + if (local_light_attenuation != 0.0) { + let light_attenuation = exp(-density * bounding_radius * (absorption + scattering)); + var light_factors_per_step = fog_color * light_tint * light_attenuation * + scattering * density * step_size_world * light_intensity * exposure; + +#ifdef ATMOSPHERE + // attenuate by atmospheric scattering + let P = P_world + depth_offset; + let P_scaled = P * vec3(atmosphere_data.settings.scene_units_to_m); + let O = vec3(0.0, atmosphere_data.atmosphere.bottom_radius, 0.0); + let P_as = P_scaled + O; + let r = length(P_as); + let local_up = normalize(P_as); + let mu_light = dot(L, local_up); + + let transmittance = sample_transmittance_lut(r, mu_light); + let sun_visibility = calculate_visible_sun_ratio(atmosphere_data.atmosphere, r, mu_light, (*light).sun_disk_angular_size); + light_factors_per_step *= transmittance * sun_visibility; +#endif + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; + + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + } + + // Point lights and Spot lights + let view_z = view_start_pos.z; + let is_orthographic = view.clip_from_view[3].w == 1.0; + let cluster_index = clustering::fragment_cluster_index(frag_coord.xy, view_z, is_orthographic); + var clusterable_object_index_ranges = + clustering::unpack_clusterable_object_index_ranges(cluster_index); + for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + let light = &clusterable_objects.data[light_id]; + if (((*light).flags & POINT_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + continue; + } + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size_world; + let P_view = Rd_view * f32(step) * step_size_world; + + var density = density_factor; + + let light_to_frag = (*light).position_radius.xyz - P_world; + let V = Rd_world; + let L = normalize(light_to_frag); + let distance_square = dot(light_to_frag, light_to_frag); + let distance_atten = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + var local_light_attenuation = distance_atten; + if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { + var shadow: f32 = 1.0; + if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_point_shadow_without_normal(light_id, vec4(P_world, 1.0)); + } + local_light_attenuation *= shadow; + } else { + // spot light attenuation + // reconstruct spot dir from x/z and y-direction flag + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { + spot_dir.y = -spot_dir.y; + } + let light_to_frag = (*light).position_radius.xyz - P_world; + + // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight + // spot_scale and spot_offset have been precomputed + // note we normalize here to get "l" from the filament listing. spot_dir is already normalized + let cd = dot(-spot_dir, normalize(light_to_frag)); + let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); + let spot_attenuation = attenuation * attenuation; + + var shadow: f32 = 1.0; + if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_spot_shadow_without_normal(light_id, vec4(P_world, 1.0)); + } + local_light_attenuation *= spot_attenuation * shadow; + } + + // Calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). + let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + let light_attenuation = exp(-density * bounding_radius * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * + scattering * density * step_size_world * light_intensity * exposure; + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color_inverse_square_range.rgb * light_factors_per_step; + + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + + // We're done! Return the color with alpha so it can be blended onto the + // render target. + return vec4(accumulated_color, 1.0 - background_alpha); +} + +fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4) -> f32 { + let light = &clusterable_objects.data[light_id]; + + // because the shadow maps align with the axes and the frustum planes are at 45 degrees + // we can get the worldspace depth by taking the largest absolute axis + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + let surface_to_light_abs = abs(surface_to_light); + let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); + + // The normal bias here is already scaled by the texel size at 1 world unit from the light. + // The texel size increases proportionally with distance from the light so multiplying by + // distance to light scales the normal bias to the texel size at the fragment distance. + let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz); + let offset_position = frag_position.xyz + depth_offset; + + // similar largest-absolute-axis trick as above, but now with the offset fragment position + let frag_ls = offset_position.xyz - (*light).position_radius.xyz ; + let abs_position_ls = abs(frag_ls); + let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); + + // NOTE: These simplifications come from multiplying: + // projection * vec4(0, 0, -major_axis_magnitude, 1.0) + // and keeping only the terms that have any impact on the depth. + // Projection-agnostic approach: + let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; + let depth = zw.x / zw.y; + + // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, + // so we have to flip the z-axis when sampling. + let flip_z = vec3(1.0, 1.0, -1.0); + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id); +} + +fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4) -> f32 { + let light = &clusterable_objects.data[light_id]; + + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + + // construct the light view matrix + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + // reconstruct spot dir from x/z and y-direction flag + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { + spot_dir.y = -spot_dir.y; + } + + // view matrix z_axis is the reverse of transform.forward() + let fwd = -spot_dir; + let offset_position = + -surface_to_light + + ((*light).shadow_depth_bias * normalize(surface_to_light)); + + // the construction of the up and right vectors needs to precisely mirror the code + // in render/light.rs:spot_light_view_matrix + var sign = -1.0; + if (fwd.z >= 0.0) { + sign = 1.0; + } + let a = -1.0 / (fwd.z + sign); + let b = fwd.x * fwd.y * a; + let up_dir = vec3(1.0 + sign * fwd.x * fwd.x * a, sign * b, -sign * fwd.x); + let right_dir = vec3(-b, -sign - fwd.y * fwd.y * a, fwd.y); + let light_inv_rot = mat3x3(right_dir, up_dir, fwd); + + // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate + // the product of the transpose with a vector we can just post-multiply instead of pre-multiplying. + // this allows us to keep the matrix construction code identical between CPU and GPU. + let projected_position = offset_position * light_inv_rot; + + // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w) + // to get ndc coordinates + let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z); + let shadow_xy_ndc = projected_position.xy * f_div_minus_z; + // convert to uv coordinates + let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); + + // 0.1 must match POINT_LIGHT_NEAR_Z + let depth = 0.1 / -projected_position.z; + + return sample_shadow_map( + shadow_uv, + depth, + i32(light_id) + lights.spot_light_shadowmap_offset, + SPOT_SHADOW_TEXEL_SIZE + ); +} + +#ifdef ATMOSPHERE +fn sample_transmittance_lut(r: f32, mu: f32) -> vec3 { + let uv = transmittance_lut_r_mu_to_uv(atmosphere_data.atmosphere, r, mu); + return textureSampleLevel( + atmosphere_transmittance_texture, + atmosphere_transmittance_sampler, uv, 0.0).rgb; +} +#endif // ATMOSPHERE \ No newline at end of file diff --git a/vendor/bevy_pbr/src/wireframe.rs b/vendor/bevy_pbr/src/wireframe.rs new file mode 100644 index 00000000..c4b19df6 --- /dev/null +++ b/vendor/bevy_pbr/src/wireframe.rs @@ -0,0 +1,928 @@ +use crate::{ + DrawMesh, MeshPipeline, MeshPipelineKey, RenderLightmaps, RenderMeshInstanceFlags, + RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewBindingArrayBindGroup, + ViewKeyCache, ViewSpecializationTicks, +}; +use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; +use bevy_asset::{ + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, + AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, +}; +use bevy_camera::{visibility::ViewVisibility, Camera, Camera3d}; +use bevy_color::{Color, ColorToComponents}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + change_detection::Tick, + prelude::*, + query::QueryItem, + system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem}, +}; +use bevy_mesh::{Mesh3d, MeshVertexBufferLayoutRef}; +use bevy_platform::{ + collections::{HashMap, HashSet}, + hash::FixedHasher, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::{extract_cameras, ExtractedCamera}, + diagnostic::RecordDiagnostics, + extract_resource::ExtractResource, + mesh::{ + allocator::{MeshAllocator, SlabId}, + RenderMesh, + }, + prelude::*, + render_asset::{ + prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, + }, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_phase::{ + AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, + CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, + PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, + SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, + }, + render_resource::*, + renderer::{RenderContext, RenderDevice}, + sync_world::{MainEntity, MainEntityHashMap}, + view::{ + ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities, + RetainedViewEntity, ViewDepthTexture, ViewTarget, + }, + Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; +use bevy_shader::Shader; +use core::{hash::Hash, ops::Range}; +use tracing::{error, warn}; + +/// A [`Plugin`] that draws wireframes. +/// +/// Wireframes currently do not work when using webgl or webgpu. +/// Supported rendering backends: +/// - DX12 +/// - Vulkan +/// - Metal +/// +/// This is a native only feature. +#[derive(Debug, Default)] +pub struct WireframePlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl WireframePlugin { + /// Creates a new [`WireframePlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { debug_flags } + } +} + +impl Plugin for WireframePlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "render/wireframe.wgsl"); + + app.add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + RenderAssetPlugin::::default(), + )) + .init_asset::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup_global_wireframe_material) + .add_systems( + Update, + ( + global_color_changed.run_if(resource_changed::), + wireframe_color_changed, + // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global + // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. + (apply_wireframe_material, apply_global_wireframe_material).chain(), + ), + ) + .add_systems( + PostUpdate, + check_wireframe_entities_needing_specialization + .after(AssetEventSystems) + .run_if(resource_exists::), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let required_features = WgpuFeatures::POLYGON_MODE_LINE | WgpuFeatures::PUSH_CONSTANTS; + let render_device = render_app.world().resource::(); + if !render_device.features().contains(required_features) { + warn!( + "WireframePlugin not loaded. GPU lacks support for required features: {:?}.", + required_features + ); + return; + } + + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_render_graph_node::>(Core3d, Node3d::Wireframe) + .add_render_graph_edges( + Core3d, + ( + Node3d::EndMainPass, + Node3d::Wireframe, + Node3d::PostProcessing, + ), + ) + .add_systems(RenderStartup, init_wireframe_3d_pipeline) + .add_systems( + ExtractSchedule, + ( + extract_wireframe_3d_camera, + extract_wireframe_entities_needing_specialization.after(extract_cameras), + extract_wireframe_materials, + ), + ) + .add_systems( + Render, + ( + specialize_wireframes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(prepare_assets::), + queue_wireframes + .in_set(RenderSystems::QueueMeshes) + .after(prepare_assets::), + ), + ); + } +} + +/// Enables wireframe rendering for any entity it is attached to. +/// It will ignore the [`WireframeConfig`] global setting. +/// +/// This requires the [`WireframePlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq)] +pub struct Wireframe; + +pub struct Wireframe3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Wireframe3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: Wireframe3dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for Wireframe3d { + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + fn batch_range(&self) -> &Range { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl CachedRenderPipelinePhaseItem for Wireframe3d { + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +impl BinnedPhaseItem for Wireframe3d { + type BinKey = Wireframe3dBinKey; + type BatchSetKey = Wireframe3dBatchSetKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The wireframe material asset ID. + pub asset_id: UntypedAssetId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for Wireframe3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data that must be identical in order to *batch* phase items together. +/// +/// Note that a *batch set* (if multi-draw is in use) contains multiple batches. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBinKey { + /// The wireframe mesh asset ID. + pub asset_id: UntypedAssetId, +} + +pub struct SetWireframe3dPushConstants; + +impl RenderCommand

for SetWireframe3dPushConstants { + type Param = ( + SRes, + SRes>, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + + pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&wireframe_material.color), + ); + RenderCommandResult::Success + } +} + +pub type DrawWireframe3d = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetWireframe3dPushConstants, + DrawMesh, +); + +#[derive(Resource, Clone)] +pub struct Wireframe3dPipeline { + mesh_pipeline: MeshPipeline, + shader: Handle, +} + +pub fn init_wireframe_3d_pipeline( + mut commands: Commands, + mesh_pipeline: Res, + asset_server: Res, +) { + commands.insert_resource(Wireframe3dPipeline { + mesh_pipeline: mesh_pipeline.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "render/wireframe.wgsl"), + }); +} + +impl SpecializedMeshPipeline for Wireframe3dPipeline { + type Key = MeshPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self.mesh_pipeline.specialize(key, layout)?; + descriptor.label = Some("wireframe_3d_pipeline".into()); + descriptor.push_constant_ranges.push(PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..16, + }); + let fragment = descriptor.fragment.as_mut().unwrap(); + fragment.shader = self.shader.clone(); + descriptor.primitive.polygon_mode = PolygonMode::Line; + descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0; + Ok(descriptor) + } +} + +#[derive(Default)] +struct Wireframe3dNode; +impl ViewNode for Wireframe3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(wireframe_phase) = world.get_resource::>() + else { + return Ok(()); + }; + + let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("wireframe_3d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "wireframe_3d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) { + error!("Error encountered while rendering the stencil phase {err:?}"); + return Err(NodeRunError::DrawError(err)); + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Sets the color of the [`Wireframe`] of the entity it is attached to. +/// +/// If this component is present but there's no [`Wireframe`] component, +/// it will still affect the color of the wireframe when [`WireframeConfig::global`] is set to true. +/// +/// This overrides the [`WireframeConfig::default_color`]. +#[derive(Component, Debug, Clone, Default, Reflect)] +#[reflect(Component, Default, Debug)] +pub struct WireframeColor { + pub color: Color, +} + +#[derive(Component, Debug, Clone, Default)] +pub struct ExtractedWireframeColor { + pub color: [f32; 4], +} + +/// Disables wireframe rendering for any entity it is attached to. +/// It will ignore the [`WireframeConfig`] global setting. +/// +/// This requires the [`WireframePlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq)] +pub struct NoWireframe; + +#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] +#[reflect(Resource, Debug, Default)] +pub struct WireframeConfig { + /// Whether to show wireframes for all meshes. + /// Can be overridden for individual meshes by adding a [`Wireframe`] or [`NoWireframe`] component. + pub global: bool, + /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe`] component attached to it will have + /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe`], + /// but no [`WireframeColor`]. + pub default_color: Color, +} + +#[derive(Asset, Reflect, Clone, Debug, Default)] +#[reflect(Clone, Default)] +pub struct WireframeMaterial { + pub color: Color, +} + +pub struct RenderWireframeMaterial { + pub color: [f32; 4], +} + +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct Mesh3dWireframe(pub Handle); + +impl AsAssetId for Mesh3dWireframe { + type Asset = WireframeMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +impl RenderAsset for RenderWireframeMaterial { + type SourceAsset = WireframeMaterial; + type Param = (); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, + ) -> Result> { + Ok(RenderWireframeMaterial { + color: source_asset.color.to_linear().to_f32_array(), + }) + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct RenderWireframeInstances(MainEntityHashMap>); + +#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)] +pub struct WireframeEntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)] +pub struct WireframeEntitySpecializationTicks { + pub entities: MainEntityHashMap, +} + +/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedWireframePipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedWireframeViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +#[derive(Resource)] +struct GlobalWireframeMaterial { + // This handle will be reused when the global config is enabled + handle: Handle, +} + +pub fn extract_wireframe_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &Mesh3dWireframe), + Or<(Changed, Changed)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>, +) { + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.insert(entity.into(), material.id()); + } else { + material_instances.remove(&MainEntity::from(entity)); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + material_instances.remove(&MainEntity::from(entity)); + } + } +} + +fn setup_global_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + config: Res, +) { + // Create the handle used for the global material + commands.insert_resource(GlobalWireframeMaterial { + handle: materials.add(WireframeMaterial { + color: config.default_color, + }), + }); +} + +/// Updates the wireframe material of all entities without a [`WireframeColor`] or without a [`Wireframe`] component +fn global_color_changed( + config: Res, + mut materials: ResMut>, + global_material: Res, +) { + if let Some(global_material) = materials.get_mut(&global_material.handle) { + global_material.color = config.default_color; + } +} + +/// Updates the wireframe material when the color in [`WireframeColor`] changes +fn wireframe_color_changed( + mut materials: ResMut>, + mut colors_changed: Query< + (&mut Mesh3dWireframe, &WireframeColor), + (With, Changed), + >, +) { + for (mut handle, wireframe_color) in &mut colors_changed { + handle.0 = materials.add(WireframeMaterial { + color: wireframe_color.color, + }); + } +} + +/// Applies or remove the wireframe material to any mesh with a [`Wireframe`] component, and removes it +/// for any mesh with a [`NoWireframe`] component. +fn apply_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + wireframes: Query< + (Entity, Option<&WireframeColor>), + (With, Without), + >, + no_wireframes: Query, With)>, + mut removed_wireframes: RemovedComponents, + global_material: Res, +) { + for e in removed_wireframes.read().chain(no_wireframes.iter()) { + if let Ok(mut commands) = commands.get_entity(e) { + commands.remove::(); + } + } + + let mut material_to_spawn = vec![]; + for (e, maybe_color) in &wireframes { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + material_to_spawn.push((e, Mesh3dWireframe(material))); + } + commands.try_insert_batch(material_to_spawn); +} + +type WireframeFilter = (With, Without, Without); + +/// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component. +fn apply_global_wireframe_material( + mut commands: Commands, + config: Res, + meshes_without_material: Query< + (Entity, Option<&WireframeColor>), + (WireframeFilter, Without), + >, + meshes_with_global_material: Query)>, + global_material: Res, + mut materials: ResMut>, +) { + if config.global { + let mut material_to_spawn = vec![]; + for (e, maybe_color) in &meshes_without_material { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + // We only add the material handle but not the Wireframe component + // This makes it easy to detect which mesh is using the global material and which ones are user specified + material_to_spawn.push((e, Mesh3dWireframe(material))); + } + commands.try_insert_batch(material_to_spawn); + } else { + for e in &meshes_with_global_material { + commands.entity(e).remove::(); + } + } +} + +/// Gets a handle to a wireframe material with a fallback on the default material +fn get_wireframe_material( + maybe_color: Option<&WireframeColor>, + wireframe_materials: &mut Assets, + global_material: &GlobalWireframeMaterial, +) -> Handle { + if let Some(wireframe_color) = maybe_color { + wireframe_materials.add(WireframeMaterial { + color: wireframe_color.color, + }) + } else { + // If there's no color specified we can use the global material since it's already set to use the default_color + global_material.handle.clone() + } +} + +fn extract_wireframe_3d_camera( + mut wireframe_3d_phases: ResMut>, + cameras: Extract), With>>, + mut live_entities: Local>, + gpu_preprocessing_support: Res, +) { + live_entities.clear(); + for (main_entity, camera, no_indirect_drawing) in &cameras { + if !camera.is_active { + continue; + } + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + wireframe_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_entities.insert(retained_view_entity); + } + + // Clear out all dead views. + wireframe_3d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn extract_wireframe_entities_needing_specialization( + entities_needing_specialization: Extract>, + mut entity_specialization_ticks: ResMut, + views: Query<&ExtractedView>, + mut specialized_wireframe_pipeline_cache: ResMut, + mut removed_meshes_query: Extract>, + ticks: SystemChangeTick, +) { + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } + + for entity in removed_meshes_query.read() { + for view in &views { + if let Some(specialized_wireframe_pipeline_cache) = + specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity) + { + specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity)); + } + } + } +} + +pub fn check_wireframe_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + >, + mut entities_needing_specialization: ResMut, +) { + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} + +pub fn specialize_wireframes( + render_meshes: Res>, + render_mesh_instances: Res, + render_wireframe_instances: Res, + render_visibility_ranges: Res, + wireframe_phases: Res>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + render_lightmaps: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !wireframe_phases.contains_key(&view.retained_view_entity) { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + if !render_wireframe_instances.contains_key(visible_entity) { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key; + mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology()); + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + // Even though we don't use the lightmap in the wireframe, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + if render_lightmaps + .render_lightmaps + .contains_key(visible_entity) + { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +fn queue_wireframes( + custom_draw_functions: Res>, + render_mesh_instances: Res, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + specialized_wireframe_pipeline_cache: Res, + render_wireframe_instances: Res, + mut wireframe_3d_phases: ResMut>, + mut views: Query<(&ExtractedView, &RenderVisibleEntities)>, +) { + for (view, visible_entities) in &mut views { + let Some(wireframe_phase) = wireframe_3d_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let draw_wireframe = custom_draw_functions.read().id::(); + + let Some(view_specialized_material_pipeline_cache) = + specialized_wireframe_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else { + continue; + }; + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) { + continue; + } + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let bin_key = Wireframe3dBinKey { + asset_id: mesh_instance.mesh_asset_id.untyped(), + }; + let batch_set_key = Wireframe3dBatchSetKey { + pipeline: pipeline_id, + asset_id: wireframe_instance.untyped(), + draw_function: draw_wireframe, + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + wireframe_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + } +}