Skip to content

Commit 5a297d7

Browse files
committed
Reuse texture when resolving multiple passes (bevyengine#3552)
# Objective Fixes bevyengine#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 bevyengine#3412, so this will be a draft PR until bevyengine#3412 is merged. All changes for this PR are in the last commit.
1 parent 193e8c4 commit 5a297d7

File tree

7 files changed

+280
-37
lines changed

7 files changed

+280
-37
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ path = "examples/3d/texture.rs"
216216
name = "render_to_texture"
217217
path = "examples/3d/render_to_texture.rs"
218218

219+
[[example]]
220+
name = "two_passes"
221+
path = "examples/3d/two_passes.rs"
222+
219223
[[example]]
220224
name = "update_gltf_scene"
221225
path = "examples/3d/update_gltf_scene.rs"

crates/bevy_core_pipeline/src/lib.rs

+30-19
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use bevy_app::{App, Plugin};
2323
use bevy_core::FloatOrd;
2424
use bevy_ecs::prelude::*;
2525
use bevy_render::{
26-
camera::{ActiveCamera, Camera2d, Camera3d, RenderTarget},
26+
camera::{ActiveCamera, Camera2d, Camera3d, ExtractedCamera, RenderTarget},
2727
color::Color,
2828
render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType},
2929
render_phase::{
@@ -390,32 +390,43 @@ pub fn prepare_core_views_system(
390390
msaa: Res<Msaa>,
391391
render_device: Res<RenderDevice>,
392392
views_3d: Query<
393-
(Entity, &ExtractedView),
393+
(Entity, &ExtractedView, Option<&ExtractedCamera>),
394394
(
395395
With<RenderPhase<Opaque3d>>,
396396
With<RenderPhase<AlphaMask3d>>,
397397
With<RenderPhase<Transparent3d>>,
398398
),
399399
>,
400400
) {
401-
for (entity, view) in views_3d.iter() {
402-
let cached_texture = texture_cache.get(
403-
&render_device,
404-
TextureDescriptor {
405-
label: Some("view_depth_texture"),
406-
size: Extent3d {
407-
depth_or_array_layers: 1,
408-
width: view.width as u32,
409-
height: view.height as u32,
401+
let mut textures = HashMap::default();
402+
for (entity, view, camera) in views_3d.iter() {
403+
let mut get_cached_texture = || {
404+
texture_cache.get(
405+
&render_device,
406+
TextureDescriptor {
407+
label: Some("view_depth_texture"),
408+
size: Extent3d {
409+
depth_or_array_layers: 1,
410+
width: view.width as u32,
411+
height: view.height as u32,
412+
},
413+
mip_level_count: 1,
414+
sample_count: msaa.samples,
415+
dimension: TextureDimension::D2,
416+
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
417+
* bit depth for better performance */
418+
usage: TextureUsages::RENDER_ATTACHMENT,
410419
},
411-
mip_level_count: 1,
412-
sample_count: msaa.samples,
413-
dimension: TextureDimension::D2,
414-
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
415-
* bit depth for better performance */
416-
usage: TextureUsages::RENDER_ATTACHMENT,
417-
},
418-
);
420+
)
421+
};
422+
let cached_texture = if let Some(camera) = camera {
423+
textures
424+
.entry(camera.target.clone())
425+
.or_insert_with(get_cached_texture)
426+
.clone()
427+
} else {
428+
get_cached_texture()
429+
};
419430
commands.entity(entity).insert(ViewDepthTexture {
420431
texture: cached_texture.texture,
421432
view: cached_texture.default_view,

crates/bevy_core_pipeline/src/main_pass_3d.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,15 @@ impl Node for MainPass3dNode {
142142
})],
143143
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
144144
view: &depth.view,
145-
// NOTE: For the transparent pass we load the depth buffer but do not write to it.
145+
// NOTE: For the transparent pass we load the depth buffer. There should be no
146+
// need to write to it, but store is set to `true` as a workaround for issue #3776,
147+
// https://github.com/bevyengine/bevy/issues/3776
148+
// so that wgpu does not clear the depth buffer.
146149
// As the opaque and alpha mask passes run first, opaque meshes can occlude
147150
// transparent ones.
148151
depth_ops: Some(Operations {
149152
load: LoadOp::Load,
150-
store: false,
153+
store: true,
151154
}),
152155
stencil_ops: None,
153156
}),

crates/bevy_render/src/texture/texture_cache.rs

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct CachedTextureMeta {
1818
/// A cached GPU [`Texture`] with corresponding [`TextureView`].
1919
/// This is useful for textures that are created repeatedly (each frame) in the rendering process
2020
/// to reduce the amount of GPU memory allocations.
21+
#[derive(Clone)]
2122
pub struct CachedTexture {
2223
pub texture: Texture,
2324
pub default_view: TextureView,

crates/bevy_render/src/view/mod.rs

+22-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use bevy_app::{App, Plugin};
2121
use bevy_ecs::prelude::*;
2222
use bevy_math::{Mat4, Vec3};
2323
use bevy_transform::components::GlobalTransform;
24+
use bevy_utils::HashMap;
2425

2526
pub struct ViewPlugin;
2627

@@ -181,26 +182,31 @@ fn prepare_view_targets(
181182
mut texture_cache: ResMut<TextureCache>,
182183
cameras: Query<(Entity, &ExtractedCamera)>,
183184
) {
185+
let mut sampled_textures = HashMap::default();
184186
for (entity, camera) in cameras.iter() {
185187
if let Some(size) = camera.physical_size {
186188
if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) {
187189
let sampled_target = if msaa.samples > 1 {
188-
let sampled_texture = texture_cache.get(
189-
&render_device,
190-
TextureDescriptor {
191-
label: Some("sampled_color_attachment_texture"),
192-
size: Extent3d {
193-
width: size.x,
194-
height: size.y,
195-
depth_or_array_layers: 1,
196-
},
197-
mip_level_count: 1,
198-
sample_count: msaa.samples,
199-
dimension: TextureDimension::D2,
200-
format: TextureFormat::bevy_default(),
201-
usage: TextureUsages::RENDER_ATTACHMENT,
202-
},
203-
);
190+
let sampled_texture = sampled_textures
191+
.entry(camera.target.clone())
192+
.or_insert_with(|| {
193+
texture_cache.get(
194+
&render_device,
195+
TextureDescriptor {
196+
label: Some("sampled_color_attachment_texture"),
197+
size: Extent3d {
198+
width: size.x,
199+
height: size.y,
200+
depth_or_array_layers: 1,
201+
},
202+
mip_level_count: 1,
203+
sample_count: msaa.samples,
204+
dimension: TextureDimension::D2,
205+
format: TextureFormat::bevy_default(),
206+
usage: TextureUsages::RENDER_ATTACHMENT,
207+
},
208+
)
209+
});
204210
Some(sampled_texture.default_view.clone())
205211
} else {
206212
None

examples/3d/two_passes.rs

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
use bevy::{
2+
core_pipeline::{draw_3d_graph, node, AlphaMask3d, Opaque3d, Transparent3d},
3+
prelude::*,
4+
render::{
5+
camera::{ActiveCamera, Camera, CameraTypePlugin, RenderTarget},
6+
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotValue},
7+
render_phase::RenderPhase,
8+
renderer::RenderContext,
9+
view::RenderLayers,
10+
RenderApp, RenderStage,
11+
},
12+
window::WindowId,
13+
};
14+
15+
// The name of the final node of the first pass.
16+
pub const FIRST_PASS_DRIVER: &str = "first_pass_driver";
17+
18+
// Marks the camera that determines the view rendered in the first pass.
19+
#[derive(Component, Default)]
20+
struct FirstPassCamera;
21+
22+
fn main() {
23+
let mut app = App::new();
24+
app.insert_resource(Msaa { samples: 4 })
25+
.add_plugins(DefaultPlugins)
26+
.add_plugin(CameraTypePlugin::<FirstPassCamera>::default())
27+
.add_startup_system(setup)
28+
.add_system(cube_rotator_system)
29+
.add_system(rotator_system)
30+
.add_system(toggle_msaa);
31+
32+
let render_app = app.sub_app_mut(RenderApp);
33+
let driver = FirstPassCameraDriver::new(&mut render_app.world);
34+
35+
// This will add 3D render phases for the new camera.
36+
render_app.add_system_to_stage(RenderStage::Extract, extract_first_pass_camera_phases);
37+
38+
let mut graph = render_app.world.get_resource_mut::<RenderGraph>().unwrap();
39+
40+
// Add a node for the first pass.
41+
graph.add_node(FIRST_PASS_DRIVER, driver);
42+
43+
// The first pass's dependencies include those of the main pass.
44+
graph
45+
.add_node_edge(node::MAIN_PASS_DEPENDENCIES, FIRST_PASS_DRIVER)
46+
.unwrap();
47+
48+
// Insert the first pass node: CLEAR_PASS_DRIVER -> FIRST_PASS_DRIVER -> MAIN_PASS_DRIVER
49+
graph
50+
.add_node_edge(node::CLEAR_PASS_DRIVER, FIRST_PASS_DRIVER)
51+
.unwrap();
52+
graph
53+
.add_node_edge(FIRST_PASS_DRIVER, node::MAIN_PASS_DRIVER)
54+
.unwrap();
55+
app.run();
56+
}
57+
58+
// Add 3D render phases for FirstPassCamera.
59+
fn extract_first_pass_camera_phases(
60+
mut commands: Commands,
61+
active: Res<ActiveCamera<FirstPassCamera>>,
62+
) {
63+
if let Some(entity) = active.get() {
64+
commands.get_or_spawn(entity).insert_bundle((
65+
RenderPhase::<Opaque3d>::default(),
66+
RenderPhase::<AlphaMask3d>::default(),
67+
RenderPhase::<Transparent3d>::default(),
68+
));
69+
}
70+
}
71+
// A node for the first pass camera that runs draw_3d_graph with this camera.
72+
struct FirstPassCameraDriver {
73+
query: QueryState<Entity, With<FirstPassCamera>>,
74+
}
75+
76+
impl FirstPassCameraDriver {
77+
pub fn new(render_world: &mut World) -> Self {
78+
Self {
79+
query: QueryState::new(render_world),
80+
}
81+
}
82+
}
83+
84+
impl Node for FirstPassCameraDriver {
85+
fn update(&mut self, world: &mut World) {
86+
self.query.update_archetypes(world);
87+
}
88+
89+
fn run(
90+
&self,
91+
graph: &mut RenderGraphContext,
92+
_render_context: &mut RenderContext,
93+
world: &World,
94+
) -> Result<(), NodeRunError> {
95+
for camera in self.query.iter_manual(world) {
96+
graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(camera)])?;
97+
}
98+
Ok(())
99+
}
100+
}
101+
102+
// Marks the first pass cube.
103+
#[derive(Component)]
104+
struct FirstPassCube;
105+
106+
// Marks the main pass cube.
107+
#[derive(Component)]
108+
struct MainPassCube;
109+
110+
fn setup(
111+
mut commands: Commands,
112+
mut meshes: ResMut<Assets<Mesh>>,
113+
mut materials: ResMut<Assets<StandardMaterial>>,
114+
) {
115+
let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 }));
116+
let cube_material_handle = materials.add(StandardMaterial {
117+
base_color: Color::GREEN,
118+
reflectance: 0.02,
119+
unlit: false,
120+
..Default::default()
121+
});
122+
123+
let split = 2.0;
124+
125+
// This specifies the layer used for the first pass, which will be attached to the first pass camera and cube.
126+
let first_pass_layer = RenderLayers::layer(1);
127+
128+
// The first pass cube.
129+
commands
130+
.spawn_bundle(PbrBundle {
131+
mesh: cube_handle,
132+
material: cube_material_handle,
133+
transform: Transform::from_translation(Vec3::new(-split, 0.0, 1.0)),
134+
..Default::default()
135+
})
136+
.insert(FirstPassCube)
137+
.insert(first_pass_layer);
138+
139+
// Light
140+
// NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462
141+
commands.spawn_bundle(PointLightBundle {
142+
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
143+
..Default::default()
144+
});
145+
146+
// First pass camera
147+
commands
148+
.spawn_bundle(PerspectiveCameraBundle::<FirstPassCamera> {
149+
camera: Camera {
150+
target: RenderTarget::Window(WindowId::primary()),
151+
..Default::default()
152+
},
153+
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0))
154+
.looking_at(Vec3::default(), Vec3::Y),
155+
..PerspectiveCameraBundle::new()
156+
})
157+
.insert(first_pass_layer);
158+
159+
let cube_size = 4.0;
160+
let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size)));
161+
162+
let material_handle = materials.add(StandardMaterial {
163+
base_color: Color::RED,
164+
reflectance: 0.02,
165+
unlit: false,
166+
..Default::default()
167+
});
168+
169+
// Main pass cube.
170+
commands
171+
.spawn_bundle(PbrBundle {
172+
mesh: cube_handle,
173+
material: material_handle,
174+
transform: Transform {
175+
translation: Vec3::new(split, 0.0, -4.5),
176+
rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0),
177+
..Default::default()
178+
},
179+
..Default::default()
180+
})
181+
.insert(MainPassCube);
182+
183+
// The main pass camera.
184+
commands.spawn_bundle(PerspectiveCameraBundle {
185+
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0))
186+
.looking_at(Vec3::default(), Vec3::Y),
187+
..Default::default()
188+
});
189+
}
190+
191+
/// Rotates the inner cube (first pass)
192+
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<FirstPassCube>>) {
193+
for mut transform in query.iter_mut() {
194+
transform.rotation *= Quat::from_rotation_x(1.5 * time.delta_seconds());
195+
transform.rotation *= Quat::from_rotation_z(1.3 * time.delta_seconds());
196+
}
197+
}
198+
199+
/// Rotates the outer cube (main pass)
200+
fn cube_rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<MainPassCube>>) {
201+
for mut transform in query.iter_mut() {
202+
transform.rotation *= Quat::from_rotation_x(1.0 * time.delta_seconds());
203+
transform.rotation *= Quat::from_rotation_y(0.7 * time.delta_seconds());
204+
}
205+
}
206+
207+
fn toggle_msaa(input: Res<Input<KeyCode>>, mut msaa: ResMut<Msaa>) {
208+
if input.just_pressed(KeyCode::M) {
209+
if msaa.samples == 4 {
210+
info!("Not using MSAA");
211+
msaa.samples = 1;
212+
} else {
213+
info!("Using 4x MSAA");
214+
msaa.samples = 4;
215+
}
216+
}
217+
}

examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Example | File | Description
109109
`parenting` | [`3d/parenting.rs`](./3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
110110
`pbr` | [`3d/pbr.rs`](./3d/pbr.rs) | Demonstrates use of Physically Based Rendering (PBR) properties
111111
`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
112+
`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
112113
`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
113114
`shadow_biases` | [`3d/shadow_biases.rs`](./3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
114115
`spherical_area_lights` | [`3d/spherical_area_lights.rs`](./3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior.

0 commit comments

Comments
 (0)