mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
Reuse texture when resolving multiple passes (#3552)
# Objective Fixes https://github.com/bevyengine/bevy/issues/3499 ## Solution Uses a `HashMap` from `RenderTarget` to sampled textures when preparing `ViewTarget`s to ensure that two passes with the same render target get sampled to the same texture. This builds on and depends on https://github.com/bevyengine/bevy/pull/3412, so this will be a draft PR until #3412 is merged. All changes for this PR are in the last commit.
This commit is contained in:
parent
193e8c4ada
commit
5a297d7903
7 changed files with 280 additions and 37 deletions
|
@ -216,6 +216,10 @@ path = "examples/3d/texture.rs"
|
|||
name = "render_to_texture"
|
||||
path = "examples/3d/render_to_texture.rs"
|
||||
|
||||
[[example]]
|
||||
name = "two_passes"
|
||||
path = "examples/3d/two_passes.rs"
|
||||
|
||||
[[example]]
|
||||
name = "update_gltf_scene"
|
||||
path = "examples/3d/update_gltf_scene.rs"
|
||||
|
|
|
@ -23,7 +23,7 @@ use bevy_app::{App, Plugin};
|
|||
use bevy_core::FloatOrd;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_render::{
|
||||
camera::{ActiveCamera, Camera2d, Camera3d, RenderTarget},
|
||||
camera::{ActiveCamera, Camera2d, Camera3d, ExtractedCamera, RenderTarget},
|
||||
color::Color,
|
||||
render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType},
|
||||
render_phase::{
|
||||
|
@ -390,7 +390,7 @@ pub fn prepare_core_views_system(
|
|||
msaa: Res<Msaa>,
|
||||
render_device: Res<RenderDevice>,
|
||||
views_3d: Query<
|
||||
(Entity, &ExtractedView),
|
||||
(Entity, &ExtractedView, Option<&ExtractedCamera>),
|
||||
(
|
||||
With<RenderPhase<Opaque3d>>,
|
||||
With<RenderPhase<AlphaMask3d>>,
|
||||
|
@ -398,8 +398,10 @@ pub fn prepare_core_views_system(
|
|||
),
|
||||
>,
|
||||
) {
|
||||
for (entity, view) in views_3d.iter() {
|
||||
let cached_texture = texture_cache.get(
|
||||
let mut textures = HashMap::default();
|
||||
for (entity, view, camera) in views_3d.iter() {
|
||||
let mut get_cached_texture = || {
|
||||
texture_cache.get(
|
||||
&render_device,
|
||||
TextureDescriptor {
|
||||
label: Some("view_depth_texture"),
|
||||
|
@ -415,7 +417,16 @@ pub fn prepare_core_views_system(
|
|||
* bit depth for better performance */
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
},
|
||||
);
|
||||
)
|
||||
};
|
||||
let cached_texture = if let Some(camera) = camera {
|
||||
textures
|
||||
.entry(camera.target.clone())
|
||||
.or_insert_with(get_cached_texture)
|
||||
.clone()
|
||||
} else {
|
||||
get_cached_texture()
|
||||
};
|
||||
commands.entity(entity).insert(ViewDepthTexture {
|
||||
texture: cached_texture.texture,
|
||||
view: cached_texture.default_view,
|
||||
|
|
|
@ -142,12 +142,15 @@ impl Node for MainPass3dNode {
|
|||
})],
|
||||
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
|
||||
view: &depth.view,
|
||||
// NOTE: For the transparent pass we load the depth buffer but do not write to it.
|
||||
// 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_ops: Some(Operations {
|
||||
load: LoadOp::Load,
|
||||
store: false,
|
||||
store: true,
|
||||
}),
|
||||
stencil_ops: None,
|
||||
}),
|
||||
|
|
|
@ -18,6 +18,7 @@ struct CachedTextureMeta {
|
|||
/// A cached GPU [`Texture`] with corresponding [`TextureView`].
|
||||
/// This is useful for textures that are created repeatedly (each frame) in the rendering process
|
||||
/// to reduce the amount of GPU memory allocations.
|
||||
#[derive(Clone)]
|
||||
pub struct CachedTexture {
|
||||
pub texture: Texture,
|
||||
pub default_view: TextureView,
|
||||
|
|
|
@ -21,6 +21,7 @@ use bevy_app::{App, Plugin};
|
|||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{Mat4, Vec3};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::HashMap;
|
||||
|
||||
pub struct ViewPlugin;
|
||||
|
||||
|
@ -181,11 +182,15 @@ fn prepare_view_targets(
|
|||
mut texture_cache: ResMut<TextureCache>,
|
||||
cameras: Query<(Entity, &ExtractedCamera)>,
|
||||
) {
|
||||
let mut sampled_textures = HashMap::default();
|
||||
for (entity, camera) in cameras.iter() {
|
||||
if let Some(size) = camera.physical_size {
|
||||
if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) {
|
||||
let sampled_target = if msaa.samples > 1 {
|
||||
let sampled_texture = texture_cache.get(
|
||||
let sampled_texture = sampled_textures
|
||||
.entry(camera.target.clone())
|
||||
.or_insert_with(|| {
|
||||
texture_cache.get(
|
||||
&render_device,
|
||||
TextureDescriptor {
|
||||
label: Some("sampled_color_attachment_texture"),
|
||||
|
@ -200,7 +205,8 @@ fn prepare_view_targets(
|
|||
format: TextureFormat::bevy_default(),
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
},
|
||||
);
|
||||
)
|
||||
});
|
||||
Some(sampled_texture.default_view.clone())
|
||||
} else {
|
||||
None
|
||||
|
|
217
examples/3d/two_passes.rs
Normal file
217
examples/3d/two_passes.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
use bevy::{
|
||||
core_pipeline::{draw_3d_graph, node, AlphaMask3d, Opaque3d, Transparent3d},
|
||||
prelude::*,
|
||||
render::{
|
||||
camera::{ActiveCamera, Camera, CameraTypePlugin, RenderTarget},
|
||||
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotValue},
|
||||
render_phase::RenderPhase,
|
||||
renderer::RenderContext,
|
||||
view::RenderLayers,
|
||||
RenderApp, RenderStage,
|
||||
},
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
// The name of the final node of the first pass.
|
||||
pub const FIRST_PASS_DRIVER: &str = "first_pass_driver";
|
||||
|
||||
// Marks the camera that determines the view rendered in the first pass.
|
||||
#[derive(Component, Default)]
|
||||
struct FirstPassCamera;
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
app.insert_resource(Msaa { samples: 4 })
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugin(CameraTypePlugin::<FirstPassCamera>::default())
|
||||
.add_startup_system(setup)
|
||||
.add_system(cube_rotator_system)
|
||||
.add_system(rotator_system)
|
||||
.add_system(toggle_msaa);
|
||||
|
||||
let render_app = app.sub_app_mut(RenderApp);
|
||||
let driver = FirstPassCameraDriver::new(&mut render_app.world);
|
||||
|
||||
// This will add 3D render phases for the new camera.
|
||||
render_app.add_system_to_stage(RenderStage::Extract, extract_first_pass_camera_phases);
|
||||
|
||||
let mut graph = render_app.world.get_resource_mut::<RenderGraph>().unwrap();
|
||||
|
||||
// Add a node for the first pass.
|
||||
graph.add_node(FIRST_PASS_DRIVER, driver);
|
||||
|
||||
// The first pass's dependencies include those of the main pass.
|
||||
graph
|
||||
.add_node_edge(node::MAIN_PASS_DEPENDENCIES, FIRST_PASS_DRIVER)
|
||||
.unwrap();
|
||||
|
||||
// Insert the first pass node: CLEAR_PASS_DRIVER -> FIRST_PASS_DRIVER -> MAIN_PASS_DRIVER
|
||||
graph
|
||||
.add_node_edge(node::CLEAR_PASS_DRIVER, FIRST_PASS_DRIVER)
|
||||
.unwrap();
|
||||
graph
|
||||
.add_node_edge(FIRST_PASS_DRIVER, node::MAIN_PASS_DRIVER)
|
||||
.unwrap();
|
||||
app.run();
|
||||
}
|
||||
|
||||
// Add 3D render phases for FirstPassCamera.
|
||||
fn extract_first_pass_camera_phases(
|
||||
mut commands: Commands,
|
||||
active: Res<ActiveCamera<FirstPassCamera>>,
|
||||
) {
|
||||
if let Some(entity) = active.get() {
|
||||
commands.get_or_spawn(entity).insert_bundle((
|
||||
RenderPhase::<Opaque3d>::default(),
|
||||
RenderPhase::<AlphaMask3d>::default(),
|
||||
RenderPhase::<Transparent3d>::default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// A node for the first pass camera that runs draw_3d_graph with this camera.
|
||||
struct FirstPassCameraDriver {
|
||||
query: QueryState<Entity, With<FirstPassCamera>>,
|
||||
}
|
||||
|
||||
impl FirstPassCameraDriver {
|
||||
pub fn new(render_world: &mut World) -> Self {
|
||||
Self {
|
||||
query: QueryState::new(render_world),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for FirstPassCameraDriver {
|
||||
fn update(&mut self, world: &mut World) {
|
||||
self.query.update_archetypes(world);
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
graph: &mut RenderGraphContext,
|
||||
_render_context: &mut RenderContext,
|
||||
world: &World,
|
||||
) -> Result<(), NodeRunError> {
|
||||
for camera in self.query.iter_manual(world) {
|
||||
graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(camera)])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Marks the first pass cube.
|
||||
#[derive(Component)]
|
||||
struct FirstPassCube;
|
||||
|
||||
// Marks the main pass cube.
|
||||
#[derive(Component)]
|
||||
struct MainPassCube;
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 }));
|
||||
let cube_material_handle = materials.add(StandardMaterial {
|
||||
base_color: Color::GREEN,
|
||||
reflectance: 0.02,
|
||||
unlit: false,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let split = 2.0;
|
||||
|
||||
// This specifies the layer used for the first pass, which will be attached to the first pass camera and cube.
|
||||
let first_pass_layer = RenderLayers::layer(1);
|
||||
|
||||
// The first pass cube.
|
||||
commands
|
||||
.spawn_bundle(PbrBundle {
|
||||
mesh: cube_handle,
|
||||
material: cube_material_handle,
|
||||
transform: Transform::from_translation(Vec3::new(-split, 0.0, 1.0)),
|
||||
..Default::default()
|
||||
})
|
||||
.insert(FirstPassCube)
|
||||
.insert(first_pass_layer);
|
||||
|
||||
// Light
|
||||
// NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462
|
||||
commands.spawn_bundle(PointLightBundle {
|
||||
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// First pass camera
|
||||
commands
|
||||
.spawn_bundle(PerspectiveCameraBundle::<FirstPassCamera> {
|
||||
camera: Camera {
|
||||
target: RenderTarget::Window(WindowId::primary()),
|
||||
..Default::default()
|
||||
},
|
||||
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0))
|
||||
.looking_at(Vec3::default(), Vec3::Y),
|
||||
..PerspectiveCameraBundle::new()
|
||||
})
|
||||
.insert(first_pass_layer);
|
||||
|
||||
let cube_size = 4.0;
|
||||
let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size)));
|
||||
|
||||
let material_handle = materials.add(StandardMaterial {
|
||||
base_color: Color::RED,
|
||||
reflectance: 0.02,
|
||||
unlit: false,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Main pass cube.
|
||||
commands
|
||||
.spawn_bundle(PbrBundle {
|
||||
mesh: cube_handle,
|
||||
material: material_handle,
|
||||
transform: Transform {
|
||||
translation: Vec3::new(split, 0.0, -4.5),
|
||||
rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.insert(MainPassCube);
|
||||
|
||||
// The main pass camera.
|
||||
commands.spawn_bundle(PerspectiveCameraBundle {
|
||||
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0))
|
||||
.looking_at(Vec3::default(), Vec3::Y),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// Rotates the inner cube (first pass)
|
||||
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<FirstPassCube>>) {
|
||||
for mut transform in query.iter_mut() {
|
||||
transform.rotation *= Quat::from_rotation_x(1.5 * time.delta_seconds());
|
||||
transform.rotation *= Quat::from_rotation_z(1.3 * time.delta_seconds());
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotates the outer cube (main pass)
|
||||
fn cube_rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<MainPassCube>>) {
|
||||
for mut transform in query.iter_mut() {
|
||||
transform.rotation *= Quat::from_rotation_x(1.0 * time.delta_seconds());
|
||||
transform.rotation *= Quat::from_rotation_y(0.7 * time.delta_seconds());
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_msaa(input: Res<Input<KeyCode>>, mut msaa: ResMut<Msaa>) {
|
||||
if input.just_pressed(KeyCode::M) {
|
||||
if msaa.samples == 4 {
|
||||
info!("Not using MSAA");
|
||||
msaa.samples = 1;
|
||||
} else {
|
||||
info!("Using 4x MSAA");
|
||||
msaa.samples = 4;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,6 +109,7 @@ Example | File | Description
|
|||
`parenting` | [`3d/parenting.rs`](./3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
|
||||
`pbr` | [`3d/pbr.rs`](./3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
|
||||
`render_to_texture` | [`3d/render_to_texture.rs`](./3d/render_to_texture.rs) | Shows how to render to a texture, useful for mirrors, UI, or exporting images
|
||||
`two_passes` | [`3d/two_passes.rs`](./3d/two_passes.rs) | Shows how to render multiple passes to the same window, useful for rendering different views or drawing an object on top regardless of depth
|
||||
`shadow_caster_receiver` | [`3d/shadow_caster_receiver.rs`](./3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
|
||||
`shadow_biases` | [`3d/shadow_biases.rs`](./3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
|
||||
`spherical_area_lights` | [`3d/spherical_area_lights.rs`](./3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior.
|
||||
|
|
Loading…
Reference in a new issue