mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add runtime occlusion culling with conservative depth and valid GPU bindings
This commit is contained in:
1 parent
d31f81f95e
commit
a21f78aa52
57 files changed
+12066
-24
No files matched your search
Generated
-2
@@ -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",
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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}"#)
|
||||||
|
|||||||
@@ -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.
|
||||||
Vendored
+19
@@ -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
File diff suppressed because it is too large.
Load diff
Vendored
+160
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
# Bevy Core Pipeline
|
||||||
|
|
||||||
|
[](https://github.com/bevyengine/bevy#license)
|
||||||
|
[](https://crates.io/crates/bevy_core_pipeline)
|
||||||
|
[](https://crates.io/crates/bevy_core_pipeline)
|
||||||
|
[](https://docs.rs/bevy_core_pipeline/latest/bevy_core_pipeline/)
|
||||||
|
[](https://discord.gg/bevy)
|
||||||
@@ -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
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+1251
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(©_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
@@ -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
@@ -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(())
|
||||||
|
}
|
||||||
+403
@@ -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())
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+57
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
Binary file not shown.
+456
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user