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:
Dusty DeWeese 2022-04-12 19:27:30 +00:00
parent 193e8c4ada
commit 5a297d7903
7 changed files with 280 additions and 37 deletions

View file

@ -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"

View file

@ -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,

View file

@ -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,
}),

View file

@ -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,

View file

@ -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
View 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;
}
}
}

View file

@ -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.