Add runtime occlusion culling with conservative depth and valid GPU bindings

This commit is contained in:
chazmm committed 2026-09-06 16:43:06 +01:00
1 parent d31f81f95e
commit a21f78aa52
57 files changed
+12066 -24

No files matched your search

Generated
-2
View File
@@ -570,8 +570,6 @@ dependencies = [
[[package]] [[package]]
name = "bevy_core_pipeline" name = "bevy_core_pipeline"
version = "0.18.1" version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d0810e85c2436e50c67448d48a83bf0bb1b5849899619ae2c7ea817221e9172"
dependencies = [ dependencies = [
"bevy_app", "bevy_app",
"bevy_asset", "bevy_asset",
+1
View File
@@ -22,3 +22,4 @@ opt-level = 3
[patch.crates-io] [patch.crates-io]
bevy_pbr = { path = "vendor/bevy_pbr" } bevy_pbr = { path = "vendor/bevy_pbr" }
bevy_core_pipeline = { path = "vendor/bevy_core_pipeline" }
+5 -2
View File
@@ -4,10 +4,13 @@ Double-click **PLAY.bat** to launch. Use an XInput-compatible controller;
**Esc** opens the pause/graphics menu. Startup selects the supplied **Easy** profile. **Esc** opens the pause/graphics menu. Startup selects the supplied **Easy** profile.
The menu supports window resolution (720p through 4K), internal render scale The menu supports window resolution (720p through 4K), internal render scale
(25–100%), supported MSAA levels (Off/2×/4×/8×), and an FPS cap (30–240 or (25–100%), supported MSAA levels (Off/2×/4×/8×), an FPS cap (30–240 or
Unlimited). Click a row to cycle it, or use arrow keys and Enter. Changes apply Unlimited), and GPU occlusion culling (On/Off). Click a row to cycle it, or use arrow keys and Enter. Changes apply
immediately and save to `settings/graphics.json`. Escape or Resume returns to immediately and save to `settings/graphics.json`. Escape or Resume returns to
gameplay; Quit game exits. The menu stays at the window's native resolution. gameplay; Quit game exits. The menu stays at the window's native resolution.
Occlusion culling defaults to On and skips meshes hidden behind other geometry.
It needs no changes to `.skate` files. Its benefit depends on the view; compare
On/Off where you see dips. It does not reduce visible mesh detail or collision.
The standing/rolling **360 flip** now uses the supplied gesture files and The standing/rolling **360 flip** now uses the supplied gesture files and
ActionGraph/MotionGraph route, including right-stick crouch preparation. ActionGraph/MotionGraph route, including right-stick crouch preparation.
+70 -9
View File
@@ -1,9 +1,11 @@
//! Native-resolution pause UI over a separately scaled 3D render target. //! Native-resolution pause UI over a separately scaled 3D render target.
use bevy::{ use bevy::{
camera::RenderTarget, camera::RenderTarget,
core_pipeline::prepass::DepthPrepass,
image::ImageSampler, image::ImageSampler,
prelude::*, prelude::*,
render::{ render::{
experimental::occlusion_culling::OcclusionCulling,
render_resource::{Extent3d, TextureFormat}, render_resource::{Extent3d, TextureFormat},
renderer::RenderAdapter, renderer::RenderAdapter,
}, },
@@ -36,6 +38,7 @@ struct GraphicsSettings {
scale: u32, scale: u32,
samples: u32, samples: u32,
fps: u32, fps: u32,
occlusion: bool,
} }
impl Default for GraphicsSettings { impl Default for GraphicsSettings {
fn default() -> Self { fn default() -> Self {
@@ -45,6 +48,7 @@ impl Default for GraphicsSettings {
scale: 100, scale: 100,
samples: 4, samples: 4,
fps: 0, fps: 0,
occlusion: true,
} }
} }
} }
@@ -141,6 +145,12 @@ fn setup(
}) })
.collect(); .collect();
let mut settings = settings; let mut settings = settings;
// Reproducible A/B override; normal launches use the saved menu setting.
match std::env::var("SKATE_OCCLUSION").as_deref() {
Ok("0") => settings.occlusion = false,
Ok("1") => settings.occlusion = true,
_ => {}
}
if !supported_msaa.contains(&settings.samples) { if !supported_msaa.contains(&settings.samples) {
settings.samples = 1; settings.samples = 1;
} }
@@ -184,7 +194,7 @@ fn setup(
BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| { BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| {
panel.spawn((Text::new("PAUSED"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE))); panel.spawn((Text::new("PAUSED"),TextFont {font_size:32.,..default()},TextColor(Color::WHITE)));
panel.spawn((Text::new("GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85)))); panel.spawn((Text::new("GRAPHICS"),TextFont {font_size:16.,..default()},TextColor(Color::srgb(0.4,0.85,0.85))));
for i in 0..6 { for i in 0..7 {
panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(48),padding:UiRect::all(px(12)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()}, panel.spawn((Button, MenuRow(i), Node {width:percent(100),min_height:px(48),padding:UiRect::all(px(12)),align_items:AlignItems::Center,border_radius:BorderRadius::all(px(5)),..default()},
BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| { BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {
row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:20.,..default()},TextColor(Color::WHITE))); row.spawn((MenuLabel(i),Text::new(""),TextFont {font_size:20.,..default()},TextColor(Color::WHITE)));
@@ -229,10 +239,10 @@ fn interact(
let mut action = None; let mut action = None;
if menu.open { if menu.open {
if keys.just_pressed(KeyCode::ArrowUp) { if keys.just_pressed(KeyCode::ArrowUp) {
menu.selected = (menu.selected + 5) % 6; menu.selected = (menu.selected + 6) % 7;
} }
if keys.just_pressed(KeyCode::ArrowDown) { if keys.just_pressed(KeyCode::ArrowDown) {
menu.selected = (menu.selected + 1) % 6; menu.selected = (menu.selected + 1) % 7;
} }
if keys.just_pressed(KeyCode::ArrowLeft) { if keys.just_pressed(KeyCode::ArrowLeft) {
action = Some((menu.selected, -1)); action = Some((menu.selected, -1));
@@ -263,13 +273,14 @@ fn interact(
cycle(&menu.supported_msaa, menu.settings.samples, direction) cycle(&menu.supported_msaa, menu.settings.samples, direction)
} }
3 => menu.settings.fps = cycle(LIMITS, menu.settings.fps, direction), 3 => menu.settings.fps = cycle(LIMITS, menu.settings.fps, direction),
4 => menu.open = false, 4 => menu.settings.occlusion = !menu.settings.occlusion,
5 => { 5 => menu.open = false,
6 => {
exit.write(AppExit::Success); exit.write(AppExit::Success);
} }
_ => {} _ => {}
} }
if row < 4 { if row < 5 {
let save = (|| -> Result<(), String> { let save = (|| -> Result<(), String> {
std::fs::create_dir_all(menu.path.parent().unwrap()).map_err(|e| e.to_string())?; std::fs::create_dir_all(menu.path.parent().unwrap()).map_err(|e| e.to_string())?;
std::fs::write( std::fs::write(
@@ -291,11 +302,12 @@ fn interact(
} }
} }
fn apply( fn apply(
mut commands: Commands,
menu: Res<Menu>, menu: Res<Menu>,
mut window: Single<&mut Window, With<PrimaryWindow>>, mut window: Single<&mut Window, With<PrimaryWindow>>,
target: Res<SceneTarget>, target: Res<SceneTarget>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
mut cameras: Query<&mut Msaa, With<Camera3d>>, mut cameras: Query<(Entity, &mut Msaa), With<Camera3d>>,
mut previous: Local<Option<GraphicsSettings>>, mut previous: Local<Option<GraphicsSettings>>,
) { ) {
if previous if previous
@@ -310,10 +322,20 @@ fn apply(
.as_ref() .as_ref()
.is_none_or(|p| p.samples != menu.settings.samples) .is_none_or(|p| p.samples != menu.settings.samples)
{ {
for mut samples in &mut cameras { for (_, mut samples) in &mut cameras {
*samples = msaa(menu.settings.samples); *samples = msaa(menu.settings.samples);
} }
} }
if previous.as_ref().is_none_or(|p| p.occlusion != menu.settings.occlusion) {
for (entity, _) in &cameras {
if menu.settings.occlusion {
commands.entity(entity).insert((DepthPrepass, OcclusionCulling));
} else {
commands.entity(entity).remove::<(DepthPrepass, OcclusionCulling)>();
}
}
info!("GPU occlusion culling: {}", menu.settings.occlusion);
}
let size = menu.settings.internal_size(window.physical_size()); let size = menu.settings.internal_size(window.physical_size());
if let Some(image) = images.get(&target.0) { if let Some(image) = images.get(&target.0) {
if image.size() != size { if image.size() != size {
@@ -367,7 +389,8 @@ fn labels(
s.fps.to_string() s.fps.to_string()
} }
), ),
4 => "Resume".into(), 4 => format!("Occlusion culling {}", if s.occlusion { "On" } else { "Off" }),
5 => "Resume".into(),
_ => "Quit game".into(), _ => "Quit game".into(),
}; };
} }
@@ -397,6 +420,44 @@ fn pace(menu: Option<Res<Menu>>, mut pacer: ResMut<FramePacer>) {
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn culling_can_toggle_with_msaa_and_render_scale_changes() {
let mut app = App::new();
let mut images = Assets::<Image>::default();
let target = images.add(Image::new_target_texture(1280, 800, TextureFormat::Rgba8UnormSrgb, None));
app.insert_resource(SceneTarget(target.clone()))
.insert_resource(images)
.insert_resource(Menu {
open: false, selected: 0, settings: GraphicsSettings::default(),
path: PathBuf::new(), supported_msaa: vec![1, 2, 4, 8], status: String::new(),
})
.add_systems(Update, apply);
app.world_mut().spawn((Window::default(), PrimaryWindow));
let camera = app.world_mut().spawn((Camera3d::default(), Msaa::Off)).id();
app.update();
assert!(app.world().entity(camera).contains::<OcclusionCulling>());
assert!(app.world().entity(camera).contains::<DepthPrepass>());
{
let mut menu = app.world_mut().resource_mut::<Menu>();
menu.settings.occlusion = false;
menu.settings.samples = 1;
menu.settings.scale = 67;
}
app.update();
assert!(!app.world().entity(camera).contains::<OcclusionCulling>());
assert!(!app.world().entity(camera).contains::<DepthPrepass>());
assert_eq!(*app.world().get::<Msaa>(camera).unwrap(), Msaa::Off);
assert_eq!(app.world().resource::<Assets<Image>>().get(&target).unwrap().size(), UVec2::new(857, 536));
{
let mut menu = app.world_mut().resource_mut::<Menu>();
menu.settings.occlusion = true;
menu.settings.samples = 8;
}
app.update();
assert!(app.world().entity(camera).contains::<OcclusionCulling>());
assert!(app.world().entity(camera).contains::<DepthPrepass>());
assert_eq!(*app.world().get::<Msaa>(camera).unwrap(), Msaa::Sample8);
}
#[test]
fn invalid_saved_values_fall_back() { fn invalid_saved_values_fall_back() {
let settings: GraphicsSettings = let settings: GraphicsSettings =
serde_json::from_str(r#"{"width":0,"height":999999,"scale":0,"samples":3,"fps":1}"#) serde_json::from_str(r#"{"width":0,"height":999999,"scale":0,"samples":3,"fps":1}"#)
+36
View File
@@ -142,3 +142,39 @@ describe correctness fixes in 0.19. It was not enabled as an unverified workarou
Validation: the explicit GPU regression test and game suite (46 passed, Validation: the explicit GPU regression test and game suite (46 passed,
21 existing ignored) passed. The staged build completed the normal San Vanelona 21 existing ignored) passed. The staged build completed the normal San Vanelona
startup capture at the user's saved settings, with benchmark overrides disabled. startup capture at the user's saved settings, with benchmark overrides disabled.
## Runtime occlusion culling
The Escape graphics menu now supports GPU occlusion culling, default On. Existing
settings files without that field pick up the default. `SKATE_OCCLUSION=0` or `1`
overrides the saved value at launch for A/B runs. The two-phase GPU algorithm
uses a depth prepass and dynamically rejects hidden meshes; no map baking, LOD
generation, distance cutoff, or collision changes are involved.
This backports [Bevy #22603](https://github.com/bevyengine/bevy/pull/22603) to
0.18.1's core pipeline: conservative depth-pyramid sampling at non-power-of-two
resolutions. It also fixes a reproduced GPU validation failure in 0.18.1's late
preprocessing pass by making its indirect argument buffer binding read-only.
Details and provenance are in `vendor/README.md`.
Same saved 1440p / 100% / 8x MSAA settings, rotating view, no concurrent builds:
| Trial | Average FPS | Frame interval p95 |
| --- | ---: | ---: |
| Original material batches, culling Off | 124.1 | 22.1 ms |
| Original material batches, culling On | 128.0 | 21.3 ms |
| 128-unit spatial batches, culling On | 72.4 | 51.2 ms |
Reports: `logs/cull-off.json`, `logs/cull-on2.json`, `logs/cull-cell128.json`.
The small On/Off difference is within plausible run variation, not evidence of
a major FPS win. CPU/render submission overhead remains significant even when
the GPU skips hidden geometry. These sweeps are not a minimum-FPS guarantee.
Spatial splitting increased batches from 30,097 to 55,850 and was removed from
the final implementation. All 8,214,063 render triangles remain available.
Validation: city sweep and normal startup capture completed without GPU errors
at 8x MSAA. The game suite has 47 passing tests and 21 pre-existing ignored tests,
including changing culling, MSAA and internal resolution together. The explicit
GPU lightmap cache regression test also passed. The startup
image was visually inspected. Manual menu testing was interrupted by the user
stopping Computer Use, so live UI toggling has not been visually verified.
+19
View File
@@ -1,5 +1,24 @@
# Local Bevy patch # Local Bevy patch
## Conservative occlusion depth pyramid
`bevy_core_pipeline` is vendored from crates.io 0.18.1 with its original licenses.
Its `src/experimental/mip_generation/{mod.rs,downsample_depth.wgsl}` backports
[Bevy #22603](https://github.com/bevyengine/bevy/pull/22603), commit
`43f4552308a29f4130c3ad71ea65d48f77daea6e`. The upstream file paths changed
between releases; the algorithm is the upstream fix. Power-of-two pyramid
levels and conservative source sampling prevent false occlusion at arbitrary
viewport sizes, including scaled targets and MSAA. The associated meshlet
texture-dimension fix is included in `bevy_pbr/src/meshlet/meshlet_cull_shared.wgsl`.
The game uses ordinary meshes, not meshlets.
The late mesh preprocessing pass in 0.18.1 also binds its indirect dispatch
buffer as writable storage, which fails wgpu validation when occlusion is on.
`bevy_pbr/src/render/{gpu_preprocess.rs,mesh_preprocess.wgsl}` now declares that
binding read-only in the late pass. Its counters are plain `u32` in that shader
variant, preserving the same buffer layout; the early producer retains atomic
counters and writable storage. No dispatch count or culling decision changes.
`bevy_pbr` is the unmodified crates.io 0.18.1 source except for the changes `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 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. it through the workspace `[patch.crates-io]` entry; the Cargo registry is untouched.
+3328
View File
File diff suppressed because it is too large. Load diff
+160
View File
@@ -0,0 +1,160 @@
# 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_core_pipeline"
version = "0.18.1"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Provides a core render pipeline for 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]
tonemapping_luts = [
"bevy_image/ktx2",
"bevy_image/zstd",
]
trace = []
webgl = []
webgpu = []
[lib]
name = "bevy_core_pipeline"
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_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_math]
version = "0.18.0"
[dependencies.bevy_platform]
version = "0.18.0"
features = [
"std",
"serialize",
]
default-features = false
[dependencies.bevy_reflect]
version = "0.18.0"
[dependencies.bevy_render]
version = "0.18.0"
[dependencies.bevy_shader]
version = "0.18.0"
[dependencies.bevy_transform]
version = "0.18.0"
[dependencies.bevy_utils]
version = "0.18.0"
[dependencies.bevy_window]
version = "0.18.0"
[dependencies.bitflags]
version = "2.3"
[dependencies.nonmax]
version = "0.5"
[dependencies.radsort]
version = "0.1"
[dependencies.smallvec]
version = "1"
default-features = false
[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)"]
+52
View File
@@ -0,0 +1,52 @@
[package]
name = "bevy_core_pipeline"
version = "0.18.1"
edition = "2024"
description = "Provides a core render pipeline for Bevy Engine."
homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[features]
trace = []
webgl = []
webgpu = []
tonemapping_luts = ["bevy_image/ktx2", "bevy_image/zstd"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.18.0" }
bevy_asset = { path = "../bevy_asset", version = "0.18.0" }
bevy_color = { path = "../bevy_color", version = "0.18.0" }
bevy_derive = { path = "../bevy_derive", version = "0.18.0" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.18.0" }
bevy_ecs = { path = "../bevy_ecs", version = "0.18.0" }
bevy_image = { path = "../bevy_image", version = "0.18.0" }
bevy_camera = { path = "../bevy_camera", version = "0.18.0" }
bevy_reflect = { path = "../bevy_reflect", version = "0.18.0" }
bevy_shader = { path = "../bevy_shader", version = "0.18.0" }
bevy_render = { path = "../bevy_render", version = "0.18.0" }
bevy_transform = { path = "../bevy_transform", version = "0.18.0" }
bevy_math = { path = "../bevy_math", version = "0.18.0" }
bevy_utils = { path = "../bevy_utils", version = "0.18.0" }
bevy_window = { path = "../bevy_window", version = "0.18.0" }
bevy_platform = { path = "../bevy_platform", version = "0.18.0", default-features = false, features = [
"std",
"serialize",
] }
bitflags = "2.3"
radsort = "0.1"
nonmax = "0.5"
smallvec = { version = "1", default-features = false }
thiserror = { version = "2", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] }
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
all-features = true
features = ["bevy_image/zstd_rust"]
+176
View File
@@ -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
+19
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
# Bevy Core Pipeline
[![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_core_pipeline.svg)](https://crates.io/crates/bevy_core_pipeline)
[![Downloads](https://img.shields.io/crates/d/bevy_core_pipeline.svg)](https://crates.io/crates/bevy_core_pipeline)
[![Docs](https://docs.rs/bevy_core_pipeline/badge.svg)](https://docs.rs/bevy_core_pipeline/latest/bevy_core_pipeline/)
[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy)
+9
View File
@@ -0,0 +1,9 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(0) var in_texture: texture_2d<f32>;
@group(0) @binding(1) var in_sampler: sampler;
@fragment
fn fs_main(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
return textureSample(in_texture, in_sampler, in.uv);
}
+115
View File
@@ -0,0 +1,115 @@
use crate::FullscreenShader;
use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle};
use bevy_ecs::prelude::*;
use bevy_render::{
render_resource::{
binding_types::{sampler, texture_2d},
*,
},
renderer::RenderDevice,
RenderApp, RenderStartup,
};
use bevy_shader::Shader;
use bevy_utils::default;
/// Adds support for specialized "blit pipelines", which can be used to write one texture to another.
pub struct BlitPlugin;
impl Plugin for BlitPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "blit.wgsl");
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.allow_ambiguous_resource::<SpecializedRenderPipelines<BlitPipeline>>()
.init_resource::<SpecializedRenderPipelines<BlitPipeline>>()
.add_systems(RenderStartup, init_blit_pipeline);
}
}
#[derive(Resource)]
pub struct BlitPipeline {
pub layout: BindGroupLayoutDescriptor,
pub sampler: Sampler,
pub fullscreen_shader: FullscreenShader,
pub fragment_shader: Handle<Shader>,
}
pub fn init_blit_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
) {
let layout = BindGroupLayoutDescriptor::new(
"blit_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: false }),
sampler(SamplerBindingType::NonFiltering),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
commands.insert_resource(BlitPipeline {
layout,
sampler,
fullscreen_shader: fullscreen_shader.clone(),
fragment_shader: load_embedded_asset!(asset_server.as_ref(), "blit.wgsl"),
});
}
impl BlitPipeline {
pub fn create_bind_group(
&self,
render_device: &RenderDevice,
src_texture: &TextureView,
pipeline_cache: &PipelineCache,
) -> BindGroup {
render_device.create_bind_group(
None,
&pipeline_cache.get_bind_group_layout(&self.layout),
&BindGroupEntries::sequential((src_texture, &self.sampler)),
)
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct BlitPipelineKey {
pub texture_format: TextureFormat,
pub blend_state: Option<BlendState>,
pub samples: u32,
}
impl SpecializedRenderPipeline for BlitPipeline {
type Key = BlitPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("blit pipeline".into()),
layout: vec![self.layout.clone()],
vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: key.blend_state,
write_mask: ColorWrites::ALL,
})],
..default()
}),
multisample: MultisampleState {
count: key.samples,
..default()
},
..default()
}
}
}
@@ -0,0 +1,106 @@
use crate::core_2d::Opaque2d;
use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use super::AlphaMask2d;
/// A [`bevy_render::render_graph::Node`] that runs the
/// [`Opaque2d`] [`ViewBinnedRenderPhases`] and [`AlphaMask2d`] [`ViewBinnedRenderPhases`]
#[derive(Default)]
pub struct MainOpaquePass2dNode;
impl ViewNode for MainOpaquePass2dNode {
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(opaque_phases), Some(alpha_mask_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque2d>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask2d>>(),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
let (Some(opaque_phase), Some(alpha_mask_phase)) = (
opaque_phases.get(&view.retained_view_entity),
alpha_mask_phases.get(&view.retained_view_entity),
) else {
return Ok(());
};
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _main_opaque_pass_2d_span = info_span!("main_opaque_pass_2d").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_opaque_pass_2d_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_opaque_pass_2d"),
color_attachments: &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, "main_opaque_pass_2d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered();
if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the 2d opaque phase {err:?}");
}
}
// Alpha mask draws
if !alpha_mask_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_main_pass_2d_span = info_span!("alpha_mask_main_pass_2d").entered();
if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the 2d alpha mask phase {err:?}");
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
command_encoder.finish()
});
Ok(())
}
}
@@ -0,0 +1,120 @@
use crate::core_2d::Transparent2d;
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewSortedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
#[derive(Default)]
pub struct MainTransparentPass2dNode {}
impl ViewNode for MainTransparentPass2dNode {
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): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(transparent_phases) =
world.get_resource::<ViewSortedRenderPhases<Transparent2d>>()
else {
return Ok(());
};
let view_entity = graph.view_entity();
let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
// NOTE: For the transparent pass we load the depth buffer. There should be no
// need to write to it, but store is set to `true` as a workaround for issue #3776,
// https://github.com/bevyengine/bevy/issues/3776
// so that wgpu does not clear the depth buffer.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
render_context.add_command_buffer_generation_task(move |render_device| {
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_transparent_pass_2d_command_encoder"),
});
// This needs to run at least once to clear the background color, even if there are no items to render
{
#[cfg(feature = "trace")]
let _main_pass_2d = info_span!("main_transparent_pass_2d").entered();
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_transparent_pass_2d"),
color_attachments: &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, "main_transparent_pass_2d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if !transparent_phase.items.is_empty() {
#[cfg(feature = "trace")]
let _transparent_main_pass_2d_span =
info_span!("transparent_main_pass_2d").entered();
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity)
{
error!(
"Error encountered while rendering the transparent 2D phase {err:?}"
);
}
}
pass_span.end(&mut render_pass);
}
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered();
let pass_descriptor = RenderPassDescriptor {
label: Some("reset_viewport_pass_2d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
command_encoder.begin_render_pass(&pass_descriptor);
}
command_encoder.finish()
});
Ok(())
}
}
+508
View File
@@ -0,0 +1,508 @@
mod main_opaque_pass_2d_node;
mod main_transparent_pass_2d_node;
pub mod graph {
use bevy_render::render_graph::{RenderLabel, RenderSubGraph};
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)]
pub struct Core2d;
pub mod input {
pub const VIEW_ENTITY: &str = "view_entity";
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub enum Node2d {
MsaaWriteback,
StartMainPass,
MainOpaquePass,
MainTransparentPass,
EndMainPass,
Wireframe,
StartMainPassPostProcessing,
Bloom,
PostProcessing,
Tonemapping,
Fxaa,
Smaa,
Upscaling,
ContrastAdaptiveSharpening,
EndMainPassPostProcessing,
}
}
use core::ops::Range;
use bevy_asset::UntypedAssetId;
use bevy_camera::{Camera, Camera2d};
use bevy_image::ToExtents;
use bevy_platform::collections::{HashMap, HashSet};
use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingMode,
camera::CameraRenderGraph,
render_phase::PhaseItemBatchSetKey,
view::{ExtractedView, RetainedViewEntity},
};
pub use main_opaque_pass_2d_node::*;
pub use main_transparent_pass_2d_node::*;
use crate::{
tonemapping::{DebandDither, Tonemapping, TonemappingNode},
upscaling::UpscalingNode,
};
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_math::FloatOrd;
use bevy_render::{
camera::ExtractedCamera,
extract_component::ExtractComponentPlugin,
render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner},
render_phase::{
sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,
ViewSortedRenderPhases,
},
render_resource::{
BindGroupId, CachedRenderPipelineId, TextureDescriptor, TextureDimension, TextureFormat,
TextureUsages,
},
renderer::RenderDevice,
sync_world::MainEntity,
texture::TextureCache,
view::{Msaa, ViewDepthTexture},
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
};
use self::graph::{Core2d, Node2d};
pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
pub struct Core2dPlugin;
impl Plugin for Core2dPlugin {
fn build(&self, app: &mut App) {
app.register_required_components::<Camera2d, DebandDither>()
.register_required_components_with::<Camera2d, CameraRenderGraph>(|| {
CameraRenderGraph::new(Core2d)
})
.register_required_components_with::<Camera2d, Tonemapping>(|| Tonemapping::None)
.add_plugins(ExtractComponentPlugin::<Camera2d>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<DrawFunctions<Opaque2d>>()
.init_resource::<DrawFunctions<AlphaMask2d>>()
.init_resource::<DrawFunctions<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Transparent2d>>()
.init_resource::<ViewBinnedRenderPhases<Opaque2d>>()
.init_resource::<ViewBinnedRenderPhases<AlphaMask2d>>()
.add_systems(ExtractSchedule, extract_core_2d_camera_phases)
.add_systems(
Render,
(
sort_phase_system::<Transparent2d>.in_set(RenderSystems::PhaseSort),
prepare_core_2d_depth_textures.in_set(RenderSystems::PrepareResources),
),
);
render_app
.add_render_sub_graph(Core2d)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::StartMainPass)
.add_render_graph_node::<ViewNodeRunner<MainOpaquePass2dNode>>(
Core2d,
Node2d::MainOpaquePass,
)
.add_render_graph_node::<ViewNodeRunner<MainTransparentPass2dNode>>(
Core2d,
Node2d::MainTransparentPass,
)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::EndMainPass)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::StartMainPassPostProcessing)
.add_render_graph_node::<ViewNodeRunner<TonemappingNode>>(Core2d, Node2d::Tonemapping)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::EndMainPassPostProcessing)
.add_render_graph_node::<ViewNodeRunner<UpscalingNode>>(Core2d, Node2d::Upscaling)
.add_render_graph_edges(
Core2d,
(
Node2d::StartMainPass,
Node2d::MainOpaquePass,
Node2d::MainTransparentPass,
Node2d::EndMainPass,
Node2d::StartMainPassPostProcessing,
Node2d::Tonemapping,
Node2d::EndMainPassPostProcessing,
Node2d::Upscaling,
),
);
}
}
/// Opaque 2D [`BinnedPhaseItem`]s.
pub struct Opaque2d {
/// 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: BatchSetKey2d,
/// The key, which determines which can be batched.
pub bin_key: Opaque2dBinKey,
/// 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<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque2dBinKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
pub material_bind_group_id: Option<BindGroupId>,
}
impl PhaseItem for Opaque2d {
#[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.bin_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque2d {
// Since 2D meshes presently can't be multidrawn, the batch set key is
// irrelevant.
type BatchSetKey = BatchSetKey2d;
type BinKey = Opaque2dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Opaque2d {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
/// 2D meshes aren't currently multi-drawn together, so this batch set key only
/// stores whether the mesh is indexed.
#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct BatchSetKey2d {
/// True if the mesh is indexed.
pub indexed: bool,
}
impl PhaseItemBatchSetKey for BatchSetKey2d {
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for Opaque2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.bin_key.pipeline
}
}
/// Alpha mask 2D [`BinnedPhaseItem`]s.
pub struct AlphaMask2d {
/// 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: BatchSetKey2d,
/// The key, which determines which can be batched.
pub bin_key: AlphaMask2dBinKey,
/// 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<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AlphaMask2dBinKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
pub material_bind_group_id: Option<BindGroupId>,
}
impl PhaseItem for AlphaMask2d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.bin_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask2d {
// Since 2D meshes presently can't be multidrawn, the batch set key is
// irrelevant.
type BatchSetKey = BatchSetKey2d;
type BinKey = AlphaMask2dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
AlphaMask2d {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.bin_key.pipeline
}
}
/// Transparent 2D [`SortedPhaseItem`]s.
pub struct Transparent2d {
pub sort_key: FloatOrd,
pub entity: (Entity, MainEntity),
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extracted_index: usize,
pub extra_index: PhaseItemExtraIndex,
/// Whether the mesh in question is indexed (uses an index buffer in
/// addition to its vertex buffer).
pub indexed: bool,
}
impl PhaseItem for Transparent2d {
#[inline]
fn entity(&self) -> Entity {
self.entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&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<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl SortedPhaseItem for Transparent2d {
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn sort(items: &mut [Self]) {
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
radsort::sort_by_key(items, |item| item.sort_key().0);
}
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for Transparent2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub fn extract_core_2d_camera_phases(
mut transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_2d_phases: ResMut<ViewBinnedRenderPhases<Opaque2d>>,
mut alpha_mask_2d_phases: ResMut<ViewBinnedRenderPhases<AlphaMask2d>>,
cameras_2d: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<HashSet<RetainedViewEntity>>,
) {
live_entities.clear();
for (main_entity, camera) in &cameras_2d {
if !camera.is_active {
continue;
}
// This is the main 2D camera, so we use the first subview index (0).
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
transparent_2d_phases.insert_or_clear(retained_view_entity);
opaque_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
alpha_mask_2d_phases
.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
live_entities.insert(retained_view_entity);
}
// Clear out all dead views.
transparent_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
opaque_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
alpha_mask_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
}
pub fn prepare_core_2d_depth_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
transparent_2d_phases: Res<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: Res<ViewBinnedRenderPhases<Opaque2d>>,
views_2d: Query<(Entity, &ExtractedCamera, &ExtractedView, &Msaa), (With<Camera2d>,)>,
) {
let mut textures = <HashMap<_, _>>::default();
for (view, camera, extracted_view, msaa) in &views_2d {
if !opaque_2d_phases.contains_key(&extracted_view.retained_view_entity)
|| !transparent_2d_phases.contains_key(&extracted_view.retained_view_entity)
{
continue;
};
let Some(physical_target_size) = camera.physical_target_size else {
continue;
};
let cached_texture = textures
.entry(camera.target.clone())
.or_insert_with(|| {
let descriptor = TextureDescriptor {
label: Some("view_depth_texture"),
// The size of the depth texture
size: physical_target_size.to_extents(),
mip_level_count: 1,
sample_count: msaa.samples(),
dimension: TextureDimension::D2,
format: CORE_2D_DEPTH_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
};
texture_cache.get(&render_device, descriptor)
})
.clone();
commands
.entity(view)
.insert(ViewDepthTexture::new(cached_texture, Some(0.0)));
}
}
@@ -0,0 +1,142 @@
use crate::{
core_3d::Opaque3d,
skybox::{SkyboxBindGroup, SkyboxPipelineId},
};
use bevy_camera::{MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use super::AlphaMask3d;
/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`]
/// [`ViewBinnedRenderPhases`]s.
#[derive(Default)]
pub struct MainOpaquePass3dNode;
impl ViewNode for MainOpaquePass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
Option<&'static SkyboxPipelineId>,
Option<&'static SkyboxBindGroup>,
&'static ViewUniformOffset,
Option<&'static MainPassResolutionOverride>,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
camera,
extracted_view,
target,
depth,
skybox_pipeline,
skybox_bind_group,
view_uniform_offset,
resolution_override,
): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_phases), Some(alpha_mask_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3d>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3d>>(),
) else {
return Ok(());
};
let (Some(opaque_phase), Some(alpha_mask_phase)) = (
opaque_phases.get(&extracted_view.retained_view_entity),
alpha_mask_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_opaque_pass_3d_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_opaque_pass_3d"),
color_attachments: &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, "main_opaque_pass_3d");
if let Some(viewport) =
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
{
render_pass.set_camera_viewport(&viewport);
}
// Opaque draws
if !opaque_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered();
if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque phase {err:?}");
}
}
// Alpha draws
if !alpha_mask_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered();
if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the alpha mask phase {err:?}");
}
}
// Skybox draw using a fullscreen triangle
if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) =
(skybox_pipeline, skybox_bind_group)
{
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
&skybox_bind_group.0,
&[view_uniform_offset.offset, skybox_bind_group.1],
);
render_pass.draw(0..3, 0..1);
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
command_encoder.finish()
});
Ok(())
}
}
@@ -0,0 +1,167 @@
use super::ViewTransmissionTexture;
use crate::core_3d::Transmissive3d;
use bevy_camera::{Camera3d, MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::ToExtents;
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
render_resource::{RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use core::ops::Range;
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`]
/// [`ViewSortedRenderPhases`].
#[derive(Default)]
pub struct MainTransmissivePass3dNode;
impl ViewNode for MainTransmissivePass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static Camera3d,
&'static ViewTarget,
Option<&'static ViewTransmissionTexture>,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let Some(transmissive_phases) =
world.get_resource::<ViewSortedRenderPhases<Transmissive3d>>()
else {
return Ok(());
};
let Some(transmissive_phase) = transmissive_phases.get(&view.retained_view_entity) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let physical_target_size = camera.physical_target_size.unwrap();
let render_pass_descriptor = RenderPassDescriptor {
label: Some("main_transmissive_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
timestamp_writes: None,
occlusion_query_set: None,
};
// Run the transmissive pass, sorted back-to-front
// NOTE: Scoped to drop the mutable borrow of render_context
#[cfg(feature = "trace")]
let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered();
if !transmissive_phase.items.is_empty() {
let screen_space_specular_transmission_steps =
camera_3d.screen_space_specular_transmission_steps;
if screen_space_specular_transmission_steps > 0 {
let transmission =
transmission.expect("`ViewTransmissionTexture` should exist at this point");
// `transmissive_phase.items` are depth sorted, so we split them into N = `screen_space_specular_transmission_steps`
// ranges, rendering them back-to-front in multiple steps, allowing multiple levels of transparency.
//
// Note: For the sake of simplicity, we currently split items evenly among steps. In the future, we
// might want to use a more sophisticated heuristic (e.g. based on view bounds, or with an exponential
// falloff so that nearby objects have more levels of transparency available to them)
for range in split_range(
0..transmissive_phase.items.len(),
screen_space_specular_transmission_steps,
) {
// Copy the main texture to the transmission texture, allowing to use the color output of the
// previous step (or of the `Opaque3d` phase, for the first step) as a transmissive color input
render_context.command_encoder().copy_texture_to_texture(
target.main_texture().as_image_copy(),
transmission.texture.as_image_copy(),
physical_target_size.to_extents(),
);
let mut render_pass =
render_context.begin_tracked_render_pass(render_pass_descriptor.clone());
let pass_span =
diagnostics.pass_span(&mut render_pass, "main_transmissive_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// render items in range
if let Err(err) =
transmissive_phase.render_range(&mut render_pass, world, view_entity, range)
{
error!("Error encountered while rendering the transmissive phase {err:?}");
}
pass_span.end(&mut render_pass);
}
} else {
let mut render_pass =
render_context.begin_tracked_render_pass(render_pass_descriptor);
let pass_span =
diagnostics.pass_span(&mut render_pass, "main_transmissive_pass_3d");
if let Some(viewport) = Viewport::from_viewport_and_override(
camera.viewport.as_ref(),
resolution_override,
) {
render_pass.set_camera_viewport(&viewport);
}
if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the transmissive phase {err:?}");
}
pass_span.end(&mut render_pass);
}
}
Ok(())
}
}
/// Splits a [`Range`] into at most `max_num_splits` sub-ranges without overlaps
///
/// Properly takes into account remainders of inexact divisions (by adding extra
/// elements to the initial sub-ranges as needed)
fn split_range(range: Range<usize>, max_num_splits: usize) -> impl Iterator<Item = Range<usize>> {
let len = range.end - range.start;
assert!(len > 0, "to be split, a range must not be empty");
assert!(max_num_splits > 0, "max_num_splits must be at least 1");
let num_splits = max_num_splits.min(len);
let step = len / num_splits;
let mut rem = len % num_splits;
let mut start = range.start;
(0..num_splits).map(move |_| {
let extra = if rem > 0 {
rem -= 1;
1
} else {
0
};
let end = (start + step + extra).min(range.end);
let result = start..end;
start = end;
result
})
}
@@ -0,0 +1,107 @@
use crate::core_3d::Transparent3d;
use bevy_camera::{MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
render_resource::{RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
/// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`]
/// [`ViewSortedRenderPhases`].
#[derive(Default)]
pub struct MainTransparentPass3dNode;
impl ViewNode for MainTransparentPass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, target, depth, resolution_override): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let Some(transparent_phases) =
world.get_resource::<ViewSortedRenderPhases<Transparent3d>>()
else {
return Ok(());
};
let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else {
return Ok(());
};
if !transparent_phase.items.is_empty() {
// Run the transparent pass, sorted back-to-front
// NOTE: Scoped to drop the mutable borrow of render_context
#[cfg(feature = "trace")]
let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered();
let diagnostics = render_context.diagnostic_recorder();
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("main_transparent_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
// NOTE: For the transparent pass we load the depth buffer. There should be no
// need to write to it, but store is set to `true` as a workaround for issue #3776,
// https://github.com/bevyengine/bevy/issues/3776
// so that wgpu does not clear the depth buffer.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
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, "main_transparent_pass_3d");
if let Some(viewport) =
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
{
render_pass.set_camera_viewport(&viewport);
}
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the transparent phase {err:?}");
}
pass_span.end(&mut render_pass);
}
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered();
let pass_descriptor = RenderPassDescriptor {
label: Some("reset_viewport_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
}
Ok(())
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,18 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(0)
var material_id_texture: texture_2d<u32>;
struct FragmentOutput {
@builtin(frag_depth) frag_depth: f32,
}
@fragment
fn fragment(in: FullscreenVertexOutput) -> FragmentOutput {
var out: FragmentOutput;
// Depth is stored as unorm, so we are dividing the u8 by 255.0 here.
out.frag_depth = f32(textureLoad(material_id_texture, vec2<i32>(in.position.xy), 0).x) / 255.0;
return out;
}
@@ -0,0 +1,192 @@
use crate::{
prepass::{DeferredPrepass, ViewPrepassTextures},
FullscreenShader,
};
use bevy_app::prelude::*;
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer};
use bevy_ecs::prelude::*;
use bevy_image::ToExtents;
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_resource::{binding_types::texture_2d, *},
renderer::RenderDevice,
texture::{CachedTexture, TextureCache},
view::ViewTarget,
Render, RenderApp, RenderStartup, RenderSystems,
};
use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT;
use bevy_ecs::query::QueryItem;
use bevy_render::{
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
renderer::RenderContext,
};
use bevy_utils::default;
pub struct CopyDeferredLightingIdPlugin;
impl Plugin for CopyDeferredLightingIdPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "copy_deferred_lighting_id.wgsl");
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.add_systems(RenderStartup, init_copy_deferred_lighting_id_pipeline)
.add_systems(
Render,
(prepare_deferred_lighting_id_textures.in_set(RenderSystems::PrepareResources),),
);
}
}
#[derive(Default)]
pub struct CopyDeferredLightingIdNode;
impl CopyDeferredLightingIdNode {
pub const NAME: &'static str = "copy_deferred_lighting_id";
}
impl ViewNode for CopyDeferredLightingIdNode {
type ViewQuery = (
&'static ViewTarget,
&'static ViewPrepassTextures,
&'static DeferredLightingIdDepthTexture,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(_view_target, view_prepass_textures, deferred_lighting_id_depth_texture): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let copy_deferred_lighting_id_pipeline = world.resource::<CopyDeferredLightingIdPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) =
pipeline_cache.get_render_pipeline(copy_deferred_lighting_id_pipeline.pipeline_id)
else {
return Ok(());
};
let Some(deferred_lighting_pass_id_texture) =
&view_prepass_textures.deferred_lighting_pass_id
else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let bind_group = render_context.render_device().create_bind_group(
"copy_deferred_lighting_id_bind_group",
&pipeline_cache.get_bind_group_layout(&copy_deferred_lighting_id_pipeline.layout),
&BindGroupEntries::single(&deferred_lighting_pass_id_texture.texture.default_view),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("copy_deferred_lighting_id"),
color_attachments: &[],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &deferred_lighting_id_depth_texture.texture.default_view,
depth_ops: Some(Operations {
load: LoadOp::Clear(0.0),
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
let pass_span = diagnostics.pass_span(&mut render_pass, "copy_deferred_lighting_id");
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[]);
render_pass.draw(0..3, 0..1);
pass_span.end(&mut render_pass);
Ok(())
}
}
#[derive(Resource)]
struct CopyDeferredLightingIdPipeline {
layout: BindGroupLayoutDescriptor,
pipeline_id: CachedRenderPipelineId,
}
pub fn init_copy_deferred_lighting_id_pipeline(
mut commands: Commands,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = BindGroupLayoutDescriptor::new(
"copy_deferred_lighting_id_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::FRAGMENT,
texture_2d(TextureSampleType::Uint),
),
);
let vertex_state = fullscreen_shader.to_vertex_state();
let shader = load_embedded_asset!(asset_server.as_ref(), "copy_deferred_lighting_id.wgsl");
let pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("copy_deferred_lighting_id_pipeline".into()),
layout: vec![layout.clone()],
vertex: vertex_state,
fragment: Some(FragmentState {
shader,
..default()
}),
depth_stencil: Some(DepthStencilState {
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::Always,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
..default()
});
commands.insert_resource(CopyDeferredLightingIdPipeline {
layout,
pipeline_id,
});
}
#[derive(Component)]
pub struct DeferredLightingIdDepthTexture {
pub texture: CachedTexture,
}
fn prepare_deferred_lighting_id_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<DeferredPrepass>>,
) {
for (entity, camera) in &views {
if let Some(physical_target_size) = camera.physical_target_size {
let texture_descriptor = TextureDescriptor {
label: Some("deferred_lighting_id_depth_texture_a"),
size: physical_target_size.to_extents(),
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
view_formats: &[],
};
let texture = texture_cache.get(&render_device, texture_descriptor);
commands
.entity(entity)
.insert(DeferredLightingIdDepthTexture { texture });
}
}
}
+186
View File
@@ -0,0 +1,186 @@
pub mod copy_lighting_id;
pub mod node;
use core::ops::Range;
use crate::prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey};
use bevy_ecs::prelude::*;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
PhaseItemExtraIndex,
},
render_resource::{CachedRenderPipelineId, TextureFormat},
};
pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint;
pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint;
pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth16Unorm;
/// Opaque phase of the 3D Deferred pass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all 3D meshes with materials that have no transparency.
#[derive(PartialEq, Eq, Hash)]
pub struct Opaque3dDeferred {
/// 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: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for Opaque3dDeferred {
#[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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&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<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque3dDeferred {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for Opaque3dDeferred {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
/// Alpha mask phase of the 3D Deferred pass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dDeferred {
/// 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: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for AlphaMask3dDeferred {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
#[inline]
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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&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<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask3dDeferred {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
+273
View File
@@ -0,0 +1,273 @@
use bevy_camera::{MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode;
use bevy_render::view::{ExtractedView, NoIndirectDrawing};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::ViewDepthTexture,
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use crate::prepass::ViewPrepassTextures;
use super::{AlphaMask3dDeferred, Opaque3dDeferred};
/// The phase of the deferred prepass that draws meshes that were visible last
/// frame.
///
/// If occlusion culling isn't in use, this prepass simply draws all meshes.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct EarlyDeferredGBufferPrepassNode;
impl ViewNode for EarlyDeferredGBufferPrepassNode {
type ViewQuery = <LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery;
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_deferred_prepass(
graph,
render_context,
view_query,
false,
world,
"early deferred prepass",
)
}
}
/// The phase of the prepass that runs after occlusion culling against the
/// meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this is a no-op.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct LateDeferredGBufferPrepassNode;
impl ViewNode for LateDeferredGBufferPrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
Option<&'static MainPassResolutionOverride>,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (.., occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
run_deferred_prepass(
graph,
render_context,
view_query,
true,
world,
"late deferred prepass",
)
}
}
/// Runs the deferred prepass that draws all meshes to the depth buffer and
/// G-buffers.
///
/// If occlusion culling isn't in use, and a prepass is enabled, then there's
/// only one prepass. If occlusion culling is in use, then any prepass is split
/// into two: an *early* prepass and a *late* prepass. The early prepass draws
/// what was visible last frame, and the last prepass performs occlusion culling
/// against a conservative hierarchical Z buffer before drawing unoccluded
/// meshes.
fn run_deferred_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem<
'w,
'_,
<LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery,
>,
is_late: bool,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(),
) else {
return Ok(());
};
let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = (
opaque_deferred_phases.get(&extracted_view.retained_view_entity),
alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let diagnostic = render_context.diagnostic_recorder();
let mut color_attachments = vec![];
color_attachments.push(
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
);
color_attachments.push(
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
);
// If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors:
// Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format.
// Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT.
// Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9
// For webgl2 we fallback to manually clearing
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if !is_late {
if let Some(deferred_texture) = &view_prepass_textures.deferred {
render_context.command_encoder().clear_texture(
&deferred_texture.texture.texture,
&bevy_render::render_resource::ImageSubresourceRange::default(),
);
}
}
color_attachments.push(
view_prepass_textures
.deferred
.as_ref()
.map(|deferred_texture| {
if is_late {
deferred_texture.get_attachment()
} else {
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
{
bevy_render::render_resource::RenderPassColorAttachment {
view: &deferred_texture.texture.default_view,
resolve_target: None,
ops: bevy_render::render_resource::Operations {
load: bevy_render::render_resource::LoadOp::Load,
store: StoreOp::Store,
},
depth_slice: None,
}
}
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
deferred_texture.get_attachment()
}
}),
);
color_attachments.push(
view_prepass_textures
.deferred_lighting_pass_id
.as_ref()
.map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()),
);
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &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 = diagnostic.pass_span(&mut render_pass, label);
if let Some(viewport) =
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
{
render_pass.set_camera_viewport(&viewport);
}
// Opaque draws
if !opaque_deferred_phase.multidrawable_meshes.is_empty()
|| !opaque_deferred_phase.batchable_meshes.is_empty()
|| !opaque_deferred_phase.unbatchable_meshes.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque deferred phase {err:?}");
}
}
// Alpha masked draws
if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
if let Err(err) = alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the alpha mask deferred phase {err:?}");
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
// After rendering to the view depth texture, copy it to the prepass depth texture
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
command_encoder.finish()
});
Ok(())
}
@@ -0,0 +1,403 @@
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(0) var mip_0: texture_storage_2d<r64uint, read>;
#else
#ifdef MESHLET
@group(0) @binding(0) var mip_0: texture_storage_2d<r32uint, read>;
#else // MESHLET
#ifdef MULTISAMPLE
@group(0) @binding(0) var mip_0: texture_depth_multisampled_2d;
#else // MULTISAMPLE
@group(0) @binding(0) var mip_0: texture_depth_2d;
#endif // MULTISAMPLE
#endif // MESHLET
#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(1) var mip_1: texture_storage_2d<r32float, write>;
@group(0) @binding(2) var mip_2: texture_storage_2d<r32float, write>;
@group(0) @binding(3) var mip_3: texture_storage_2d<r32float, write>;
@group(0) @binding(4) var mip_4: texture_storage_2d<r32float, write>;
@group(0) @binding(5) var mip_5: texture_storage_2d<r32float, write>;
@group(0) @binding(6) var mip_6: texture_storage_2d<r32float, read_write>;
@group(0) @binding(7) var mip_7: texture_storage_2d<r32float, write>;
@group(0) @binding(8) var mip_8: texture_storage_2d<r32float, write>;
@group(0) @binding(9) var mip_9: texture_storage_2d<r32float, write>;
@group(0) @binding(10) var mip_10: texture_storage_2d<r32float, write>;
@group(0) @binding(11) var mip_11: texture_storage_2d<r32float, write>;
@group(0) @binding(12) var mip_12: texture_storage_2d<r32float, write>;
@group(0) @binding(13) var samplr: sampler;
struct Constants { max_mip_level: u32 }
var<push_constant> constants: Constants;
/// Generates a hierarchical depth buffer.
/// Based on FidelityFX SPD v2.1 https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/sdk/include/FidelityFX/gpu/spd/ffx_spd.h#L528
///
/// `mip_0` may be of any size, but `mip_1` and down must have side lengths that
/// are powers of two.
// TODO:
// * Subgroup support
// * True single pass downsampling
var<workgroup> intermediate_memory: array<array<f32, 16>, 16>;
@compute
@workgroup_size(256, 1, 1)
fn downsample_depth_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);
downsample_mips_0_and_1(x, y, workgroup_id.xy, local_invocation_index);
downsample_mips_2_to_5(x, y, workgroup_id.xy, local_invocation_index);
}
@compute
@workgroup_size(256, 1, 1)
fn downsample_depth_second(@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);
downsample_mips_6_and_7(x, y);
downsample_mips_8_to_11(x, y, local_invocation_index);
}
fn downsample_mips_0_and_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
var v: vec4f;
var tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u);
var pix = vec2(workgroup_id * 32u) + vec2(x, y);
v[0] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[0]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u);
pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y);
v[1] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[1]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u + 32u);
pix = vec2(workgroup_id * 32u) + vec2(x, y + 16u);
v[2] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[2]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u + 32u);
pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y + 16u);
v[3] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[3]));
if constants.max_mip_level <= 1u { return; }
for (var i = 0u; i < 4u; i++) {
intermediate_memory[x][y] = v[i];
workgroupBarrier();
if local_invocation_index < 64u {
v[i] = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
pix = (workgroup_id * 16u) + vec2(
x + (i % 2u) * 8u,
y + (i / 2u) * 8u,
);
textureStore(mip_2, pix, vec4(v[i]));
}
workgroupBarrier();
}
if local_invocation_index < 64u {
intermediate_memory[x + 0u][y + 0u] = v[0];
intermediate_memory[x + 8u][y + 0u] = v[1];
intermediate_memory[x + 0u][y + 8u] = v[2];
intermediate_memory[x + 8u][y + 8u] = v[3];
}
}
fn downsample_mips_2_to_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if constants.max_mip_level <= 2u { return; }
workgroupBarrier();
downsample_mip_2(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 3u { return; }
workgroupBarrier();
downsample_mip_3(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 4u { return; }
workgroupBarrier();
downsample_mip_4(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 5u { return; }
workgroupBarrier();
downsample_mip_5(workgroup_id, local_invocation_index);
}
fn downsample_mip_2(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 64u {
let v = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
textureStore(mip_3, (workgroup_id * 8u) + vec2(x, y), vec4(v));
intermediate_memory[x * 2u + y % 2u][y * 2u] = v;
}
}
fn downsample_mip_3(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 16u {
let v = reduce_4(vec4(
intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u],
intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u],
));
textureStore(mip_4, (workgroup_id * 4u) + vec2(x, y), vec4(v));
intermediate_memory[x * 4u + y][y * 4u] = v;
}
}
fn downsample_mip_4(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 4u {
let v = reduce_4(vec4(
intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u],
intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u],
));
textureStore(mip_5, (workgroup_id * 2u) + vec2(x, y), vec4(v));
intermediate_memory[x + y * 2u][0u] = v;
}
}
fn downsample_mip_5(workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 1u {
let v = reduce_4(vec4(
intermediate_memory[0u][0u],
intermediate_memory[1u][0u],
intermediate_memory[2u][0u],
intermediate_memory[3u][0u],
));
textureStore(mip_6, workgroup_id, vec4(v));
}
}
fn downsample_mips_6_and_7(x: u32, y: u32) {
var v: vec4f;
var tex = vec2(x * 4u + 0u, y * 4u + 0u);
var pix = vec2(x * 2u + 0u, y * 2u + 0u);
v[0] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[0]));
tex = vec2(x * 4u + 2u, y * 4u + 0u);
pix = vec2(x * 2u + 1u, y * 2u + 0u);
v[1] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[1]));
tex = vec2(x * 4u + 0u, y * 4u + 2u);
pix = vec2(x * 2u + 0u, y * 2u + 1u);
v[2] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[2]));
tex = vec2(x * 4u + 2u, y * 4u + 2u);
pix = vec2(x * 2u + 1u, y * 2u + 1u);
v[3] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[3]));
if constants.max_mip_level <= 7u { return; }
let vr = reduce_4(v);
textureStore(mip_8, vec2(x, y), vec4(vr));
intermediate_memory[x][y] = vr;
}
fn downsample_mips_8_to_11(x: u32, y: u32, local_invocation_index: u32) {
if constants.max_mip_level <= 8u { return; }
workgroupBarrier();
downsample_mip_8(x, y, local_invocation_index);
if constants.max_mip_level <= 9u { return; }
workgroupBarrier();
downsample_mip_9(x, y, local_invocation_index);
if constants.max_mip_level <= 10u { return; }
workgroupBarrier();
downsample_mip_10(x, y, local_invocation_index);
if constants.max_mip_level <= 11u { return; }
workgroupBarrier();
downsample_mip_11(local_invocation_index);
}
fn downsample_mip_8(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 64u {
let v = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
textureStore(mip_9, vec2(x, y), vec4(v));
intermediate_memory[x * 2u + y % 2u][y * 2u] = v;
}
}
fn downsample_mip_9(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 16u {
let v = reduce_4(vec4(
intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u],
intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u],
));
textureStore(mip_10, vec2(x, y), vec4(v));
intermediate_memory[x * 4u + y][y * 4u] = v;
}
}
fn downsample_mip_10(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 4u {
let v = reduce_4(vec4(
intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u],
intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u],
));
textureStore(mip_11, vec2(x, y), vec4(v));
intermediate_memory[x + y * 2u][0u] = v;
}
}
fn downsample_mip_11(local_invocation_index: u32) {
if local_invocation_index < 1u {
let v = reduce_4(vec4(
intermediate_memory[0u][0u],
intermediate_memory[1u][0u],
intermediate_memory[2u][0u],
intermediate_memory[3u][0u],
));
textureStore(mip_12, vec2(0u, 0u), vec4(v));
}
}
fn remap_for_wave_reduction(a: u32) -> vec2u {
return vec2(
insertBits(extractBits(a, 2u, 3u), a, 0u, 1u),
insertBits(extractBits(a, 3u, 3u), extractBits(a, 1u, 2u), 0u, 2u),
);
}
fn reduce_load_mip_0(tex: vec2u) -> f32 {
let a = load_mip_0(tex.x, tex.y);
let b = load_mip_0(tex.x + 1u, tex.y);
let c = load_mip_0(tex.x, tex.y + 1u);
let d = load_mip_0(tex.x + 1u, tex.y + 1u);
return reduce_4(vec4(a, b, c, d));
}
fn reduce_load_mip_6(tex: vec2u) -> f32 {
return reduce_4(vec4(
textureLoad(mip_6, tex + vec2(0u, 0u)).r,
textureLoad(mip_6, tex + vec2(0u, 1u)).r,
textureLoad(mip_6, tex + vec2(1u, 0u)).r,
textureLoad(mip_6, tex + vec2(1u, 1u)).r,
));
}
// Loads the top mip level at virtual position (x, y).
//
// This is the value that *would be* returned from sampling a scaled depth
// buffer with side lengths rounded up to the next power of two, without
// actually constructing such a depth buffer.
//
// See the comments in `ViewDepthPyramid::downsample_depth` for more
// information.
fn load_mip_0(x: u32, y: u32) -> f32 {
let actual_size = textureDimensions(mip_0).xy;
let virtual_size = vec2<u32>(
next_power_of_two(actual_size.x),
next_power_of_two(actual_size.y)
);
let virtual_uv = (vec2<f32>(f32(x), f32(y)) + 0.5) / vec2<f32>(virtual_size);
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
let virtual_st = virtual_uv * vec2<f32>(actual_size);
let visibility = load_mip_0_meshlet(virtual_st, 32u);
return reduce_4(visibility);
#else // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
#ifdef MESHLET
let virtual_st = virtual_uv * vec2<f32>(actual_size);
let visibility = load_mip_0_meshlet(virtual_st, 0u);
return reduce_4(visibility);
#else // MESHLET
// Downsample the top level.
#ifdef MULTISAMPLE
// The top level is multisampled, so we need to loop over all the samples
// and reduce them to 1.
let virtual_st = virtual_uv * vec2<f32>(actual_size);
var result = load_mip_0_single_sample(virtual_st, 0);
let sample_count = i32(textureNumSamples(mip_0));
for (var sample = 1; sample < sample_count; sample += 1) {
result = min(result, load_mip_0_single_sample(virtual_st, sample));
}
return result;
#else // MULTISAMPLE
return reduce_4(textureGather(mip_0, samplr, virtual_uv));
#endif // MULTISAMPLE
#endif // MESHLET
#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
}
#ifdef MESHLET
// Loads a single 2×2 square of texels at the given position from the source
// image and returns all four (like `textureGather` does).
//
// `st` should be in texels, not in the [0, 1] range like UVs. That is, `st` is
// `uv * textureDimensions(mip_0).xy`.
fn load_mip_0_meshlet(st: vec2<f32>, shift: u32) -> vec4<f32> {
let st0 = vec2<u32>(floor(st - 0.5));
let st1 = st0 + 1u;
return vec4<f32>(
bitcast<f32>(u32(textureLoad(mip_0, vec2<u32>(st0.x, st0.y)).r) >> shift),
bitcast<f32>(u32(textureLoad(mip_0, vec2<u32>(st0.x, st1.y)).r) >> shift),
bitcast<f32>(u32(textureLoad(mip_0, vec2<u32>(st1.x, st0.y)).r) >> shift),
bitcast<f32>(u32(textureLoad(mip_0, vec2<u32>(st1.x, st1.y)).r) >> shift)
);
}
#endif // MESHLET
#ifdef MULTISAMPLE
// Loads a single 2×2 square of texels at the given position from the source
// image, reduces them, and returns the result.
//
// `st` should be in texels, not in the [0, 1] range like UVs. That is, `st` is
// `uv * textureDimensions(mip_0).xy`.
fn load_mip_0_single_sample(st: vec2<f32>, sample: i32) -> f32 {
let st0 = vec2<u32>(floor(st - 0.5));
let st1 = st0 + 1u;
let v = vec4<f32>(
textureLoad(mip_0, vec2<u32>(st0.x, st0.y), sample),
textureLoad(mip_0, vec2<u32>(st0.x, st1.y), sample),
textureLoad(mip_0, vec2<u32>(st1.x, st0.y), sample),
textureLoad(mip_0, vec2<u32>(st1.x, st1.y), sample)
);
return reduce_4(v);
}
#endif // MULTISAMPLE
fn reduce_4(v: vec4f) -> f32 {
return min(min(v.x, v.y), min(v.z, v.w));
}
// Returns the next power of two of x.
//
// If x is itself a power of two, this still returns the *next* power of two.
// This is different from Rust's `next_power_of_two` function.
fn next_power_of_two(x: u32) -> u32 {
return 1u << (32u - countLeadingZeros(x));
}
@@ -0,0 +1,811 @@
//! Downsampling of textures to produce mipmap levels.
//!
//! Currently, this module only supports generation of hierarchical Z buffers
//! for occlusion culling. Includes Bevy #22603's conservative power-of-two
//! depth pyramid fix for arbitrary viewport sizes.
use core::array;
use crate::core_3d::{
graph::{Core3d, Node3d},
prepare_core_3d_depth_textures,
};
use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
prelude::{resource_exists, Without},
query::{Or, QueryState, With},
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_math::{uvec2, UVec2, Vec4Swizzles as _};
use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingSupport,
render_resource::BindGroupLayoutDescriptor, RenderStartup,
};
use bevy_render::{
experimental::occlusion_culling::{
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
},
render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt},
render_resource::{
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedComputePipelineId, ComputePassDescriptor, ComputePipeline, ComputePipelineDescriptor,
Extent3d, IntoBinding, PipelineCache, PushConstantRange, Sampler, SamplerBindingType,
SamplerDescriptor, ShaderStages, SpecializedComputePipeline, SpecializedComputePipelines,
StorageTextureAccess, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat,
TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension,
},
renderer::{RenderContext, RenderDevice},
texture::TextureCache,
view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture},
Render, RenderApp, RenderSystems,
};
use bevy_shader::Shader;
use bevy_utils::default;
use bitflags::bitflags;
use tracing::debug;
/// Identifies the `downsample_depth.wgsl` shader.
#[derive(Resource, Deref)]
pub struct DownsampleDepthShader(Handle<Shader>);
/// The maximum number of mip levels that we can produce.
///
/// 2^12 is 4096, so that's the maximum size of the depth buffer that we
/// support.
pub const DEPTH_PYRAMID_MIP_COUNT: usize = 12;
/// A plugin that allows Bevy to repeatedly downsample textures to create
/// mipmaps.
///
/// Currently, this is only used for hierarchical Z buffer generation for the
/// purposes of occlusion culling.
pub struct MipGenerationPlugin;
impl Plugin for MipGenerationPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "downsample_depth.wgsl");
let downsample_depth_shader = load_embedded_asset!(app, "downsample_depth.wgsl");
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.insert_resource(DownsampleDepthShader(downsample_depth_shader))
.init_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::EarlyDownsampleDepth)
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::LateDownsampleDepth)
.add_render_graph_edges(
Core3d,
(
Node3d::EarlyPrepass,
Node3d::EarlyDeferredPrepass,
Node3d::EarlyDownsampleDepth,
Node3d::LatePrepass,
Node3d::LateDeferredPrepass,
),
)
.add_render_graph_edges(
Core3d,
(
Node3d::StartMainPassPostProcessing,
Node3d::LateDownsampleDepth,
Node3d::EndMainPassPostProcessing,
),
)
.add_systems(RenderStartup, init_depth_pyramid_dummy_texture)
.add_systems(
Render,
create_downsample_depth_pipelines.in_set(RenderSystems::Prepare),
)
.add_systems(
Render,
(
prepare_view_depth_pyramids,
prepare_downsample_depth_view_bind_groups,
)
.chain()
.in_set(RenderSystems::PrepareResources)
.run_if(resource_exists::<DownsampleDepthPipelines>)
.after(prepare_core_3d_depth_textures),
);
}
}
/// The nodes that produce a hierarchical Z-buffer, also known as a depth
/// pyramid.
///
/// This runs the single-pass downsampling (SPD) shader with the *min* filter in
/// order to generate a series of mipmaps for the Z buffer. The resulting
/// hierarchical Z-buffer can be used for occlusion culling.
///
/// There are two instances of this node. The *early* downsample depth pass is
/// the first hierarchical Z-buffer stage, which runs after the early prepass
/// and before the late prepass. It prepares the Z-buffer for the bounding box
/// tests that the late mesh preprocessing stage will perform. The *late*
/// downsample depth pass runs at the end of the main phase. It prepares the
/// Z-buffer for the occlusion culling that the early mesh preprocessing phase
/// of the *next* frame will perform.
///
/// This node won't do anything if occlusion culling isn't on.
pub struct DownsampleDepthNode {
/// The query that we use to find views that need occlusion culling for
/// their Z-buffer.
main_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<ViewDepthTexture>,
Option<Read<OcclusionCullingSubviewEntities>>,
)>,
/// The query that we use to find shadow maps that need occlusion culling.
shadow_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<OcclusionCullingSubview>,
)>,
}
impl FromWorld for DownsampleDepthNode {
fn from_world(world: &mut World) -> Self {
Self {
main_view_query: QueryState::new(world),
shadow_view_query: QueryState::new(world),
}
}
}
impl Node for DownsampleDepthNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.shadow_view_query.update_archetypes(world);
}
fn run<'w>(
&self,
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Ok((
view_depth_pyramid,
view_downsample_depth_bind_group,
view_depth_texture,
maybe_view_light_entities,
)) = self
.main_view_query
.get_manual(world, render_graph_context.view_entity())
else {
return Ok(());
};
// Downsample depth for the main Z-buffer.
downsample_depth(
render_graph_context,
render_context,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
uvec2(
view_depth_texture.texture.width(),
view_depth_texture.texture.height(),
),
view_depth_texture.texture.sample_count(),
)?;
// Downsample depth for shadow maps that have occlusion culling enabled.
if let Some(view_light_entities) = maybe_view_light_entities {
for &view_light_entity in &view_light_entities.0 {
let Ok((view_depth_pyramid, view_downsample_depth_bind_group, occlusion_culling)) =
self.shadow_view_query.get_manual(world, view_light_entity)
else {
continue;
};
downsample_depth(
render_graph_context,
render_context,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
UVec2::splat(occlusion_culling.depth_texture_size),
1,
)?;
}
}
Ok(())
}
}
/// Produces a depth pyramid from the current depth buffer for a single view.
/// The resulting depth pyramid can be used for occlusion testing.
fn downsample_depth<'w>(
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
view_depth_pyramid: &ViewDepthPyramid,
view_downsample_depth_bind_group: &ViewDownsampleDepthBindGroup,
view_size: UVec2,
sample_count: u32,
) -> Result<(), NodeRunError> {
let downsample_depth_pipelines = world.resource::<DownsampleDepthPipelines>();
let pipeline_cache = world.resource::<PipelineCache>();
// Despite the name "single-pass downsampling", we actually need two
// passes because of the lack of `coherent` buffers in WGPU/WGSL.
// Between each pass, there's an implicit synchronization barrier.
// Fetch the appropriate pipeline ID, depending on whether the depth
// buffer is multisampled or not.
let (Some(first_downsample_depth_pipeline_id), Some(second_downsample_depth_pipeline_id)) =
(if sample_count > 1 {
(
downsample_depth_pipelines.first_multisample.pipeline_id,
downsample_depth_pipelines.second_multisample.pipeline_id,
)
} else {
(
downsample_depth_pipelines.first.pipeline_id,
downsample_depth_pipelines.second.pipeline_id,
)
})
else {
return Ok(());
};
// Fetch the pipelines for the two passes.
let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = (
pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id),
pipeline_cache.get_compute_pipeline(second_downsample_depth_pipeline_id),
) else {
return Ok(());
};
// Run the depth downsampling.
view_depth_pyramid.downsample_depth(
&format!("{:?}", render_graph_context.label()),
render_context,
view_size,
view_downsample_depth_bind_group,
first_downsample_depth_pipeline,
second_downsample_depth_pipeline,
);
Ok(())
}
/// A single depth downsample pipeline.
#[derive(Resource)]
pub struct DownsampleDepthPipeline {
/// The bind group layout for this pipeline.
bind_group_layout: BindGroupLayoutDescriptor,
/// A handle that identifies the compiled shader.
pipeline_id: Option<CachedComputePipelineId>,
/// The shader asset handle.
shader: Handle<Shader>,
}
impl DownsampleDepthPipeline {
/// Creates a new [`DownsampleDepthPipeline`] from a bind group layout and the downsample
/// shader.
///
/// This doesn't actually specialize the pipeline; that must be done
/// afterward.
fn new(
bind_group_layout: BindGroupLayoutDescriptor,
shader: Handle<Shader>,
) -> DownsampleDepthPipeline {
DownsampleDepthPipeline {
bind_group_layout,
pipeline_id: None,
shader,
}
}
}
/// Stores all depth buffer downsampling pipelines.
#[derive(Resource)]
pub struct DownsampleDepthPipelines {
/// The first pass of the pipeline, when the depth buffer is *not*
/// multisampled.
first: DownsampleDepthPipeline,
/// The second pass of the pipeline, when the depth buffer is *not*
/// multisampled.
second: DownsampleDepthPipeline,
/// The first pass of the pipeline, when the depth buffer is multisampled.
first_multisample: DownsampleDepthPipeline,
/// The second pass of the pipeline, when the depth buffer is multisampled.
second_multisample: DownsampleDepthPipeline,
/// The sampler that the depth downsampling shader uses to sample the depth
/// buffer.
sampler: Sampler,
}
/// Creates the [`DownsampleDepthPipelines`] if downsampling is supported on the
/// current platform.
fn create_downsample_depth_pipelines(
mut commands: Commands,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
mut specialized_compute_pipelines: ResMut<SpecializedComputePipelines<DownsampleDepthPipeline>>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
downsample_depth_shader: Res<DownsampleDepthShader>,
mut has_run: Local<bool>,
) {
// Only run once.
// We can't use a `resource_exists` or similar run condition here because
// this function might fail to create downsample depth pipelines if the
// current platform doesn't support compute shaders.
if *has_run {
return;
}
*has_run = true;
if !gpu_preprocessing_support.is_culling_supported() {
debug!("Downsample depth is not supported on this platform.");
return;
}
// Create the bind group layouts. The bind group layouts are identical
// between the first and second passes, so the only thing we need to
// treat specially is the type of the first mip level (non-multisampled
// or multisampled).
let standard_bind_group_layout = create_downsample_depth_bind_group_layout(false);
let multisampled_bind_group_layout = create_downsample_depth_bind_group_layout(true);
// Create the depth pyramid sampler. This is shared among all shaders.
let sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("depth pyramid sampler"),
..SamplerDescriptor::default()
});
// Initialize the pipelines.
let mut downsample_depth_pipelines = DownsampleDepthPipelines {
first: DownsampleDepthPipeline::new(
standard_bind_group_layout.clone(),
downsample_depth_shader.0.clone(),
),
second: DownsampleDepthPipeline::new(
standard_bind_group_layout.clone(),
downsample_depth_shader.0.clone(),
),
first_multisample: DownsampleDepthPipeline::new(
multisampled_bind_group_layout.clone(),
downsample_depth_shader.0.clone(),
),
second_multisample: DownsampleDepthPipeline::new(
multisampled_bind_group_layout.clone(),
downsample_depth_shader.0.clone(),
),
sampler,
};
// Specialize each pipeline with the appropriate
// `DownsampleDepthPipelineKey`.
downsample_depth_pipelines.first.pipeline_id = Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.first,
DownsampleDepthPipelineKey::empty(),
));
downsample_depth_pipelines.second.pipeline_id = Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.second,
DownsampleDepthPipelineKey::SECOND_PHASE,
));
downsample_depth_pipelines.first_multisample.pipeline_id =
Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.first_multisample,
DownsampleDepthPipelineKey::MULTISAMPLE,
));
downsample_depth_pipelines.second_multisample.pipeline_id =
Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.second_multisample,
DownsampleDepthPipelineKey::SECOND_PHASE | DownsampleDepthPipelineKey::MULTISAMPLE,
));
commands.insert_resource(downsample_depth_pipelines);
}
/// Creates a single bind group layout for the downsample depth pass.
fn create_downsample_depth_bind_group_layout(is_multisampled: bool) -> BindGroupLayoutDescriptor {
BindGroupLayoutDescriptor::new(
if is_multisampled {
"downsample multisample depth bind group layout"
} else {
"downsample depth bind group layout"
},
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
// We only care about the multisample status of the depth buffer
// for the first mip level. After the first mip level is
// sampled, we drop to a single sample.
if is_multisampled {
texture_2d_multisampled(TextureSampleType::Depth)
} else {
texture_2d(TextureSampleType::Depth)
},
// All the mip levels follow:
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadWrite),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
sampler(SamplerBindingType::NonFiltering),
),
),
)
}
bitflags! {
/// Uniquely identifies a configuration of the downsample depth shader.
///
/// Note that meshlets maintain their downsample depth shaders on their own
/// and don't use this infrastructure; thus there's no flag for meshlets in
/// here, even though the shader has defines for it.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct DownsampleDepthPipelineKey: u8 {
/// True if the depth buffer is multisampled.
const MULTISAMPLE = 1;
/// True if this shader is the second phase of the downsample depth
/// process; false if this shader is the first phase.
const SECOND_PHASE = 2;
}
}
impl SpecializedComputePipeline for DownsampleDepthPipeline {
type Key = DownsampleDepthPipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
let mut shader_defs = vec![];
if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) {
shader_defs.push("MULTISAMPLE".into());
}
let label = format!(
"downsample depth{}{} pipeline",
if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) {
" multisample"
} else {
""
},
if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
" second phase"
} else {
" first phase"
}
)
.into();
ComputePipelineDescriptor {
label: Some(label),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: self.shader.clone(),
shader_defs,
entry_point: Some(if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
"downsample_depth_second".into()
} else {
"downsample_depth_first".into()
}),
..default()
}
}
}
/// Stores a placeholder texture that can be bound to a depth pyramid binding if
/// no depth pyramid is needed.
#[derive(Resource, Deref, DerefMut)]
pub struct DepthPyramidDummyTexture(TextureView);
pub fn init_depth_pyramid_dummy_texture(mut commands: Commands, render_device: Res<RenderDevice>) {
commands.insert_resource(DepthPyramidDummyTexture(
create_depth_pyramid_dummy_texture(
&render_device,
"depth pyramid dummy texture",
"depth pyramid dummy texture view",
),
));
}
/// Creates a placeholder texture that can be bound to a depth pyramid binding
/// if no depth pyramid is needed.
pub fn create_depth_pyramid_dummy_texture(
render_device: &RenderDevice,
texture_label: &'static str,
texture_view_label: &'static str,
) -> TextureView {
render_device
.create_texture(&TextureDescriptor {
label: Some(texture_label),
size: Extent3d::default(),
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::R32Float,
usage: TextureUsages::STORAGE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor {
label: Some(texture_view_label),
format: Some(TextureFormat::R32Float),
dimension: Some(TextureViewDimension::D2),
usage: None,
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: Some(1),
base_array_layer: 0,
array_layer_count: Some(1),
})
}
/// Stores a hierarchical Z-buffer for a view, which is a series of mipmaps
/// useful for efficient occlusion culling.
///
/// This will only be present on a view when occlusion culling is enabled.
#[derive(Component)]
pub struct ViewDepthPyramid {
/// A texture view containing the entire depth texture.
pub all_mips: TextureView,
/// A series of texture views containing one mip level each.
pub mips: [TextureView; DEPTH_PYRAMID_MIP_COUNT],
/// The total number of mipmap levels.
///
/// This is the base-2 logarithm of the greatest dimension of the depth
/// buffer, rounded up.
pub mip_count: u32,
}
impl ViewDepthPyramid {
/// Allocates a new depth pyramid for a depth buffer with the given size.
pub fn new(
render_device: &RenderDevice,
texture_cache: &mut TextureCache,
depth_pyramid_dummy_texture: &TextureView,
size: UVec2,
texture_label: &'static str,
texture_view_label: &'static str,
) -> ViewDepthPyramid {
// Calculate the size of the depth pyramid. This is the size of the
// depth buffer rounded down to the previous power of two.
let depth_pyramid_size = Extent3d {
width: previous_power_of_two(size.x),
height: previous_power_of_two(size.y),
depth_or_array_layers: 1,
};
// Calculate the number of mip levels we need.
let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2);
// Create the depth pyramid.
let depth_pyramid = texture_cache.get(
render_device,
TextureDescriptor {
label: Some(texture_label),
size: depth_pyramid_size,
mip_level_count: depth_pyramid_mip_count,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::R32Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
// Create individual views for each level of the depth pyramid.
let depth_pyramid_mips = array::from_fn(|i| {
if (i as u32) < depth_pyramid_mip_count {
depth_pyramid.texture.create_view(&TextureViewDescriptor {
label: Some(texture_view_label),
format: Some(TextureFormat::R32Float),
dimension: Some(TextureViewDimension::D2),
usage: None,
aspect: TextureAspect::All,
base_mip_level: i as u32,
mip_level_count: Some(1),
base_array_layer: 0,
array_layer_count: Some(1),
})
} else {
(*depth_pyramid_dummy_texture).clone()
}
});
// Create the view for the depth pyramid as a whole.
let depth_pyramid_all_mips = depth_pyramid.default_view.clone();
Self {
all_mips: depth_pyramid_all_mips,
mips: depth_pyramid_mips,
mip_count: depth_pyramid_mip_count,
}
}
/// Creates a bind group that allows the depth buffer to be attached to the
/// `downsample_depth.wgsl` shader.
pub fn create_bind_group<'a, R>(
&'a self,
render_device: &RenderDevice,
label: &'static str,
bind_group_layout: &BindGroupLayout,
source_image: R,
sampler: &'a Sampler,
) -> BindGroup
where
R: IntoBinding<'a>,
{
render_device.create_bind_group(
label,
bind_group_layout,
&BindGroupEntries::sequential((
source_image,
&self.mips[0],
&self.mips[1],
&self.mips[2],
&self.mips[3],
&self.mips[4],
&self.mips[5],
&self.mips[6],
&self.mips[7],
&self.mips[8],
&self.mips[9],
&self.mips[10],
&self.mips[11],
sampler,
)),
)
}
/// Invokes the shaders to generate the hierarchical Z-buffer.
///
/// This is intended to be invoked as part of a render node.
pub fn downsample_depth(
&self,
label: &str,
render_context: &mut RenderContext,
view_size: UVec2,
downsample_depth_bind_group: &BindGroup,
downsample_depth_first_pipeline: &ComputePipeline,
downsample_depth_second_pipeline: &ComputePipeline,
) {
// We need to make sure that every mip level the single-pass
// downsampling (SPD) shader sees has lengths that are powers of two for
// correct conservative depth buffer downsampling. To do this, we
// maintain the fiction that we're downsampling a depth buffer scaled up
// so that it has side lengths rounded up to the next power of two. (If
// the depth buffer already has a side length that's a power of two,
// then we double it anyway; this ensures that we don't lose any
// precision in the top level of the depth pyramid.) The
// `downsample_depth` shader's `load_mip_0` function returns the value
// that sampling such a depth buffer would yield, without actually
// having to construct such a scaled depth buffer.
let virtual_view_size = uvec2(
(view_size.x + 1).next_power_of_two(),
(view_size.y + 1).next_power_of_two(),
);
let command_encoder = render_context.command_encoder();
let mut downsample_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some(label),
timestamp_writes: None,
});
downsample_pass.set_pipeline(downsample_depth_first_pipeline);
// Pass the mip count as a push constant, for simplicity.
downsample_pass.set_push_constants(0, &self.mip_count.to_le_bytes());
downsample_pass.set_bind_group(0, downsample_depth_bind_group, &[]);
downsample_pass.dispatch_workgroups(
virtual_view_size.x.div_ceil(64),
virtual_view_size.y.div_ceil(64),
1,
);
if self.mip_count >= 7 {
downsample_pass.set_pipeline(downsample_depth_second_pipeline);
downsample_pass.dispatch_workgroups(1, 1, 1);
}
}
}
/// Creates depth pyramids for views that have occlusion culling enabled.
pub fn prepare_view_depth_pyramids(
mut commands: Commands,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
depth_pyramid_dummy_texture: Res<DepthPyramidDummyTexture>,
views: Query<(Entity, &ExtractedView), (With<OcclusionCulling>, Without<NoIndirectDrawing>)>,
) {
for (view_entity, view) in &views {
commands.entity(view_entity).insert(ViewDepthPyramid::new(
&render_device,
&mut texture_cache,
&depth_pyramid_dummy_texture,
view.viewport.zw(),
"view depth pyramid texture",
"view depth pyramid texture view",
));
}
}
/// The bind group that we use to attach the depth buffer and depth pyramid for
/// a view to the `downsample_depth.wgsl` shader.
///
/// This will only be present for a view if occlusion culling is enabled.
#[derive(Component, Deref, DerefMut)]
pub struct ViewDownsampleDepthBindGroup(BindGroup);
/// Creates the [`ViewDownsampleDepthBindGroup`]s for all views with occlusion
/// culling enabled.
fn prepare_downsample_depth_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
downsample_depth_pipelines: Res<DownsampleDepthPipelines>,
pipeline_cache: Res<PipelineCache>,
view_depth_textures: Query<
(
Entity,
&ViewDepthPyramid,
Option<&ViewDepthTexture>,
Option<&OcclusionCullingSubview>,
),
Or<(With<ViewDepthTexture>, With<OcclusionCullingSubview>)>,
>,
) {
for (view_entity, view_depth_pyramid, view_depth_texture, shadow_occlusion_culling) in
&view_depth_textures
{
let is_multisampled = view_depth_texture
.is_some_and(|view_depth_texture| view_depth_texture.texture.sample_count() > 1);
commands
.entity(view_entity)
.insert(ViewDownsampleDepthBindGroup(
view_depth_pyramid.create_bind_group(
&render_device,
if is_multisampled {
"downsample multisample depth bind group"
} else {
"downsample depth bind group"
},
&pipeline_cache.get_bind_group_layout(if is_multisampled {
&downsample_depth_pipelines
.first_multisample
.bind_group_layout
} else {
&downsample_depth_pipelines.first.bind_group_layout
}),
match (view_depth_texture, shadow_occlusion_culling) {
(Some(view_depth_texture), _) => view_depth_texture.view(),
(None, Some(shadow_occlusion_culling)) => {
&shadow_occlusion_culling.depth_texture_view
}
(None, None) => panic!("Should never happen"),
},
&downsample_depth_pipelines.sampler,
),
));
}
}
/// Returns the previous power of two of x, or, if x is exactly a power of two,
/// returns x unchanged.
fn previous_power_of_two(x: u32) -> u32 {
1 << (31 - x.leading_zeros())
}
+7
View File
@@ -0,0 +1,7 @@
//! Experimental rendering features.
//!
//! Experimental features are features with known problems, missing features,
//! compatibility issues, low performance, and/or future breaking changes, but
//! are included nonetheless for testing purposes.
pub mod mip_generation;
+329
View File
@@ -0,0 +1,329 @@
//! This is mostly a pluginified version of the `custom_post_processing` example
//!
//! The plugin will create a new node that runs a fullscreen triangle.
//!
//! Users need to use the [`FullscreenMaterial`] trait to define the parameters like the graph label or the graph ordering.
use core::any::type_name;
use core::marker::PhantomData;
use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader};
use bevy_app::{App, Plugin};
use bevy_asset::AssetServer;
use bevy_camera::{Camera2d, Camera3d};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Added, Has, QueryItem},
resource::Resource,
system::{Commands, Res},
world::{FromWorld, World},
};
use bevy_image::BevyDefault;
use bevy_render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_graph::{
InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext,
RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
encase::internal::WriteInto,
BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations,
PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor,
Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat,
TextureSampleType,
},
renderer::{RenderContext, RenderDevice},
view::ViewTarget,
ExtractSchedule, MainWorld, RenderApp, RenderStartup,
};
use bevy_shader::ShaderRef;
use bevy_utils::default;
use tracing::warn;
#[derive(Default)]
pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}
impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<T>::default(),
UniformComponentPlugin::<T>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(RenderStartup, init_pipeline::<T>);
if let Some(sub_graph) = T::sub_graph() {
render_app.add_render_graph_node::<ViewNodeRunner<FullscreenMaterialNode<T>>>(
sub_graph,
T::node_label(),
);
// We can't use add_render_graph_edges because it doesn't accept a Vec<RenderLabel>
if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::<RenderGraph>()
&& let Some(graph) = render_graph.get_sub_graph_mut(sub_graph)
{
for window in T::node_edges().windows(2) {
let [a, b] = window else {
break;
};
let Err(err) = graph.try_add_node_edge(*a, *b) else {
continue;
};
match err {
// Already existing edges are very easy to produce with this api
// and shouldn't cause a panic
RenderGraphError::EdgeAlreadyExists(_) => {}
_ => panic!("{err:?}"),
}
}
} else {
warn!("Failed to add edges for FullscreenMaterial");
};
} else {
// If there was no sub_graph specified we try to determine the graph based on the camera
// it gets added to.
render_app.add_systems(ExtractSchedule, extract_on_add::<T>);
}
}
}
fn extract_on_add<T: FullscreenMaterial>(world: &mut World) {
world.resource_scope::<MainWorld, ()>(|world, mut main_world| {
// Extract the material from the main world
let mut query =
main_world.query_filtered::<(Entity, Has<Camera3d>, Has<Camera2d>), Added<T>>();
// Create the node and add it to the render graph
world.resource_scope::<RenderGraph, ()>(|world, mut render_graph| {
for (_entity, is_3d, is_2d) in query.iter(&main_world) {
let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) {
graph
} else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) {
graph
} else {
warn!("FullscreenMaterial was added to an entity that isn't a camera");
continue;
};
let node = ViewNodeRunner::<FullscreenMaterialNode<T>>::from_world(world);
graph.add_node(T::node_label(), node);
for window in T::node_edges().windows(2) {
let [a, b] = window else {
break;
};
let Err(err) = graph.try_add_node_edge(*a, *b) else {
continue;
};
match err {
// Already existing edges are very easy to produce with this api
// and shouldn't cause a panic
RenderGraphError::EdgeAlreadyExists(_) => {}
_ => panic!("{err:?}"),
}
}
}
});
});
}
/// A trait to define a material that will render to the entire screen using a fullscrene triangle
pub trait FullscreenMaterial:
Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default
{
/// The shader that will run on the entire screen using a fullscreen triangle
fn fragment_shader() -> ShaderRef;
/// The list of `node_edges`. In 3d, for a post processing effect, it would look like this:
///
/// ```compile_fail
/// # use bevy_core_pipeline::core_3d::graph::Node3d;
/// # use bevy_render::render_graph::RenderLabel;
/// vec![
/// Node3d::Tonemapping.intern(),
/// // Self::sub_graph().intern(), // <--- your own label here
/// Node3d::EndMainPassPostProcessing.intern(),
/// ]
/// ```
///
/// This tell the render graph to run your fullscreen effect after the tonemapping pass but
/// before the end of post processing. For 2d, it would be the same but using Node2d. You can
/// specify any edges you want but make sure to include your own label.
fn node_edges() -> Vec<InternedRenderLabel>;
/// The [`bevy_render::render_graph::RenderSubGraph`] the effect will run in
///
/// For 2d this is generally [`crate::core_2d::graph::Core2d`] and for 3d it's
/// [`crate::core_3d::graph::Core3d`]
fn sub_graph() -> Option<InternedRenderSubGraph> {
None
}
/// The label used to represent the render node that will run the pass
fn node_label() -> impl RenderLabel {
FullscreenMaterialLabel(type_name::<Self>())
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
struct FullscreenMaterialLabel(&'static str);
impl RenderLabel for FullscreenMaterialLabel
where
Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash,
{
fn dyn_clone(&self) -> Box<dyn RenderLabel> {
Box::new(::core::clone::Clone::clone(self))
}
}
#[derive(Resource)]
struct FullscreenMaterialPipeline {
layout: BindGroupLayoutDescriptor,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
pipeline_id_hdr: CachedRenderPipelineId,
}
fn init_pipeline<T: FullscreenMaterial>(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen_shader: Res<FullscreenShader>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = BindGroupLayoutDescriptor::new(
"post_process_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// The screen texture
texture_2d(TextureSampleType::Float { filterable: true }),
// The sampler that will be used to sample the screen texture
sampler(SamplerBindingType::Filtering),
// We use a uniform buffer so users can pass some data to the effect
// Eventually we should just use a separate bind group for user data
uniform_buffer::<T>(true),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
let shader = match T::fragment_shader() {
ShaderRef::Default => {
// TODO not sure what an actual fallback should be. An empty shader or output a solid
// color to indicate a missing shader?
unimplemented!(
"FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
)
}
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
};
// Setup a fullscreen triangle for the vertex state.
let vertex_state = fullscreen_shader.to_vertex_state();
let mut desc = RenderPipelineDescriptor {
label: Some("post_process_pipeline".into()),
layout: vec![layout.clone()],
vertex: vertex_state,
fragment: Some(FragmentState {
shader,
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
};
let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone());
desc.fragment.as_mut().unwrap().targets[0]
.as_mut()
.unwrap()
.format = ViewTarget::TEXTURE_FORMAT_HDR;
let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc);
commands.insert_resource(FullscreenMaterialPipeline {
layout,
sampler,
pipeline_id,
pipeline_id_hdr,
});
}
#[derive(Default)]
struct FullscreenMaterialNode<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}
impl<T: FullscreenMaterial> ViewNode for FullscreenMaterialNode<T> {
// TODO we should expose the depth buffer and the gbuffer if using deferred
type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex<T>);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let fullscreen_pipeline = world.resource::<FullscreenMaterialPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline_id = if view_target.is_hdr() {
fullscreen_pipeline.pipeline_id_hdr
} else {
fullscreen_pipeline.pipeline_id
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
return Ok(());
};
let data_uniforms = world.resource::<ComponentUniforms<T>>();
let Some(settings_binding) = data_uniforms.uniforms().binding() else {
return Ok(());
};
let post_process = view_target.post_process_write();
let bind_group = render_context.render_device().create_bind_group(
"post_process_bind_group",
&pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
&BindGroupEntries::sequential((
post_process.source,
&fullscreen_pipeline.sampler,
settings_binding.clone(),
)),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("post_process_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: post_process.destination,
depth_slice: None,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
@@ -0,0 +1,34 @@
#define_import_path bevy_core_pipeline::fullscreen_vertex_shader
struct FullscreenVertexOutput {
@builtin(position)
position: vec4<f32>,
@location(0)
uv: vec2<f32>,
};
// This vertex shader produces the following, when drawn using indices 0..3:
//
// 1 | 0-----x.....2
// 0 | | s | . ´
// -1 | x_____x´
// -2 | : .´
// -3 | 1´
// +---------------
// -1 0 1 2 3
//
// The axes are clip-space x and y. The region marked s is the visible region.
// The digits in the corners of the right-angled triangle are the vertex
// indices.
//
// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0.
// This means that the UV gets interpolated to 1,1 at the bottom-right corner
// of the clip-space rectangle that is at 1,-1 in clip space.
@vertex
fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
// See the explanation above for how this works
let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
return FullscreenVertexOutput(clip_position, uv);
}
@@ -0,0 +1,41 @@
use bevy_asset::{load_embedded_asset, Handle};
use bevy_ecs::{resource::Resource, world::FromWorld};
use bevy_render::render_resource::VertexState;
use bevy_shader::Shader;
/// A shader that renders to the whole screen. Useful for post-processing.
#[derive(Resource, Clone)]
pub struct FullscreenShader(Handle<Shader>);
impl FromWorld for FullscreenShader {
fn from_world(world: &mut bevy_ecs::world::World) -> Self {
Self(load_embedded_asset!(world, "fullscreen.wgsl"))
}
}
impl FullscreenShader {
/// Gets the raw shader handle.
pub fn shader(&self) -> Handle<Shader> {
self.0.clone()
}
/// Creates a [`VertexState`] that uses the [`FullscreenShader`] to output a
/// ```wgsl
/// struct FullscreenVertexOutput {
/// @builtin(position)
/// position: vec4<f32>;
/// @location(0)
/// uv: vec2<f32>;
/// };
/// ```
/// from the vertex shader.
/// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);`
pub fn to_vertex_state(&self) -> VertexState {
VertexState {
shader: self.0.clone(),
shader_defs: Vec::new(),
entry_point: Some("fullscreen_vertex_shader".into()),
buffers: Vec::new(),
}
}
}
+57
View File
@@ -0,0 +1,57 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevy.org/assets/icon.png"
)]
pub mod blit;
pub mod core_2d;
pub mod core_3d;
pub mod deferred;
pub mod experimental;
pub mod fullscreen_material;
pub mod oit;
pub mod prepass;
pub mod tonemapping;
pub mod upscaling;
pub use fullscreen_vertex_shader::FullscreenShader;
pub use skybox::Skybox;
mod fullscreen_vertex_shader;
mod skybox;
use crate::{
blit::BlitPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin,
deferred::copy_lighting_id::CopyDeferredLightingIdPlugin,
experimental::mip_generation::MipGenerationPlugin, tonemapping::TonemappingPlugin,
upscaling::UpscalingPlugin,
};
use bevy_app::{App, Plugin};
use bevy_asset::embedded_asset;
use bevy_render::RenderApp;
use oit::OrderIndependentTransparencyPlugin;
#[derive(Default)]
pub struct CorePipelinePlugin;
impl Plugin for CorePipelinePlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "fullscreen_vertex_shader/fullscreen.wgsl");
app.add_plugins((Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin))
.add_plugins((
BlitPlugin,
TonemappingPlugin,
UpscalingPlugin,
OrderIndependentTransparencyPlugin,
MipGenerationPlugin,
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<FullscreenShader>();
}
}
+297
View File
@@ -0,0 +1,297 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*;
use bevy_camera::{Camera3d, RenderTarget};
use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*};
use bevy_math::UVec2;
use bevy_platform::collections::HashSet;
use bevy_platform::time::Instant;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::ExtractedCamera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{RenderGraphExt, ViewNodeRunner},
render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, ShaderType, TextureUsages},
renderer::{RenderDevice, RenderQueue},
view::Msaa,
Render, RenderApp, RenderStartup, RenderSystems,
};
use bevy_shader::load_shader_library;
use bevy_window::PrimaryWindow;
use resolve::{
node::{OitResolveNode, OitResolvePass},
OitResolvePlugin,
};
use tracing::{trace, warn};
use crate::core_3d::graph::{Core3d, Node3d};
/// Module that defines the necessary systems to resolve the OIT buffer and render it to the screen.
pub mod resolve;
/// Used to identify which camera will use OIT to render transparent meshes
/// and to configure OIT.
// TODO consider supporting multiple OIT techniques like WBOIT, Moment Based OIT,
// depth peeling, stochastic transparency, ray tracing etc.
// This should probably be done by adding an enum to this component.
// We use the same struct to pass on the settings to the drawing shader.
#[derive(Clone, Copy, ExtractComponent, Reflect, ShaderType)]
#[reflect(Clone, Default)]
pub struct OrderIndependentTransparencySettings {
/// Controls how many layers will be used to compute the blending.
/// The more layers you use the more memory it will use but it will also give better results.
/// 8 is generally recommended, going above 32 is probably not worth it in the vast majority of cases
pub layer_count: i32,
/// Threshold for which fragments will be added to the blending layers.
/// This can be tweaked to optimize quality / layers count. Higher values will
/// allow lower number of layers and a better performance, compromising quality.
pub alpha_threshold: f32,
}
impl Default for OrderIndependentTransparencySettings {
fn default() -> Self {
Self {
layer_count: 8,
alpha_threshold: 0.0,
}
}
}
// OrderIndependentTransparencySettings is also a Component. We explicitly implement the trait so
// we can hook on_add to issue a warning in case `layer_count` is seemingly too high.
impl Component for OrderIndependentTransparencySettings {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;
fn on_add() -> Option<ComponentHook> {
Some(|world, context| {
if let Some(value) = world.get::<OrderIndependentTransparencySettings>(context.entity)
&& value.layer_count > 32
{
warn!("{}OrderIndependentTransparencySettings layer_count set to {} might be too high.",
context.caller.map(|location|format!("{location}: ")).unwrap_or_default(),
value.layer_count
);
}
})
}
}
/// A plugin that adds support for Order Independent Transparency (OIT).
/// This can correctly render some scenes that would otherwise have artifacts due to alpha blending, but uses more memory.
///
/// To enable OIT for a camera you need to add the [`OrderIndependentTransparencySettings`] component to it.
///
/// If you want to use OIT for your custom material you need to call `oit_draw(position, color)` in your fragment shader.
/// You also need to make sure that your fragment shader doesn't output any colors.
///
/// # Implementation details
/// This implementation uses 2 passes.
///
/// The first pass writes the depth and color of all the fragments to a big buffer.
/// The buffer contains N layers for each pixel, where N can be set with [`OrderIndependentTransparencySettings::layer_count`].
/// This pass is essentially a forward pass.
///
/// The second pass is a single fullscreen triangle pass that sorts all the fragments then blends them together
/// and outputs the result to the screen.
pub struct OrderIndependentTransparencyPlugin;
impl Plugin for OrderIndependentTransparencyPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "oit_draw.wgsl");
app.add_plugins((
ExtractComponentPlugin::<OrderIndependentTransparencySettings>::default(),
OitResolvePlugin,
))
.add_systems(Update, check_msaa)
.add_systems(Last, configure_depth_texture_usages);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.add_systems(RenderStartup, init_oit_buffers)
.add_systems(
Render,
prepare_oit_buffers.in_set(RenderSystems::PrepareResources),
);
render_app
.add_render_graph_node::<ViewNodeRunner<OitResolveNode>>(Core3d, OitResolvePass)
.add_render_graph_edges(
Core3d,
(
Node3d::MainTransparentPass,
OitResolvePass,
Node3d::EndMainPass,
),
);
}
}
// WARN This should only happen for cameras with the [`OrderIndependentTransparencySettings`] component
// but when multiple cameras are present on the same window
// bevy reuses the same depth texture so we need to set this on all cameras with the same render target.
fn configure_depth_texture_usages(
p: Query<Entity, With<PrimaryWindow>>,
cameras: Query<(&RenderTarget, Has<OrderIndependentTransparencySettings>)>,
mut new_cameras: Query<(&mut Camera3d, &RenderTarget), Added<Camera3d>>,
) {
if new_cameras.is_empty() {
return;
}
// Find all the render target that potentially uses OIT
let primary_window = p.single().ok();
let mut render_target_has_oit = <HashSet<_>>::default();
for (render_target, has_oit) in &cameras {
if has_oit {
render_target_has_oit.insert(render_target.normalize(primary_window));
}
}
// Update the depth texture usage for cameras with a render target that has OIT
for (mut camera_3d, render_target) in &mut new_cameras {
if render_target_has_oit.contains(&render_target.normalize(primary_window)) {
let mut usages = TextureUsages::from(camera_3d.depth_texture_usages);
usages |= TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
camera_3d.depth_texture_usages = usages.into();
}
}
}
fn check_msaa(cameras: Query<&Msaa, With<OrderIndependentTransparencySettings>>) {
for msaa in &cameras {
if msaa.samples() > 1 {
panic!("MSAA is not supported when using OrderIndependentTransparency");
}
}
}
/// Holds the buffers that contain the data of all OIT layers.
/// We use one big buffer for the entire app. Each camera will reuse it so it will
/// always be the size of the biggest OIT enabled camera.
#[derive(Resource)]
pub struct OitBuffers {
/// The OIT layers containing depth and color for each fragments.
/// This is essentially used as a 3d array where xy is the screen coordinate and z is
/// the list of fragments rendered with OIT.
pub layers: BufferVec<UVec2>,
/// Buffer containing the index of the last layer that was written for each fragment.
pub layer_ids: BufferVec<i32>,
pub settings: DynamicUniformBuffer<OrderIndependentTransparencySettings>,
}
pub fn init_oit_buffers(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
) {
// initialize buffers with something so there's a valid binding
let mut layers = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layers.set_label(Some("oit_layers"));
layers.reserve(1, &render_device);
layers.write_buffer(&render_device, &render_queue);
let mut layer_ids = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layer_ids.set_label(Some("oit_layer_ids"));
layer_ids.reserve(1, &render_device);
layer_ids.write_buffer(&render_device, &render_queue);
let mut settings = DynamicUniformBuffer::default();
settings.set_label(Some("oit_settings"));
commands.insert_resource(OitBuffers {
layers,
layer_ids,
settings,
});
}
#[derive(Component)]
pub struct OrderIndependentTransparencySettingsOffset {
pub offset: u32,
}
/// This creates or resizes the oit buffers for each camera.
/// It will always create one big buffer that's as big as the biggest buffer needed.
/// Cameras with smaller viewports or less layers will simply use the big buffer and ignore the rest.
pub fn prepare_oit_buffers(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
cameras: Query<
(&ExtractedCamera, &OrderIndependentTransparencySettings),
(
Changed<ExtractedCamera>,
Changed<OrderIndependentTransparencySettings>,
),
>,
camera_oit_uniforms: Query<(Entity, &OrderIndependentTransparencySettings)>,
mut buffers: ResMut<OitBuffers>,
) {
// Get the max buffer size for any OIT enabled camera
let mut max_layer_ids_size = usize::MIN;
let mut max_layers_size = usize::MIN;
for (camera, settings) in &cameras {
let Some(size) = camera.physical_target_size else {
continue;
};
let layer_count = settings.layer_count as usize;
let size = (size.x * size.y) as usize;
max_layer_ids_size = max_layer_ids_size.max(size);
max_layers_size = max_layers_size.max(size * layer_count);
}
// Create or update the layers buffer based on the max size
if buffers.layers.capacity() < max_layers_size {
let start = Instant::now();
buffers.layers.reserve(max_layers_size, &render_device);
let remaining = max_layers_size - buffers.layers.capacity();
for _ in 0..remaining {
buffers.layers.push(UVec2::ZERO);
}
buffers.layers.write_buffer(&render_device, &render_queue);
trace!(
"OIT layers buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layers.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
// Create or update the layer_ids buffer based on the max size
if buffers.layer_ids.capacity() < max_layer_ids_size {
let start = Instant::now();
buffers
.layer_ids
.reserve(max_layer_ids_size, &render_device);
let remaining = max_layer_ids_size - buffers.layer_ids.capacity();
for _ in 0..remaining {
buffers.layer_ids.push(0);
}
buffers
.layer_ids
.write_buffer(&render_device, &render_queue);
trace!(
"OIT layer ids buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layer_ids.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
if let Some(mut writer) = buffers.settings.get_writer(
camera_oit_uniforms.iter().len(),
&render_device,
&render_queue,
) {
for (entity, settings) in &camera_oit_uniforms {
let offset = writer.write(settings);
commands
.entity(entity)
.insert(OrderIndependentTransparencySettingsOffset { offset });
}
}
}
+48
View File
@@ -0,0 +1,48 @@
#define_import_path bevy_core_pipeline::oit
#import bevy_pbr::mesh_view_bindings::{view, oit_layers, oit_layer_ids, oit_settings}
#ifdef OIT_ENABLED
// Add the fragment to the oit buffer
fn oit_draw(position: vec4f, color: vec4f) {
// Don't add fully transparent fragments to the list
// because we don't want to have to sort them in the resolve pass
if color.a < oit_settings.alpha_threshold {
return;
}
// get the index of the current fragment relative to the screen size
let screen_index = i32(floor(position.x) + floor(position.y) * view.viewport.z);
// get the size of the buffer.
// It's always the size of the screen
let buffer_size = i32(view.viewport.z * view.viewport.w);
// gets the layer index of the current fragment
var layer_id = atomicAdd(&oit_layer_ids[screen_index], 1);
// exit early if we've reached the maximum amount of fragments per layer
if layer_id >= oit_settings.layers_count {
// force to store the oit_layers_count to make sure we don't
// accidentally increase the index above the maximum value
atomicStore(&oit_layer_ids[screen_index], oit_settings.layers_count);
// TODO for tail blending we should return the color here
return;
}
// get the layer_index from the screen
let layer_index = screen_index + layer_id * buffer_size;
let rgb9e5_color = bevy_pbr::rgb9e5::vec3_to_rgb9e5_(color.rgb);
let depth_alpha = pack_24bit_depth_8bit_alpha(position.z, color.a);
oit_layers[layer_index] = vec2(rgb9e5_color, depth_alpha);
}
#endif // OIT_ENABLED
fn pack_24bit_depth_8bit_alpha(depth: f32, alpha: f32) -> u32 {
let depth_bits = u32(saturate(depth) * f32(0xFFFFFFu) + 0.5);
let alpha_bits = u32(saturate(alpha) * f32(0xFFu) + 0.5);
return (depth_bits & 0xFFFFFFu) | ((alpha_bits & 0xFFu) << 24u);
}
fn unpack_24bit_depth_8bit_alpha(packed: u32) -> vec2<f32> {
let depth_bits = packed & 0xFFFFFFu;
let alpha_bits = (packed >> 24u) & 0xFFu;
return vec2(f32(depth_bits) / f32(0xFFFFFFu), f32(alpha_bits) / f32(0xFFu));
}
+255
View File
@@ -0,0 +1,255 @@
use super::OitBuffers;
use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader};
use bevy_app::Plugin;
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer};
use bevy_derive::Deref;
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_image::BevyDefault as _;
use bevy_render::{
render_resource::{
binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
BlendComponent, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites,
DownlevelFlags, FragmentState, PipelineCache, RenderPipelineDescriptor, ShaderStages,
TextureFormat,
},
renderer::{RenderAdapter, RenderDevice},
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSystems,
};
use bevy_shader::ShaderDefVal;
use bevy_utils::default;
use tracing::warn;
/// Contains the render node used to run the resolve pass.
pub mod node;
/// Minimum required value of `wgpu::Limits::max_storage_buffers_per_shader_stage`.
pub const OIT_REQUIRED_STORAGE_BUFFERS: u32 = 2;
/// Plugin needed to resolve the Order Independent Transparency (OIT) buffer to the screen.
pub struct OitResolvePlugin;
impl Plugin for OitResolvePlugin {
fn build(&self, app: &mut bevy_app::App) {
embedded_asset!(app, "oit_resolve.wgsl");
}
fn finish(&self, app: &mut bevy_app::App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
if !is_oit_supported(
render_app.world().resource::<RenderAdapter>(),
render_app.world().resource::<RenderDevice>(),
true,
) {
return;
}
render_app
.add_systems(
Render,
(
queue_oit_resolve_pipeline.in_set(RenderSystems::Queue),
prepare_oit_resolve_bind_group.in_set(RenderSystems::PrepareBindGroups),
),
)
.insert_resource(OitResolvePipeline::new());
}
}
pub fn is_oit_supported(adapter: &RenderAdapter, device: &RenderDevice, warn: bool) -> bool {
if !adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
{
if warn {
warn!("OrderIndependentTransparencyPlugin not loaded. GPU lacks support: DownlevelFlags::FRAGMENT_WRITABLE_STORAGE.");
}
return false;
}
let max_storage_buffers_per_shader_stage = device.limits().max_storage_buffers_per_shader_stage;
if max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS {
if warn {
warn!(
max_storage_buffers_per_shader_stage,
OIT_REQUIRED_STORAGE_BUFFERS,
"OrderIndependentTransparencyPlugin not loaded. RenderDevice lacks support: max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS."
);
}
return false;
}
true
}
/// Bind group for the OIT resolve pass.
#[derive(Resource, Deref)]
pub struct OitResolveBindGroup(pub BindGroup);
/// Bind group layouts used for the OIT resolve pass.
#[derive(Resource)]
pub struct OitResolvePipeline {
/// View bind group layout.
pub view_bind_group_layout: BindGroupLayoutDescriptor,
/// Depth bind group layout.
pub oit_depth_bind_group_layout: BindGroupLayoutDescriptor,
}
impl OitResolvePipeline {
fn new() -> Self {
let view_bind_group_layout = BindGroupLayoutDescriptor::new(
"oit_resolve_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
// layers
storage_buffer_sized(false, None),
// layer ids
storage_buffer_sized(false, None),
),
),
);
let oit_depth_bind_group_layout = BindGroupLayoutDescriptor::new(
"oit_depth_bind_group_layout",
&BindGroupLayoutEntries::single(ShaderStages::FRAGMENT, texture_depth_2d()),
);
OitResolvePipeline {
view_bind_group_layout,
oit_depth_bind_group_layout,
}
}
}
#[derive(Component, Deref, Clone, Copy)]
pub struct OitResolvePipelineId(pub CachedRenderPipelineId);
/// This key is used to cache the pipeline id and to specialize the render pipeline descriptor.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct OitResolvePipelineKey {
hdr: bool,
layer_count: i32,
}
pub fn queue_oit_resolve_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
resolve_pipeline: Res<OitResolvePipeline>,
views: Query<
(
Entity,
&ExtractedView,
&OrderIndependentTransparencySettings,
),
With<OrderIndependentTransparencySettings>,
>,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
// Store the key with the id to make the clean up logic easier.
// This also means it will always replace the entry if the key changes so nothing to clean up.
mut cached_pipeline_id: Local<EntityHashMap<(OitResolvePipelineKey, CachedRenderPipelineId)>>,
) {
let mut current_view_entities = EntityHashSet::default();
for (e, view, oit_settings) in &views {
current_view_entities.insert(e);
let key = OitResolvePipelineKey {
hdr: view.hdr,
layer_count: oit_settings.layer_count,
};
if let Some((cached_key, id)) = cached_pipeline_id.get(&e)
&& *cached_key == key
{
commands.entity(e).insert(OitResolvePipelineId(*id));
continue;
}
let desc = specialize_oit_resolve_pipeline(
key,
&resolve_pipeline,
&fullscreen_shader,
&asset_server,
);
let pipeline_id = pipeline_cache.queue_render_pipeline(desc);
commands.entity(e).insert(OitResolvePipelineId(pipeline_id));
cached_pipeline_id.insert(e, (key, pipeline_id));
}
// Clear cache for views that don't exist anymore.
for e in cached_pipeline_id.keys().copied().collect::<Vec<_>>() {
if !current_view_entities.contains(&e) {
cached_pipeline_id.remove(&e);
}
}
}
fn specialize_oit_resolve_pipeline(
key: OitResolvePipelineKey,
resolve_pipeline: &OitResolvePipeline,
fullscreen_shader: &FullscreenShader,
asset_server: &AssetServer,
) -> RenderPipelineDescriptor {
let format = if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
RenderPipelineDescriptor {
label: Some("oit_resolve_pipeline".into()),
layout: vec![
resolve_pipeline.view_bind_group_layout.clone(),
resolve_pipeline.oit_depth_bind_group_layout.clone(),
],
fragment: Some(FragmentState {
shader: load_embedded_asset!(asset_server, "oit_resolve.wgsl"),
shader_defs: vec![ShaderDefVal::UInt(
"LAYER_COUNT".into(),
key.layer_count as u32,
)],
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState {
color: BlendComponent::OVER,
alpha: BlendComponent::OVER,
}),
write_mask: ColorWrites::ALL,
})],
..default()
}),
vertex: fullscreen_shader.to_vertex_state(),
..default()
}
}
pub fn prepare_oit_resolve_bind_group(
mut commands: Commands,
resolve_pipeline: Res<OitResolvePipeline>,
render_device: Res<RenderDevice>,
view_uniforms: Res<ViewUniforms>,
pipeline_cache: Res<PipelineCache>,
buffers: Res<OitBuffers>,
) {
if let (Some(binding), Some(layers_binding), Some(layer_ids_binding)) = (
view_uniforms.uniforms.binding(),
buffers.layers.binding(),
buffers.layer_ids.binding(),
) {
let bind_group = render_device.create_bind_group(
"oit_resolve_bind_group",
&pipeline_cache.get_bind_group_layout(&resolve_pipeline.view_bind_group_layout),
&BindGroupEntries::sequential((binding.clone(), layers_binding, layer_ids_binding)),
);
commands.insert_resource(OitResolveBindGroup(bind_group));
}
}
+89
View File
@@ -0,0 +1,89 @@
use bevy_camera::{MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
view::{ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
use super::{OitResolveBindGroup, OitResolvePipeline, OitResolvePipelineId};
/// Render label for the OIT resolve pass.
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
pub struct OitResolvePass;
/// The node that executes the OIT resolve pass.
#[derive(Default)]
pub struct OitResolveNode;
impl ViewNode for OitResolveNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static ViewUniformOffset,
&'static OitResolvePipelineId,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view_target, view_uniform, oit_resolve_pipeline_id, depth, resolution_override): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let Some(resolve_pipeline) = world.get_resource::<OitResolvePipeline>() else {
return Ok(());
};
// resolve oit
// sorts the layers and renders the final blended color to the screen
{
let pipeline_cache = world.resource::<PipelineCache>();
let bind_group = world.resource::<OitResolveBindGroup>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(oit_resolve_pipeline_id.0)
else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let depth_bind_group = render_context.render_device().create_bind_group(
"oit_resolve_depth_bind_group",
&pipeline_cache
.get_bind_group_layout(&resolve_pipeline.oit_depth_bind_group_layout),
&BindGroupEntries::single(depth.view()),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("oit_resolve"),
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_pass, "oit_resolve");
if let Some(viewport) =
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
{
render_pass.set_camera_viewport(&viewport);
}
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[view_uniform.offset]);
render_pass.set_bind_group(1, &depth_bind_group, &[]);
render_pass.draw(0..3, 0..1);
pass_span.end(&mut render_pass);
}
Ok(())
}
}
@@ -0,0 +1,117 @@
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<storage, read_write> layers: array<vec2<u32>>;
@group(0) @binding(2) var<storage, read_write> layer_ids: array<atomic<i32>>;
@group(1) @binding(0) var depth: texture_depth_2d;
struct OitFragment {
color: vec3<f32>,
alpha: f32,
depth: f32,
}
// Contains all the colors and depth for this specific fragment
var<private> fragment_list: array<OitFragment, #{LAYER_COUNT}>;
struct FullscreenVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let buffer_size = i32(view.viewport.z * view.viewport.w);
let screen_index = i32(floor(in.position.x) + floor(in.position.y) * view.viewport.z);
let counter = atomicLoad(&layer_ids[screen_index]);
if counter == 0 {
reset_indices(screen_index);
// https://github.com/gfx-rs/wgpu/issues/4416
if true {
discard;
}
return vec4(0.0);
} else {
// Load depth for manual depth testing.
// This is necessary because early z doesn't seem to trigger in the transparent pass.
// This should be done during the draw pass so those fragments simply don't exist in the list,
// but this requires a bigger refactor
let d = textureLoad(depth, vec2<i32>(in.position.xy), 0);
let result = sort(screen_index, buffer_size, d);
reset_indices(screen_index);
return result.color;
}
}
// Resets all indices to 0.
// This means we don't have to clear the entire layers buffer
fn reset_indices(screen_index: i32) {
atomicStore(&layer_ids[screen_index], 0);
layers[screen_index] = vec2(0u);
}
struct SortResult {
color: vec4f,
depth: f32,
}
fn sort(screen_index: i32, buffer_size: i32, opaque_depth: f32) -> SortResult {
var counter = atomicLoad(&layer_ids[screen_index]);
// fill list
for (var i = 0; i < counter; i += 1) {
let fragment = layers[screen_index + buffer_size * i];
// unpack color/alpha/depth
let color = bevy_pbr::rgb9e5::rgb9e5_to_vec3_(fragment.x);
let depth_alpha = bevy_core_pipeline::oit::unpack_24bit_depth_8bit_alpha(fragment.y);
fragment_list[i].color = color;
fragment_list[i].alpha = depth_alpha.y;
fragment_list[i].depth = depth_alpha.x;
}
// bubble sort the list based on the depth
for (var i = counter; i >= 0; i -= 1) {
for (var j = 0; j < i; j += 1) {
if fragment_list[j].depth < fragment_list[j + 1].depth {
// swap
let temp = fragment_list[j + 1];
fragment_list[j + 1] = fragment_list[j];
fragment_list[j] = temp;
}
}
}
// resolve blend
var final_color = vec4(0.0);
for (var i = 0; i <= counter; i += 1) {
// depth testing
// This needs to happen here because we can only stop iterating if the fragment is
// occluded by something opaque and the fragments need to be sorted first
if fragment_list[i].depth < opaque_depth {
break;
}
let color = fragment_list[i].color;
let alpha = fragment_list[i].alpha;
var base_color = vec4(color.rgb * alpha, alpha);
final_color = blend(final_color, base_color);
if final_color.a == 1.0 {
break;
}
}
var result: SortResult;
result.color = final_color;
result.depth = fragment_list[0].depth;
return result;
}
// OVER operator using premultiplied alpha
// see: https://en.wikipedia.org/wiki/Alpha_compositing
fn blend(color_a: vec4<f32>, color_b: vec4<f32>) -> vec4<f32> {
let final_color = color_a.rgb + (1.0 - color_a.a) * color_b.rgb;
let alpha = color_a.a + (1.0 - color_a.a) * color_b.a;
return vec4(final_color.rgb, alpha);
}
+410
View File
@@ -0,0 +1,410 @@
//! Run a prepass before the main pass to generate depth, normals, and/or motion vectors textures, sometimes called a thin g-buffer.
//! These textures are useful for various screen-space effects and reducing overdraw in the main pass.
//!
//! The prepass only runs for opaque meshes or meshes with an alpha mask. Transparent meshes are ignored.
//!
//! To enable the prepass, you need to add a prepass component to a [`bevy_camera::Camera3d`].
//!
//! [`DepthPrepass`]
//! [`NormalPrepass`]
//! [`MotionVectorPrepass`]
//!
//! The textures are automatically added to the default mesh view bindings. You can also get the raw textures
//! by querying the [`ViewPrepassTextures`] component on any camera with a prepass component.
//!
//! The depth prepass will always run and generate the depth buffer as a side effect, but it won't copy it
//! to a separate texture unless the [`DepthPrepass`] is activated. This means that if any prepass component is present
//! it will always create a depth buffer that will be used by the main pass.
//!
//! When using the default mesh view bindings you should be able to use `prepass_depth()`,
//! `prepass_normal()`, and `prepass_motion_vector()` to load the related textures.
//! These functions are defined in `bevy_pbr::prepass_utils`. See the `shader_prepass` example that shows how to use them.
//!
//! The prepass runs for each `Material`. You can control if the prepass should run per-material by setting the `prepass_enabled`
//! flag on the `MaterialPlugin`.
//!
//! Currently only works for 3D.
pub mod node;
use core::ops::Range;
use crate::deferred::{DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT};
use bevy_asset::UntypedAssetId;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::mesh::allocator::SlabId;
use bevy_render::render_phase::PhaseItemBatchSetKey;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
PhaseItemExtraIndex,
},
render_resource::{
CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
ShaderType, TextureFormat, TextureView,
},
texture::ColorAttachment,
};
pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm;
pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float;
/// If added to a [`bevy_camera::Camera3d`] then depth values will be copied to a separate texture available to the main pass.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct DepthPrepass;
/// If added to a [`bevy_camera::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass.
/// Normals will have normal map textures already applied.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct NormalPrepass;
/// If added to a [`bevy_camera::Camera3d`] then screen space motion vectors will be copied to a separate texture available to the main pass.
///
/// Motion vectors are stored in the range -1,1, with +x right and +y down.
/// A value of (1.0,1.0) indicates a pixel moved from the top left corner to the bottom right corner of the screen.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct MotionVectorPrepass;
/// If added to a [`bevy_camera::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes.
/// Note the default deferred lighting plugin also requires `DepthPrepass` to work correctly.
#[derive(Component, Default, Reflect)]
#[reflect(Component, Default)]
pub struct DeferredPrepass;
/// Allows querying the previous frame's [`DepthPrepass`].
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
#[require(DepthPrepass)]
pub struct DepthPrepassDoubleBuffer;
/// Allows querying the previous frame's [`DeferredPrepass`].
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
#[require(DeferredPrepass)]
pub struct DeferredPrepassDoubleBuffer;
/// View matrices from the previous frame.
///
/// Useful for temporal rendering techniques that need access to last frame's camera data.
#[derive(Component, ShaderType, Clone)]
pub struct PreviousViewData {
pub view_from_world: Mat4,
pub clip_from_world: Mat4,
pub clip_from_view: Mat4,
pub world_from_clip: Mat4,
pub view_from_clip: Mat4,
}
#[derive(Resource, Default)]
pub struct PreviousViewUniforms {
pub uniforms: DynamicUniformBuffer<PreviousViewData>,
}
#[derive(Component)]
pub struct PreviousViewUniformOffset {
pub offset: u32,
}
/// Textures that are written to by the prepass.
///
/// This component will only be present if any of the relevant prepass components are also present.
#[derive(Component)]
pub struct ViewPrepassTextures {
/// The depth texture generated by the prepass.
/// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub depth: Option<ColorAttachment>,
/// The normals texture generated by the prepass.
/// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub normal: Option<ColorAttachment>,
/// The motion vectors texture generated by the prepass.
/// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget`
pub motion_vectors: Option<ColorAttachment>,
/// The deferred gbuffer generated by the deferred pass.
/// Exists only if [`DeferredPrepass`] is added to the `ViewTarget`
pub deferred: Option<ColorAttachment>,
/// A texture that specifies the deferred lighting pass id for a material.
/// Exists only if [`DeferredPrepass`] is added to the `ViewTarget`
pub deferred_lighting_pass_id: Option<ColorAttachment>,
/// The size of the textures.
pub size: Extent3d,
}
impl ViewPrepassTextures {
pub fn depth_view(&self) -> Option<&TextureView> {
self.depth.as_ref().map(|t| &t.texture.default_view)
}
pub fn previous_depth_view(&self) -> Option<&TextureView> {
self.depth
.as_ref()
.and_then(|t| t.previous_frame_texture.as_ref().map(|t| &t.default_view))
}
pub fn normal_view(&self) -> Option<&TextureView> {
self.normal.as_ref().map(|t| &t.texture.default_view)
}
pub fn motion_vectors_view(&self) -> Option<&TextureView> {
self.motion_vectors
.as_ref()
.map(|t| &t.texture.default_view)
}
pub fn deferred_view(&self) -> Option<&TextureView> {
self.deferred.as_ref().map(|t| &t.texture.default_view)
}
pub fn previous_deferred_view(&self) -> Option<&TextureView> {
self.deferred
.as_ref()
.and_then(|t| t.previous_frame_texture.as_ref().map(|t| &t.default_view))
}
}
/// Opaque phase of the 3D prepass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all 3D meshes with materials that have no transparency.
pub struct Opaque3dPrepass {
/// 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: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
/// An entity from which Bevy fetches data common to all instances in this
/// batch, such as the mesh.
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
/// Information that must be identical in order to place opaque meshes in the
/// same *batch set* in the prepass and deferred pass.
///
/// 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 OpaqueNoLightmap3dBatchSetKey {
/// The ID of the GPU pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw the mesh.
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<u32>,
/// 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<SlabId>,
}
impl PhaseItemBatchSetKey for OpaqueNoLightmap3dBatchSetKey {
fn indexed(&self) -> bool {
self.index_slab.is_some()
}
}
// TODO: Try interning these.
/// The data used to bin each opaque 3D object in the prepass and deferred pass.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OpaqueNoLightmap3dBinKey {
/// The ID of the asset.
pub asset_id: UntypedAssetId,
}
impl PhaseItem for Opaque3dPrepass {
#[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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&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<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque3dPrepass {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Opaque3dPrepass {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for Opaque3dPrepass {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
/// Alpha mask phase of the 3D prepass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dPrepass {
/// 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: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for AlphaMask3dPrepass {
#[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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&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<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask3dPrepass {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
pub fn prepass_target_descriptors(
normal_prepass: bool,
motion_vector_prepass: bool,
deferred_prepass: bool,
) -> Vec<Option<ColorTargetState>> {
vec![
normal_prepass.then_some(ColorTargetState {
format: NORMAL_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
motion_vector_prepass.then_some(ColorTargetState {
format: MOTION_VECTOR_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
deferred_prepass.then_some(ColorTargetState {
format: DEFERRED_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
deferred_prepass.then_some(ColorTargetState {
format: DEFERRED_LIGHTING_PASS_ID_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
]
}
+255
View File
@@ -0,0 +1,255 @@
use bevy_camera::{MainPassResolutionOverride, Viewport};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
experimental::occlusion_culling::OcclusionCulling,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture, ViewUniformOffset},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use crate::skybox::prepass::{RenderSkyboxPrepassPipeline, SkyboxPrepassBindGroup};
use super::{
AlphaMask3dPrepass, DeferredPrepass, Opaque3dPrepass, PreviousViewUniformOffset,
ViewPrepassTextures,
};
/// The phase of the prepass that draws meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this prepass simply draws all meshes.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct EarlyPrepassNode;
impl ViewNode for EarlyPrepassNode {
type ViewQuery = <LatePrepassNode as ViewNode>::ViewQuery;
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_prepass(graph, render_context, view_query, world, "early prepass")
}
}
/// The phase of the prepass that runs after occlusion culling against the
/// meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this is a no-op.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct LatePrepassNode;
impl ViewNode for LatePrepassNode {
type ViewQuery = (
(
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
),
(
Option<&'static DeferredPrepass>,
Option<&'static RenderSkyboxPrepassPipeline>,
Option<&'static SkyboxPrepassBindGroup>,
Option<&'static PreviousViewUniformOffset>,
Option<&'static MainPassResolutionOverride>,
),
(
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
),
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
query: QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect
// drawing.
let (_, _, (occlusion_culling, no_indirect_drawing, _)) = query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
run_prepass(graph, render_context, query, world, "late prepass")
}
}
/// Runs a prepass that draws all meshes to the depth buffer, and possibly
/// normal and motion vector buffers as well.
///
/// If occlusion culling isn't in use, and a prepass is enabled, then there's
/// only one prepass. If occlusion culling is in use, then any prepass is split
/// into two: an *early* prepass and a *late* prepass. The early prepass draws
/// what was visible last frame, and the last prepass performs occlusion culling
/// against a conservative hierarchical Z buffer before drawing unoccluded
/// meshes.
fn run_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
(camera, extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset),
(
deferred_prepass,
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
resolution_override,
),
(_, _, has_deferred),
): QueryItem<'w, '_, <LatePrepassNode as ViewNode>::ViewQuery>,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
// If we're using deferred rendering, there will be a deferred prepass
// instead of this one. Just bail out so we don't have to bother looking at
// the empty bins.
if has_deferred {
return Ok(());
}
let (Some(opaque_prepass_phases), Some(alpha_mask_prepass_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>(),
) else {
return Ok(());
};
let (Some(opaque_prepass_phase), Some(alpha_mask_prepass_phase)) = (
opaque_prepass_phases.get(&extracted_view.retained_view_entity),
alpha_mask_prepass_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let mut 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,
];
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _prepass_span = info_span!("prepass").entered();
// Command encoder setup
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &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, label);
if let Some(viewport) =
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
{
render_pass.set_camera_viewport(&viewport);
}
// Opaque draws
if !opaque_prepass_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_prepass").entered();
if let Err(err) = opaque_prepass_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque prepass phase {err:?}");
}
}
// Alpha masked draws
if !alpha_mask_prepass_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered();
if let Err(err) = alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the alpha mask prepass phase {err:?}");
}
}
// Skybox draw using a fullscreen triangle
if let (
Some(skybox_prepass_pipeline),
Some(skybox_prepass_bind_group),
Some(view_prev_uniform_offset),
) = (
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
) {
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_prepass_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
&skybox_prepass_bind_group.0,
&[view_uniform_offset.offset, view_prev_uniform_offset.offset],
);
render_pass.draw(0..3, 0..1);
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
// After rendering to the view depth texture, copy it to the prepass depth texture if deferred isn't going to
if deferred_prepass.is_none()
&& let Some(prepass_depth_texture) = &view_prepass_textures.depth
{
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
command_encoder.finish()
});
Ok(())
}
+302
View File
@@ -0,0 +1,302 @@
use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle};
use bevy_camera::Exposure;
use bevy_ecs::{
prelude::{Component, Entity},
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res, ResMut},
};
use bevy_image::{BevyDefault, Image};
use bevy_math::{Mat4, Quat};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_cube, uniform_buffer},
*,
},
renderer::RenderDevice,
texture::GpuImage,
view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderStartup, RenderSystems,
};
use bevy_shader::Shader;
use bevy_transform::components::Transform;
use bevy_utils::default;
use prepass::SkyboxPrepassPipeline;
use crate::{
core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms,
skybox::prepass::init_skybox_prepass_pipeline,
};
pub mod prepass;
pub struct SkyboxPlugin;
impl Plugin for SkyboxPlugin {
fn build(&self, app: &mut App) {
embedded_asset!(app, "skybox.wgsl");
embedded_asset!(app, "skybox_prepass.wgsl");
app.add_plugins((
ExtractComponentPlugin::<Skybox>::default(),
UniformComponentPlugin::<SkyboxUniforms>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.init_resource::<SpecializedRenderPipelines<SkyboxPrepassPipeline>>()
.init_resource::<PreviousViewUniforms>()
.add_systems(
RenderStartup,
(init_skybox_pipeline, init_skybox_prepass_pipeline),
)
.add_systems(
Render,
(
prepare_skybox_pipelines.in_set(RenderSystems::Prepare),
prepass::prepare_skybox_prepass_pipelines.in_set(RenderSystems::Prepare),
prepare_skybox_bind_groups.in_set(RenderSystems::PrepareBindGroups),
prepass::prepare_skybox_prepass_bind_groups
.in_set(RenderSystems::PrepareBindGroups),
),
);
}
}
/// Adds a skybox to a 3D camera, based on a cubemap texture.
///
/// Note that this component does not (currently) affect the scene's lighting.
/// To do so, use `EnvironmentMapLight` alongside this component.
///
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct Skybox {
pub image: Handle<Image>,
/// Scale factor applied to the skybox image.
/// After applying this multiplier to the image samples, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
pub brightness: f32,
/// View space rotation applied to the skybox cubemap.
/// This is useful for users who require a different axis, such as the Z-axis, to serve
/// as the vertical axis.
pub rotation: Quat,
}
impl Default for Skybox {
fn default() -> Self {
Skybox {
image: Handle::default(),
brightness: 0.0,
rotation: Quat::IDENTITY,
}
}
}
impl ExtractComponent for Skybox {
type QueryData = (&'static Self, Option<&'static Exposure>);
type QueryFilter = ();
type Out = (Self, SkyboxUniforms);
fn extract_component(
(skybox, exposure): QueryItem<'_, '_, Self::QueryData>,
) -> Option<Self::Out> {
let exposure = exposure
.map(Exposure::exposure)
.unwrap_or_else(|| Exposure::default().exposure());
Some((
skybox.clone(),
SkyboxUniforms {
brightness: skybox.brightness * exposure,
transform: Transform::from_rotation(skybox.rotation.inverse()).to_matrix(),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_8b: 0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_12b: 0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_16b: 0,
},
))
}
}
// TODO: Replace with a push constant once WebGPU gets support for that
#[derive(Component, ShaderType, Clone)]
pub struct SkyboxUniforms {
brightness: f32,
transform: Mat4,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_8b: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_12b: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_16b: u32,
}
#[derive(Resource)]
struct SkyboxPipeline {
bind_group_layout: BindGroupLayoutDescriptor,
shader: Handle<Shader>,
}
impl SkyboxPipeline {
fn new(shader: Handle<Shader>) -> Self {
Self {
bind_group_layout: BindGroupLayoutDescriptor::new(
"skybox_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_cube(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<ViewUniform>(true)
.visibility(ShaderStages::VERTEX_FRAGMENT),
uniform_buffer::<SkyboxUniforms>(true),
),
),
),
shader,
}
}
}
fn init_skybox_pipeline(mut commands: Commands, asset_server: Res<AssetServer>) {
let shader = load_embedded_asset!(asset_server.as_ref(), "skybox.wgsl");
commands.insert_resource(SkyboxPipeline::new(shader));
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct SkyboxPipelineKey {
hdr: bool,
samples: u32,
depth_format: TextureFormat,
}
impl SpecializedRenderPipeline for SkyboxPipeline {
type Key = SkyboxPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("skybox_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
vertex: VertexState {
shader: self.shader.clone(),
..default()
},
depth_stencil: Some(DepthStencilState {
format: key.depth_format,
depth_write_enabled: false,
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.samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(FragmentState {
shader: self.shader.clone(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
// BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases.
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
}
}
}
#[derive(Component)]
pub struct SkyboxPipelineId(pub CachedRenderPipelineId);
fn prepare_skybox_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
pipeline: Res<SkyboxPipeline>,
views: Query<(Entity, &ExtractedView, &Msaa), With<Skybox>>,
) {
for (entity, view, msaa) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
SkyboxPipelineKey {
hdr: view.hdr,
samples: msaa.samples(),
depth_format: CORE_3D_DEPTH_FORMAT,
},
);
commands
.entity(entity)
.insert(SkyboxPipelineId(pipeline_id));
}
}
#[derive(Component)]
pub struct SkyboxBindGroup(pub (BindGroup, u32));
fn prepare_skybox_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPipeline>,
view_uniforms: Res<ViewUniforms>,
skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
images: Res<RenderAssets<GpuImage>>,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
) {
for (entity, skybox, skybox_uniform_index) in &views {
if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = (
images.get(&skybox.image),
view_uniforms.uniforms.binding(),
skybox_uniforms.binding(),
) {
let bind_group = render_device.create_bind_group(
"skybox_bind_group",
&pipeline_cache.get_bind_group_layout(&pipeline.bind_group_layout),
&BindGroupEntries::sequential((
&skybox.texture_view,
&skybox.sampler,
view_uniforms,
skybox_uniforms,
)),
);
commands
.entity(entity)
.insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
}
}
}
+164
View File
@@ -0,0 +1,164 @@
//! Adds motion vector support to skyboxes. See [`SkyboxPrepassPipeline`] for details.
use bevy_asset::{load_embedded_asset, AssetServer, Handle};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Has, With},
resource::Resource,
system::{Commands, Query, Res, ResMut},
};
use bevy_render::{
render_resource::{
binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayoutDescriptor,
BindGroupLayoutEntries, CachedRenderPipelineId, CompareFunction, DepthStencilState,
FragmentState, MultisampleState, PipelineCache, RenderPipelineDescriptor, ShaderStages,
SpecializedRenderPipeline, SpecializedRenderPipelines,
},
renderer::RenderDevice,
view::{Msaa, ViewUniform, ViewUniforms},
};
use bevy_shader::Shader;
use bevy_utils::prelude::default;
use crate::{
core_3d::CORE_3D_DEPTH_FORMAT,
prepass::{
prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData,
PreviousViewUniforms,
},
FullscreenShader, Skybox,
};
/// This pipeline writes motion vectors to the prepass for all [`Skybox`]es.
///
/// This allows features like motion blur and TAA to work correctly on the skybox. Without this, for
/// example, motion blur would not be applied to the skybox when the camera is rotated and motion
/// blur is enabled.
#[derive(Resource)]
pub struct SkyboxPrepassPipeline {
bind_group_layout: BindGroupLayoutDescriptor,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
}
/// Used to specialize the [`SkyboxPrepassPipeline`].
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct SkyboxPrepassPipelineKey {
samples: u32,
normal_prepass: bool,
}
/// Stores the ID for a camera's specialized pipeline, so it can be retrieved from the
/// [`PipelineCache`].
#[derive(Component)]
pub struct RenderSkyboxPrepassPipeline(pub CachedRenderPipelineId);
/// Stores the [`SkyboxPrepassPipeline`] bind group for a camera. This is later used by the prepass
/// render graph node to add this binding to the prepass's render pass.
#[derive(Component)]
pub struct SkyboxPrepassBindGroup(pub BindGroup);
pub fn init_skybox_prepass_pipeline(
mut commands: Commands,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
) {
commands.insert_resource(SkyboxPrepassPipeline {
bind_group_layout: BindGroupLayoutDescriptor::new(
"skybox_prepass_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<PreviousViewData>(true),
),
),
),
fullscreen_shader: fullscreen_shader.clone(),
fragment_shader: load_embedded_asset!(asset_server.as_ref(), "skybox_prepass.wgsl"),
});
}
impl SpecializedRenderPipeline for SkyboxPrepassPipeline {
type Key = SkyboxPrepassPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("skybox_prepass_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
vertex: self.fullscreen_shader.to_vertex_state(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: false,
depth_compare: CompareFunction::GreaterEqual,
stencil: default(),
bias: default(),
}),
multisample: MultisampleState {
count: key.samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
targets: prepass_target_descriptors(key.normal_prepass, true, false),
..default()
}),
..default()
}
}
}
/// Specialize and cache the [`SkyboxPrepassPipeline`] for each camera with a [`Skybox`].
pub fn prepare_skybox_prepass_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPrepassPipeline>>,
pipeline: Res<SkyboxPrepassPipeline>,
views: Query<(Entity, Has<NormalPrepass>, &Msaa), (With<Skybox>, With<MotionVectorPrepass>)>,
) {
for (entity, normal_prepass, msaa) in &views {
let pipeline_key = SkyboxPrepassPipelineKey {
samples: msaa.samples(),
normal_prepass,
};
let render_skybox_prepass_pipeline =
pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key);
commands
.entity(entity)
.insert(RenderSkyboxPrepassPipeline(render_skybox_prepass_pipeline));
}
}
/// Creates the required bind groups for the [`SkyboxPrepassPipeline`]. This binds the view uniforms
/// from the CPU for access in the prepass shader on the GPU, allowing us to compute camera motion
/// between frames. This is then stored in the [`SkyboxPrepassBindGroup`] component on the camera.
pub fn prepare_skybox_prepass_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPrepassPipeline>,
view_uniforms: Res<ViewUniforms>,
prev_view_uniforms: Res<PreviousViewUniforms>,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
views: Query<Entity, (With<Skybox>, With<MotionVectorPrepass>)>,
) {
for entity in &views {
let (Some(view_uniforms), Some(prev_view_uniforms)) = (
view_uniforms.uniforms.binding(),
prev_view_uniforms.uniforms.binding(),
) else {
continue;
};
let bind_group = render_device.create_bind_group(
"skybox_prepass_bind_group",
&pipeline_cache.get_bind_group_layout(&pipeline.bind_group_layout),
&BindGroupEntries::sequential((view_uniforms, prev_view_uniforms)),
);
commands
.entity(entity)
.insert(SkyboxPrepassBindGroup(bind_group));
}
}
+81
View File
@@ -0,0 +1,81 @@
#import bevy_render::view::View
#import bevy_pbr::utils::coords_to_viewport_uv
struct SkyboxUniforms {
brightness: f32,
transform: mat4x4<f32>,
#ifdef SIXTEEN_BYTE_ALIGNMENT
_wasm_padding_8b: u32,
_wasm_padding_12b: u32,
_wasm_padding_16b: u32,
#endif
}
@group(0) @binding(0) var skybox: texture_cube<f32>;
@group(0) @binding(1) var skybox_sampler: sampler;
@group(0) @binding(2) var<uniform> view: View;
@group(0) @binding(3) var<uniform> uniforms: SkyboxUniforms;
fn coords_to_ray_direction(position: vec2<f32>, viewport: vec4<f32>) -> vec3<f32> {
// 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(
coords_to_viewport_uv(position, viewport) * vec2(2.0, -2.0) + vec2(-1.0, 1.0),
1.0,
1.0,
);
// Transforming the view space ray direction by the skybox transform matrix, it is
// equivalent to rotating the skybox itself.
var view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w;
view_ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz;
// Transforming the view space ray direction by the 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 = (uniforms.transform * vec4(view_ray_direction, 0.0)).xyz;
return normalize(ray_direction);
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
// 3 | 2.
// 2 | : `.
// 1 | x-----x.
// 0 | | s | `.
// -1 | 0-----x.....1
// +---------------
// -1 0 1 2 3
//
// The axes are clip-space x and y. The region marked s is the visible region.
// The digits in the corners of the right-angled triangle are the vertex
// indices.
@vertex
fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
// See the explanation above for how this works.
let clip_position = vec2(
f32(vertex_index & 1u),
f32((vertex_index >> 1u) & 1u),
) * 4.0 - vec2(1.0);
return VertexOutput(vec4(clip_position, 0.0, 1.0));
}
@fragment
fn skybox_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport);
// Cube maps are left-handed so we negate the z coordinate.
let out = textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0));
return vec4(out.rgb * uniforms.brightness, out.a);
}
@@ -0,0 +1,24 @@
#import bevy_render::view::View
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::view_transformations::uv_to_ndc
struct PreviousViewUniforms {
view_from_world: mat4x4<f32>,
clip_from_world: mat4x4<f32>,
clip_from_view: mat4x4<f32>,
world_from_clip: mat4x4<f32>,
view_from_clip: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> previous_view: PreviousViewUniforms;
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(1) vec4<f32> {
let clip_pos = uv_to_ndc(in.uv); // Convert from uv to clip space
let world_pos = view.world_from_clip * vec4(clip_pos, 0.0, 1.0);
let prev_clip_pos = (previous_view.clip_from_world * world_pos).xy;
let velocity = (clip_pos - prev_clip_pos) * vec2(0.5, -0.5); // Copied from mesh motion vectors
return vec4(velocity.x, velocity.y, 0.0, 1.0);
}
@@ -0,0 +1,5 @@
#define_import_path bevy_core_pipeline::tonemapping_lut_bindings
@group(0) @binding(#TONEMAPPING_LUT_TEXTURE_BINDING_INDEX) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(#TONEMAPPING_LUT_SAMPLER_BINDING_INDEX) var dt_lut_sampler: sampler;
+22
View File
@@ -0,0 +1,22 @@
--- Process for recreating AgX-default_contrast.ktx2 ---
Download:
https://github.com/MrLixm/AgXc/blob/898198e0490b0551ed81412a0c22e0b72fffb7cd/obs/obs-script/AgX-default_contrast.lut.png
Convert to vertical strip exr with:
https://gist.github.com/DGriffin91/fc8e0cfd55aaa175ac10199403bc19b8
Convert exr to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca
--- Process for recreating tony_mc_mapface.ktx2 ---
Download:
https://github.com/h3r2tic/tony-mc-mapface/blob/909e51c8a74251fd828770248476cb084081e08c/tony_mc_mapface.dds
Convert dds to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca
--- Process for recreating Blender_-11_12.ktx2 ---
Create LUT stimulus with:
https://gist.github.com/DGriffin91/e119bf32b520e219f6e102a6eba4a0cf
Open LUT image in Blender's image editor and make sure color space is set to linear.
Export from Blender as 32bit EXR, override color space to Filmic sRGB.
Import EXR back into blender set color space to sRGB, then export as 32bit EXR override color space to linear.
Convert exr to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca
+456
View File
@@ -0,0 +1,456 @@
use bevy_app::prelude::*;
use bevy_asset::{
embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
};
use bevy_camera::Camera;
use bevy_ecs::prelude::*;
use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_2d, texture_3d, uniform_buffer},
*,
},
renderer::RenderDevice,
texture::{FallbackImage, GpuImage},
view::{ExtractedView, ViewTarget, ViewUniform},
Render, RenderApp, RenderStartup, RenderSystems,
};
use bevy_shader::{load_shader_library, Shader, ShaderDefVal};
use bitflags::bitflags;
#[cfg(not(feature = "tonemapping_luts"))]
use tracing::error;
mod node;
use bevy_utils::default;
pub use node::TonemappingNode;
use crate::FullscreenShader;
/// 3D LUT (look up table) textures used for tonemapping
#[derive(Resource, Clone, ExtractResource)]
pub struct TonemappingLuts {
pub blender_filmic: Handle<Image>,
pub agx: Handle<Image>,
pub tony_mc_mapface: Handle<Image>,
}
pub struct TonemappingPlugin;
impl Plugin for TonemappingPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "tonemapping_shared.wgsl");
load_shader_library!(app, "lut_bindings.wgsl");
embedded_asset!(app, "tonemapping.wgsl");
if !app.world().is_resource_added::<TonemappingLuts>() {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
#[cfg(feature = "tonemapping_luts")]
let tonemapping_luts = {
TonemappingLuts {
blender_filmic: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/Blender_-11_12.ktx2"),
ImageType::Extension("ktx2"),
)),
agx: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/AgX-default_contrast.ktx2"),
ImageType::Extension("ktx2"),
)),
tony_mc_mapface: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/tony_mc_mapface.ktx2"),
ImageType::Extension("ktx2"),
)),
}
};
#[cfg(not(feature = "tonemapping_luts"))]
let tonemapping_luts = {
let placeholder = images.add(lut_placeholder());
TonemappingLuts {
blender_filmic: placeholder.clone(),
agx: placeholder.clone(),
tony_mc_mapface: placeholder,
}
};
app.insert_resource(tonemapping_luts);
}
app.add_plugins(ExtractResourcePlugin::<TonemappingLuts>::default());
app.add_plugins((
ExtractComponentPlugin::<Tonemapping>::default(),
ExtractComponentPlugin::<DebandDither>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
.add_systems(RenderStartup, init_tonemapping_pipeline)
.add_systems(
Render,
prepare_view_tonemapping_pipelines.in_set(RenderSystems::Prepare),
);
}
}
#[derive(Resource)]
pub struct TonemappingPipeline {
texture_bind_group: BindGroupLayoutDescriptor,
sampler: Sampler,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
}
/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity.
#[derive(
Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Debug, Hash, Default, PartialEq)]
pub enum Tonemapping {
/// Bypass tonemapping.
None,
/// Suffers from lots hue shifting, brights don't desaturate naturally.
/// Bright primaries and secondaries don't desaturate at all.
Reinhard,
/// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum.
ReinhardLuminance,
/// Same base implementation that Godot 4.0 uses for Tonemap ACES.
/// <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
/// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting.
/// Bright greens and reds turn orange. Bright blues turn magenta.
/// Significantly increased contrast. Brights desaturate across the spectrum.
AcesFitted,
/// By Troy Sobotka
/// <https://github.com/sobotka/AgX>
/// Very neutral. Image is somewhat desaturated when compared to other tonemappers.
/// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect).
/// NOTE: Requires the `tonemapping_luts` cargo feature.
AgX,
/// By Tomasz Stachowiak
/// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum.
/// Is sort of between Reinhard and `ReinhardLuminance`. Conceptually similar to reinhard-jodie.
/// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your
/// VFX to look good without hue shifting.
SomewhatBoringDisplayTransform,
/// Current Bevy default.
/// By Tomasz Stachowiak
/// <https://github.com/h3r2tic/tony-mc-mapface>
/// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum.
/// Comment from author:
/// Tony is a display transform intended for real-time applications such as games.
/// It is intentionally boring, does not increase contrast or saturation, and stays close to the
/// input stimulus where compression isn't necessary.
/// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard.
/// Color hues are preserved during compression, except for a deliberate [Bezold–Brücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift).
/// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect).
/// NOTE: Requires the `tonemapping_luts` cargo feature.
#[default]
TonyMcMapface,
/// Default Filmic Display Transform from blender.
/// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum.
/// NOTE: Requires the `tonemapping_luts` cargo feature.
BlenderFilmic,
}
impl Tonemapping {
pub fn is_enabled(&self) -> bool {
*self != Tonemapping::None
}
}
bitflags! {
/// Various flags describing what tonemapping needs to do.
///
/// This allows the shader to skip unneeded steps.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TonemappingPipelineKeyFlags: u8 {
/// The hue needs to be changed.
const HUE_ROTATE = 0x01;
/// The white balance needs to be adjusted.
const WHITE_BALANCE = 0x02;
/// Saturation/contrast/gamma/gain/lift for one or more sections
/// (shadows, midtones, highlights) need to be adjusted.
const SECTIONAL_COLOR_GRADING = 0x04;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TonemappingPipelineKey {
deband_dither: DebandDither,
tonemapping: Tonemapping,
flags: TonemappingPipelineKeyFlags,
}
impl SpecializedRenderPipeline for TonemappingPipeline {
type Key = TonemappingPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = Vec::new();
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
3,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
4,
));
if let DebandDither::Enabled = key.deband_dither {
shader_defs.push("DEBAND_DITHER".into());
}
// Define shader flags depending on the color grading options in use.
if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) {
shader_defs.push("HUE_ROTATE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::WHITE_BALANCE)
{
shader_defs.push("WHITE_BALANCE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING)
{
shader_defs.push("SECTIONAL_COLOR_GRADING".into());
}
match key.tonemapping {
Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
Tonemapping::ReinhardLuminance => {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
}
Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()),
Tonemapping::AgX => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"AgX tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_AGX".into());
}
Tonemapping::SomewhatBoringDisplayTransform => {
shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
}
Tonemapping::TonyMcMapface => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"TonyMcMapFace tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
}
Tonemapping::BlenderFilmic => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"BlenderFilmic tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
}
}
RenderPipelineDescriptor {
label: Some("tonemapping pipeline".into()),
layout: vec![self.texture_bind_group.clone()],
vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.fragment_shader.clone(),
shader_defs,
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
}
}
}
pub fn init_tonemapping_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
) {
let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
ShaderStages::FRAGMENT,
(
(0, uniform_buffer::<ViewUniform>(true)),
(
1,
texture_2d(TextureSampleType::Float { filterable: false }),
),
(2, sampler(SamplerBindingType::NonFiltering)),
),
);
let lut_layout_entries = get_lut_bind_group_layout_entries();
entries = entries.extend_with_indices(((3, lut_layout_entries[0]), (4, lut_layout_entries[1])));
let tonemap_texture_bind_group =
BindGroupLayoutDescriptor::new("tonemapping_hdr_texture_bind_group_layout", &entries);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
commands.insert_resource(TonemappingPipeline {
texture_bind_group: tonemap_texture_bind_group,
sampler,
fullscreen_shader: fullscreen_shader.clone(),
fragment_shader: load_embedded_asset!(asset_server.as_ref(), "tonemapping.wgsl"),
});
}
#[derive(Component)]
pub struct ViewTonemappingPipeline(CachedRenderPipelineId);
pub fn prepare_view_tonemapping_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
upscaling_pipeline: Res<TonemappingPipeline>,
view_targets: Query<
(
Entity,
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
),
With<ViewTarget>,
>,
) {
for (entity, view, tonemapping, dither) in view_targets.iter() {
// As an optimization, we omit parts of the shader that are unneeded.
let mut flags = TonemappingPipelineKeyFlags::empty();
flags.set(
TonemappingPipelineKeyFlags::HUE_ROTATE,
view.color_grading.global.hue != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::WHITE_BALANCE,
view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING,
view.color_grading
.all_sections()
.any(|section| *section != default()),
);
let key = TonemappingPipelineKey {
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
flags,
};
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
commands
.entity(entity)
.insert(ViewTonemappingPipeline(pipeline));
}
}
/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity.
#[derive(
Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Debug, Hash, Default, PartialEq)]
pub enum DebandDither {
#[default]
Disabled,
Enabled,
}
pub fn get_lut_bindings<'a>(
images: &'a RenderAssets<GpuImage>,
tonemapping_luts: &'a TonemappingLuts,
tonemapping: &Tonemapping,
fallback_image: &'a FallbackImage,
) -> (&'a TextureView, &'a Sampler) {
let image = match tonemapping {
// AgX lut texture used when tonemapping doesn't need a texture since it's very small (32x32x32)
Tonemapping::None
| Tonemapping::Reinhard
| Tonemapping::ReinhardLuminance
| Tonemapping::AcesFitted
| Tonemapping::AgX
| Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx,
Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface,
Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic,
};
let lut_image = images.get(image).unwrap_or(&fallback_image.d3);
(&lut_image.texture_view, &lut_image.sampler)
}
pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] {
[
texture_3d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
]
}
#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")]
#[allow(
dead_code,
reason = "There is unused code when the `tonemapping_luts` feature is disabled."
)]
fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor {
label: Some("Tonemapping LUT sampler".to_string()),
address_mode_u: bevy_image::ImageAddressMode::ClampToEdge,
address_mode_v: bevy_image::ImageAddressMode::ClampToEdge,
address_mode_w: bevy_image::ImageAddressMode::ClampToEdge,
mag_filter: bevy_image::ImageFilterMode::Linear,
min_filter: bevy_image::ImageFilterMode::Linear,
mipmap_filter: bevy_image::ImageFilterMode::Linear,
..default()
});
Image::from_buffer(
bytes,
image_type,
CompressedImageFormats::NONE,
false,
image_sampler,
RenderAssetUsages::RENDER_WORLD,
)
.unwrap()
}
pub fn lut_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::D3,
label: None,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
},
sampler: ImageSampler::Default,
texture_view_descriptor: None,
asset_usage: RenderAssetUsages::RENDER_WORLD,
copy_on_resize: false,
}
}
+148
View File
@@ -0,0 +1,148 @@
use std::sync::Mutex;
use crate::tonemapping::{TonemappingLuts, TonemappingPipeline, ViewTonemappingPipeline};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
diagnostic::RecordDiagnostics,
render_asset::RenderAssets,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroup, BindGroupEntries, BufferId, LoadOp, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, StoreOp, TextureViewId,
},
renderer::RenderContext,
texture::{FallbackImage, GpuImage},
view::{ViewTarget, ViewUniformOffset, ViewUniforms},
};
use super::{get_lut_bindings, Tonemapping};
#[derive(Default)]
pub struct TonemappingNode {
cached_bind_group: Mutex<Option<(BufferId, TextureViewId, TextureViewId, BindGroup)>>,
last_tonemapping: Mutex<Option<Tonemapping>>,
}
impl ViewNode for TonemappingNode {
type ViewQuery = (
&'static ViewUniformOffset,
&'static ViewTarget,
&'static ViewTonemappingPipeline,
&'static Tonemapping,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_uniform_offset, target, view_tonemapping_pipeline, tonemapping): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let tonemapping_pipeline = world.resource::<TonemappingPipeline>();
let gpu_images = world.get_resource::<RenderAssets<GpuImage>>().unwrap();
let fallback_image = world.resource::<FallbackImage>();
let view_uniforms_resource = world.resource::<ViewUniforms>();
let view_uniforms = &view_uniforms_resource.uniforms;
let view_uniforms_id = view_uniforms.buffer().unwrap().id();
if *tonemapping == Tonemapping::None {
return Ok(());
}
if !target.is_hdr() {
return Ok(());
}
let Some(pipeline) = pipeline_cache.get_render_pipeline(view_tonemapping_pipeline.0) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let post_process = target.post_process_write();
let source = post_process.source;
let destination = post_process.destination;
let mut last_tonemapping = self.last_tonemapping.lock().unwrap();
let tonemapping_changed = if let Some(last_tonemapping) = &*last_tonemapping {
tonemapping != last_tonemapping
} else {
true
};
if tonemapping_changed {
*last_tonemapping = Some(*tonemapping);
}
let mut cached_bind_group = self.cached_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((buffer_id, texture_id, lut_id, bind_group))
if view_uniforms_id == *buffer_id
&& source.id() == *texture_id
&& *lut_id != fallback_image.d3.texture_view.id()
&& !tonemapping_changed =>
{
bind_group
}
cached_bind_group => {
let tonemapping_luts = world.resource::<TonemappingLuts>();
let lut_bindings =
get_lut_bindings(gpu_images, tonemapping_luts, tonemapping, fallback_image);
let bind_group = render_context.render_device().create_bind_group(
None,
&pipeline_cache.get_bind_group_layout(&tonemapping_pipeline.texture_bind_group),
&BindGroupEntries::sequential((
view_uniforms,
source,
&tonemapping_pipeline.sampler,
lut_bindings.0,
lut_bindings.1,
)),
);
let (_, _, _, bind_group) = cached_bind_group.insert((
view_uniforms_id,
source.id(),
lut_bindings.0.id(),
bind_group,
));
bind_group
}
};
let pass_descriptor = RenderPassDescriptor {
label: Some("tonemapping"),
color_attachments: &[Some(RenderPassColorAttachment {
view: destination,
depth_slice: None,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Default::default()), // TODO shouldn't need to be cleared
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(&pass_descriptor);
let pass_span = diagnostics.pass_span(&mut render_pass, "tonemapping");
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[view_uniform_offset.offset]);
render_pass.draw(0..3, 0..1);
pass_span.end(&mut render_pass);
Ok(())
}
}
@@ -0,0 +1,34 @@
#define TONEMAPPING_PASS
#import bevy_render::{
view::View,
maths::powsafe,
}
#import bevy_core_pipeline::{
fullscreen_vertex_shader::FullscreenVertexOutput,
tonemapping::{tone_mapping, screen_space_dither},
}
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var hdr_texture: texture_2d<f32>;
@group(0) @binding(2) var hdr_sampler: sampler;
@group(0) @binding(3) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(4) var dt_lut_sampler: sampler;
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv);
var output_rgb = tone_mapping(hdr_color, view.color_grading).rgb;
#ifdef DEBAND_DITHER
output_rgb = powsafe(output_rgb.rgb, 1.0 / 2.2);
output_rgb = output_rgb + screen_space_dither(in.position.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.rgb, 2.2);
#endif
return vec4<f32>(output_rgb, hdr_color.a);
}
@@ -0,0 +1,391 @@
#define_import_path bevy_core_pipeline::tonemapping
#import bevy_render::{
view::ColorGrading,
color_operations::{hsv_to_rgb, rgb_to_hsv},
maths::{PI_2, powsafe},
}
#import bevy_core_pipeline::tonemapping_lut_bindings::{
dt_lut_texture,
dt_lut_sampler,
}
// Half the size of the crossfade region between shadows and midtones and
// between midtones and highlights. This value, 0.1, corresponds to 10% of the
// gamut on either side of the cutoff point.
const LEVEL_MARGIN: f32 = 0.1;
// The inverse reciprocal of twice the above, used when scaling the midtone
// region.
const LEVEL_MARGIN_DIV: f32 = 0.5 / LEVEL_MARGIN;
fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
// Don't include code that will try to sample from LUTs if tonemap method doesn't require it
// Allows this file to be imported without necessarily needing the lut texture bindings
#ifdef TONEMAP_METHOD_AGX
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else
return vec3(1.0, 0.0, 1.0);
#endif
}
// --------------------------------------
// --- SomewhatBoringDisplayTransform ---
// --------------------------------------
// By Tomasz Stachowiak
fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
let m = mat3x3<f32>(
0.2126, 0.7152, 0.0722,
-0.1146, -0.3854, 0.5,
0.5, -0.4542, -0.0458
);
return col * m;
}
fn tonemap_curve(v: f32) -> f32 {
#ifdef 0
// Large linear part in the lows, but compresses highs.
float c = v + v * v + 0.5 * v * v * v;
return c / (1.0 + c);
#else
return 1.0 - exp(-v);
#endif
}
fn tonemap_curve3_(v: vec3<f32>) -> vec3<f32> {
return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b));
}
fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> {
var boring_color = col;
let ycbcr = rgb_to_ycbcr(boring_color);
let bt = tonemap_curve(length(ycbcr.yz) * 2.4);
var desat = max((bt - 0.7) * 0.8, 0.0);
desat *= desat;
let desat_col = mix(boring_color.rgb, ycbcr.xxx, desat);
let tm_luma = tonemap_curve(ycbcr.x);
let tm0 = boring_color.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(boring_color.rgb)));
let final_mult = 0.97;
let tm1 = tonemap_curve3_(desat_col);
boring_color = mix(tm0, tm1, bt * bt);
return boring_color * final_mult;
}
// ------------------------------------------
// ------------- Tony McMapface -------------
// ------------------------------------------
// By Tomasz Stachowiak
// https://github.com/h3r2tic/tony-mc-mapface
const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0;
fn sample_tony_mc_mapface_lut(stimulus: vec3<f32>) -> vec3<f32> {
var uv = (stimulus / (stimulus + 1.0)) * (f32(TONY_MC_MAPFACE_LUT_DIMS - 1.0) / f32(TONY_MC_MAPFACE_LUT_DIMS)) + 0.5 / f32(TONY_MC_MAPFACE_LUT_DIMS);
return sample_current_lut(saturate(uv)).rgb;
}
// ---------------------------------
// ---------- ACES Fitted ----------
// ---------------------------------
// Same base implementation that Godot 4.0 uses for Tonemap ACES.
// https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// The code in this file was originally written by Stephen Hill (@self_shadow), who deserves all
// credit for coming up with this fit and implementing it. Buy him a beer next time you see him. :)
fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
let a = v * (v + 0.0245786) - 0.000090537;
let b = v * (0.983729 * v + 0.4329510) + 0.238081;
return a / b;
}
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
var fitted_color = color;
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
let rgb_to_rrt = mat3x3<f32>(
vec3(0.59719, 0.35458, 0.04823),
vec3(0.07600, 0.90834, 0.01566),
vec3(0.02840, 0.13383, 0.83777)
);
// ODT_SAT => XYZ => D60_2_D65 => sRGB
let odt_to_rgb = mat3x3<f32>(
vec3(1.60475, -0.53108, -0.07367),
vec3(-0.10208, 1.10813, -0.00605),
vec3(-0.00327, -0.07276, 1.07602)
);
fitted_color *= rgb_to_rrt;
// Apply RRT and ODT
fitted_color = RRTAndODTFit(fitted_color);
fitted_color *= odt_to_rgb;
// Clamp to [0, 1]
fitted_color = saturate(fitted_color);
return fitted_color;
}
// -------------------------------
// ------------- AgX -------------
// -------------------------------
// By Troy Sobotka
// https://github.com/MrLixm/AgXc
// https://github.com/sobotka/AgX
/*
Increase color saturation of the given color data.
:param color: expected sRGB primaries input
:param saturationAmount: expected 0-1 range with 1=neutral, 0=no saturation.
-- ref[2] [4]
*/
fn saturation(color: vec3<f32>, saturationAmount: f32) -> vec3<f32> {
let luma = tonemapping_luminance(color);
return mix(vec3(luma), color, vec3(saturationAmount));
}
/*
Output log domain encoded data.
Similar to OCIO lg2 AllocationTransform.
ref[0]
*/
fn convertOpenDomainToNormalizedLog2_(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
let in_midgray = 0.18;
// remove negative before log transform
var normalized_color = max(vec3(0.0), color);
// avoid infinite issue with log -- ref[1]
normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3<f32>(0.00003051757));
normalized_color = clamp(
log2(normalized_color / in_midgray),
vec3(minimum_ev),
vec3(maximum_ev)
);
let total_exposure = maximum_ev - minimum_ev;
return (normalized_color - minimum_ev) / total_exposure;
}
// Inverse of above
fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
var open_color = color;
let in_midgray = 0.18;
let total_exposure = maximum_ev - minimum_ev;
open_color = (open_color * total_exposure) + minimum_ev;
open_color = pow(vec3(2.0), open_color);
open_color = open_color * in_midgray;
return open_color;
}
/*=================
Main processes
=================*/
// Prepare the data for display encoding. Converted to log domain.
fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> {
var prepared_image = max(vec3(0.0), Image); // clamp negatives
let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375));
let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613));
let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297));
prepared_image = vec3(r, g, b);
prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
return prepared_image;
}
fn applyLUT3D(Image: vec3<f32>, block_size: f32) -> vec3<f32> {
return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb;
}
// -------------------------
// -------------------------
// -------------------------
fn sample_blender_filmic_lut(stimulus: vec3<f32>) -> vec3<f32> {
let block_size = 64.0;
let normalized = saturate(convertOpenDomainToNormalizedLog2_(stimulus, -11.0, 12.0));
return applyLUT3D(normalized, block_size);
}
// from https://64.github.io/tonemapping/
// reinhard on RGB oversaturates colors
fn tonemapping_reinhard(color: vec3<f32>) -> vec3<f32> {
return color / (1.0 + color);
}
// luminance coefficients from Rec. 709.
// https://en.wikipedia.org/wiki/Rec._709
fn tonemapping_luminance(v: vec3<f32>) -> f32 {
return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
}
fn tonemapping_change_luminance(c_in: vec3<f32>, l_out: f32) -> vec3<f32> {
let l_in = tonemapping_luminance(c_in);
return c_in * (l_out / l_in);
}
fn tonemapping_reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
let l_old = tonemapping_luminance(color);
let l_new = l_old / (1.0 + l_old);
return tonemapping_change_luminance(color, l_new);
}
fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
return pow(color, vec3<f32>(1.0 / 2.2));
}
// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49
// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> {
var dither = vec3<f32>(dot(vec2<f32>(171.0, 231.0), frag_coord)).xxx;
dither = fract(dither.rgb / vec3<f32>(103.0, 71.0, 97.0));
return (dither - 0.5) / 255.0;
}
// Performs the "sectional" color grading: i.e. the color grading that applies
// individually to shadows, midtones, and highlights.
fn sectional_color_grading(
in: vec3<f32>,
color_grading: ptr<function, ColorGrading>,
) -> vec3<f32> {
var color = in;
// Determine whether the color is a shadow, midtone, or highlight. Colors
// close to the edges are considered a mix of both, to avoid sharp
// discontinuities. The formulas are taken from Blender's compositor.
let level = (color.r + color.g + color.b) / 3.0;
// Determine whether this color is a shadow, midtone, or highlight. If close
// to the cutoff points, blend between the two to avoid sharp color
// discontinuities.
var levels = vec3(0.0);
let midtone_range = (*color_grading).midtone_range;
if (level < midtone_range.x - LEVEL_MARGIN) {
levels.x = 1.0;
} else if (level < midtone_range.x + LEVEL_MARGIN) {
levels.y = ((level - midtone_range.x) * LEVEL_MARGIN_DIV) + 0.5;
levels.z = 1.0 - levels.y;
} else if (level < midtone_range.y - LEVEL_MARGIN) {
levels.y = 1.0;
} else if (level < midtone_range.y + LEVEL_MARGIN) {
levels.z = ((level - midtone_range.y) * LEVEL_MARGIN_DIV) + 0.5;
levels.y = 1.0 - levels.z;
} else {
levels.z = 1.0;
}
// Calculate contrast/saturation/gamma/gain/lift.
let contrast = dot(levels, (*color_grading).contrast);
let saturation = dot(levels, (*color_grading).saturation);
let gamma = dot(levels, (*color_grading).gamma);
let gain = dot(levels, (*color_grading).gain);
let lift = dot(levels, (*color_grading).lift);
// Adjust saturation and contrast.
let luma = tonemapping_luminance(color);
color = luma + saturation * (color - luma);
color = 0.5 + (color - 0.5) * contrast;
// The [ASC CDL] formula for color correction. Given *i*, an input color, we
// have:
//
// out = (i × s + o)ⁿ
//
// Following the normal photographic naming convention, *gain* is the *s*
// factor, *lift* is the *o* term, and the inverse of *gamma* is the *n*
// exponent.
//
// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
color = powsafe(color * gain + lift, 1.0 / gamma);
// Account for exposure.
color = color * powsafe(vec3(2.0), (*color_grading).exposure);
return max(color, vec3(0.0));
}
fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
var color = max(in.rgb, vec3(0.0));
var color_grading = in_color_grading; // So we can take pointers to it.
// Rotate hue if needed, by converting to and from HSV. Remember that hue is
// an angle, so it needs to be modulo 2π.
#ifdef HUE_ROTATE
var hsv = rgb_to_hsv(color);
hsv.r = (hsv.r + color_grading.hue) % PI_2;
color = hsv_to_rgb(hsv);
#endif
// Perform white balance correction. Conveniently, this is a linear
// transform. The matrix was pre-calculated from the temperature and tint
// values on the CPU.
#ifdef WHITE_BALANCE
color = max(color_grading.balance * color, vec3(0.0));
#endif
// Perform the "sectional" color grading: i.e. the color grading that
// applies individually to shadows, midtones, and highlights.
#ifdef SECTIONAL_COLOR_GRADING
color = sectional_color_grading(color, &color_grading);
#else
// If we're not doing sectional color grading, the exposure might still need
// to be applied, for example when using auto exposure.
color = color * powsafe(vec3(2.0), color_grading.exposure);
#endif
// tone_mapping
#ifdef TONEMAP_METHOD_NONE
color = color;
#else ifdef TONEMAP_METHOD_REINHARD
color = tonemapping_reinhard(color.rgb);
#else ifdef TONEMAP_METHOD_REINHARD_LUMINANCE
color = tonemapping_reinhard_luminance(color.rgb);
#else ifdef TONEMAP_METHOD_ACES_FITTED
color = ACESFitted(color.rgb);
#else ifdef TONEMAP_METHOD_AGX
color = applyAgXLog(color);
color = applyLUT3D(color, 32.0);
#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
color = somewhat_boring_display_transform(color.rgb);
#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
color = sample_tony_mc_mapface_lut(color);
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
color = sample_blender_filmic_lut(color.rgb);
#endif
// Perceptual post tonemapping grading
color = saturation(color, color_grading.post_saturation);
return vec4(color, in.a);
}
// This is an **incredibly crude** approximation of the inverse of the tone mapping function.
// We assume here that there's a simple linear relationship between the input and output
// which is not true at all, but useful to at least preserve the overall luminance of colors
// when sampling from an already tonemapped image. (e.g. for transmissive materials when HDR is off)
fn approximate_inverse_tone_mapping(in: vec4<f32>, color_grading: ColorGrading) -> vec4<f32> {
let out = tone_mapping(in, color_grading);
let approximate_ratio = length(in.rgb) / length(out.rgb);
return vec4(in.rgb * approximate_ratio, in.a);
}
+88
View File
@@ -0,0 +1,88 @@
use crate::blit::{BlitPipeline, BlitPipelineKey};
use bevy_app::prelude::*;
use bevy_camera::CameraOutputMode;
use bevy_ecs::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_render::{
camera::ExtractedCamera, render_resource::*, view::ViewTarget, Render, RenderApp, RenderSystems,
};
mod node;
pub use node::UpscalingNode;
pub struct UpscalingPlugin;
impl Plugin for UpscalingPlugin {
fn build(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(
Render,
// This system should probably technically be run *after* all of the other systems
// that might modify `PipelineCache` via interior mutability, but for now,
// we've chosen to simply ignore the ambiguities out of a desire for a better refactor
// and aversion to extensive and intrusive system ordering.
// See https://github.com/bevyengine/bevy/issues/14770 for more context.
prepare_view_upscaling_pipelines
.in_set(RenderSystems::Prepare)
.ambiguous_with_all(),
);
}
}
}
#[derive(Component)]
pub struct ViewUpscalingPipeline(CachedRenderPipelineId);
fn prepare_view_upscaling_pipelines(
mut commands: Commands,
mut pipeline_cache: ResMut<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
blit_pipeline: Res<BlitPipeline>,
view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>,
) {
let mut output_textures = <HashSet<_>>::default();
for (entity, view_target, camera) in view_targets.iter() {
let out_texture_id = view_target.out_texture().id();
let blend_state = if let Some(extracted_camera) = camera {
match extracted_camera.output_mode {
CameraOutputMode::Skip => None,
CameraOutputMode::Write { blend_state, .. } => {
let already_seen = output_textures.contains(&out_texture_id);
output_textures.insert(out_texture_id);
match blend_state {
None => {
// If we've already seen this output for a camera and it doesn't have an output blend
// mode configured, default to alpha blend so that we don't accidentally overwrite
// the output texture
if already_seen {
Some(BlendState::ALPHA_BLENDING)
} else {
None
}
}
_ => blend_state,
}
}
}
} else {
output_textures.insert(out_texture_id);
None
};
let key = BlitPipelineKey {
texture_format: view_target.out_texture_view_format(),
blend_state,
samples: 1,
};
let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
// Ensure the pipeline is loaded before continuing the frame to prevent frames without any GPU work submitted
pipeline_cache.block_on_render_pipeline(pipeline);
commands
.entity(entity)
.insert(ViewUpscalingPipeline(pipeline));
}
}
+107
View File
@@ -0,0 +1,107 @@
use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline};
use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{BindGroup, PipelineCache, RenderPassDescriptor, TextureViewId},
renderer::RenderContext,
view::ViewTarget,
};
use std::sync::Mutex;
#[derive(Default)]
pub struct UpscalingNode {
cached_texture_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
}
impl ViewNode for UpscalingNode {
type ViewQuery = (
&'static ViewTarget,
&'static ViewUpscalingPipeline,
Option<&'static ExtractedCamera>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(target, upscaling_target, camera): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let blit_pipeline = world.resource::<BlitPipeline>();
let clear_color_global = world.resource::<ClearColor>();
let diagnostics = render_context.diagnostic_recorder();
let clear_color = if let Some(camera) = camera {
match camera.output_mode {
CameraOutputMode::Write { clear_color, .. } => clear_color,
CameraOutputMode::Skip => return Ok(()),
}
} else {
ClearColorConfig::Default
};
let clear_color = match clear_color {
ClearColorConfig::Default => Some(clear_color_global.0),
ClearColorConfig::Custom(color) => Some(color),
ClearColorConfig::None => None,
};
let converted_clear_color = clear_color.map(Into::into);
// texture to be upscaled to the output texture
let main_texture_view = target.main_texture_view();
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((id, bind_group)) if main_texture_view.id() == *id => bind_group,
cached_bind_group => {
let bind_group = blit_pipeline.create_bind_group(
render_context.render_device(),
main_texture_view,
pipeline_cache,
);
let (_, bind_group) =
cached_bind_group.insert((main_texture_view.id(), bind_group));
bind_group
}
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(upscaling_target.0) else {
return Ok(());
};
let pass_descriptor = RenderPassDescriptor {
label: Some("upscaling"),
color_attachments: &[Some(
target.out_texture_color_attachment(converted_clear_color),
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
let pass_span = diagnostics.pass_span(&mut render_pass, "upscaling");
if let Some(camera) = camera
&& let Some(viewport) = &camera.viewport
{
let size = viewport.physical_size;
let position = viewport.physical_position;
render_pass.set_scissor_rect(position.x, position.y, size.x, size.y);
}
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1);
pass_span.end(&mut render_pass);
Ok(())
}
}
+1 -3
View File
@@ -143,9 +143,8 @@ fn sample_hzb_row(sx: vec4<u32>, sy: u32, mip: i32) -> f32 {
return min(min(a, b), min(c, d)); 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<f32>) -> bool { fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2<f32>) -> bool {
let hzb_size = ceil(screen * 0.5); let hzb_size = vec2<f32>(textureDimensions(depth_pyramid).xy);
let aabb_min = aabb.min.xy * hzb_size; let aabb_min = aabb.min.xy * hzb_size;
let aabb_max = aabb.max.xy * hzb_size; let aabb_max = aabb.max.xy * hzb_size;
@@ -157,7 +156,6 @@ fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2<f32>) -> bool {
// note: add 1 before max because the unsigned overflow behavior is intentional // note: add 1 before max because the unsigned overflow behavior is intentional
// it wraps around firstLeadingBit(0) = ~0 to 0 // it wraps around firstLeadingBit(0) = ~0 to 0
// TODO: we actually sample a 4x4 block, so ideally this would be `max(..., 3u) - 3u`. // 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; var mip = max(firstLeadingBit(max_size) + 1u, 2u) - 2u;
if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) { if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) {
+8 -6
View File
@@ -1305,12 +1305,12 @@ impl FromWorld for PreprocessPipelines {
let direct_bind_group_layout_entries = preprocess_direct_bind_group_layout_entries(); 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_frustum_culling_bind_group_layout_entries = gpu_culling_bind_group_layout_entries();
let gpu_early_occlusion_culling_bind_group_layout_entries = let gpu_early_occlusion_culling_bind_group_layout_entries =
gpu_occlusion_culling_bind_group_layout_entries().extend_with_indices((( gpu_occlusion_culling_bind_group_layout_entries(false).extend_with_indices(((
11, 11,
storage_buffer::<PreprocessWorkItem>(/*has_dynamic_offset=*/ false), storage_buffer::<PreprocessWorkItem>(/*has_dynamic_offset=*/ false),
),)); ),));
let gpu_late_occlusion_culling_bind_group_layout_entries = let gpu_late_occlusion_culling_bind_group_layout_entries =
gpu_occlusion_culling_bind_group_layout_entries(); gpu_occlusion_culling_bind_group_layout_entries(true);
let reset_indirect_batch_sets_bind_group_layout_entries = let reset_indirect_batch_sets_bind_group_layout_entries =
DynamicBindGroupLayoutEntries::sequential( DynamicBindGroupLayoutEntries::sequential(
@@ -1488,7 +1488,7 @@ fn gpu_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries {
)) ))
} }
fn gpu_occlusion_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { fn gpu_occlusion_culling_bind_group_layout_entries(read_only: bool) -> DynamicBindGroupLayoutEntries {
gpu_culling_bind_group_layout_entries().extend_with_indices(( gpu_culling_bind_group_layout_entries().extend_with_indices((
( (
2, 2,
@@ -1500,9 +1500,11 @@ fn gpu_occlusion_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEn
), ),
( (
12, 12,
storage_buffer::<LatePreprocessWorkItemIndirectParameters>( if read_only {
/*has_dynamic_offset=*/ false, storage_buffer_read_only::<LatePreprocessWorkItemIndirectParameters>(false)
), } else {
storage_buffer::<LatePreprocessWorkItemIndirectParameters>(false)
},
), ),
)) ))
} }
+15 -2
View File
@@ -57,13 +57,21 @@ struct LatePreprocessWorkItemIndirectParameters {
// The number of workgroups we're going to dispatch. // The number of workgroups we're going to dispatch.
// //
// This value should always be equal to `ceil(work_item_count / 64)`. // This value should always be equal to `ceil(work_item_count / 64)`.
#ifdef EARLY_PHASE
dispatch_x: atomic<u32>, dispatch_x: atomic<u32>,
#else
dispatch_x: u32,
#endif
// The number of workgroups in the Y direction; always 1. // The number of workgroups in the Y direction; always 1.
dispatch_y: u32, dispatch_y: u32,
// The number of workgroups in the Z direction; always 1. // The number of workgroups in the Z direction; always 1.
dispatch_z: u32, dispatch_z: u32,
// The precise number of work items. // The precise number of work items.
#ifdef EARLY_PHASE
work_item_count: atomic<u32>, work_item_count: atomic<u32>,
#else
work_item_count: u32,
#endif
// Padding. // Padding.
// //
// This isn't the usual structure padding; it's needed because some hardware // This isn't the usual structure padding; it's needed because some hardware
@@ -114,7 +122,12 @@ struct PushConstants {
array<PreprocessWorkItem>; array<PreprocessWorkItem>;
#endif // EARLY_PHASE #endif // EARLY_PHASE
#ifdef EARLY_PHASE
@group(0) @binding(12) var<storage, read_write> late_preprocess_work_item_indirect_parameters: @group(0) @binding(12) var<storage, read_write> late_preprocess_work_item_indirect_parameters:
#else
// This buffer is also the indirect dispatch source; late phase must only read it.
@group(0) @binding(12) var<storage, read> late_preprocess_work_item_indirect_parameters:
#endif
array<LatePreprocessWorkItemIndirectParameters>; array<LatePreprocessWorkItemIndirectParameters>;
var<push_constant> push_constants: PushConstants; var<push_constant> push_constants: PushConstants;
@@ -163,8 +176,8 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3<u32>) {
let instance_index = global_invocation_id.x; let instance_index = global_invocation_id.x;
#ifdef LATE_PHASE #ifdef LATE_PHASE
if (instance_index >= atomicLoad(&late_preprocess_work_item_indirect_parameters[ if (instance_index >= late_preprocess_work_item_indirect_parameters[
push_constants.late_preprocess_work_item_indirect_offset].work_item_count)) { push_constants.late_preprocess_work_item_indirect_offset].work_item_count) {
return; return;
} }
#else // LATE_PHASE #else // LATE_PHASE