Skip to content
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ path = "examples/3d/pbr.rs"
name = "render_to_texture"
path = "examples/3d/render_to_texture.rs"

[[example]]
name = "render_to_double_buffer"
path = "examples/3d/render_to_double_buffer.rs"

[[example]]
name = "shadow_biases"
path = "examples/3d/shadow_biases.rs"
Expand Down
35 changes: 32 additions & 3 deletions crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ use crate::{
};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, RenderTarget},
prelude::Image,
render_asset::RenderAssets,
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{LoadOp, Operations, RenderPassDescriptor},
renderer::RenderContext,
render_resource::{
CommandEncoderDescriptor, Extent3d, LoadOp, Operations, RenderPassDescriptor,
},
renderer::{RenderContext, RenderQueue},
view::{ExtractedView, ViewTarget},
};
#[cfg(feature = "trace")]
Expand Down Expand Up @@ -94,6 +98,31 @@ impl Node for MainPass2dNode {
}
}

// TODO: should this live here or in a separate node?
// TODO: don't just have duplicated code between MainPass3dNode/MainPass2dNode
if let RenderTarget::BufferedImage(image_handle, buffer_image_handle) = &camera.target {
let gpu_images = world.get_resource::<RenderAssets<Image>>().unwrap();
let gpu_image = gpu_images.get(&image_handle).unwrap();

let mut encoder = render_context
.render_device
.create_command_encoder(&CommandEncoderDescriptor::default());

let target_image = gpu_images.get(&buffer_image_handle).unwrap();
encoder.copy_texture_to_texture(
gpu_image.texture.as_image_copy(),
target_image.texture.as_image_copy(),
Extent3d {
width: gpu_image.size.x as u32,
height: gpu_image.size.y as u32,
depth_or_array_layers: 1,
},
);

let render_queue = world.get_resource::<RenderQueue>().unwrap();
render_queue.submit(std::iter::once(encoder.finish()));
}

// 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(feature = "webgl")]
Expand Down
36 changes: 33 additions & 3 deletions crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ use crate::{
};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
camera::{ExtractedCamera, RenderTarget},
prelude::Image,
render_asset::RenderAssets,
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
renderer::RenderContext,
render_resource::{
CommandEncoderDescriptor, Extent3d, LoadOp, Operations, RenderPassDepthStencilAttachment,
RenderPassDescriptor,
},
renderer::{RenderContext, RenderQueue},
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
#[cfg(feature = "trace")]
Expand Down Expand Up @@ -194,6 +199,31 @@ impl Node for MainPass3dNode {
}
}

// TODO: should this live here or in a separate node?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate node imo.

// TODO: don't just have duplicated code between MainPass3dNode/MainPass2dNode
if let RenderTarget::BufferedImage(image_handle, buffer_image_handle) = &camera.target {
let gpu_images = world.get_resource::<RenderAssets<Image>>().unwrap();
let gpu_image = gpu_images.get(&image_handle).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe source and destination images? i.e. src_image, dst_image


let mut encoder = render_context
.render_device
.create_command_encoder(&CommandEncoderDescriptor::default());

let target_image = gpu_images.get(&buffer_image_handle).unwrap();
encoder.copy_texture_to_texture(
gpu_image.texture.as_image_copy(),
target_image.texture.as_image_copy(),
Extent3d {
width: gpu_image.size.x as u32,
height: gpu_image.size.y as u32,
depth_or_array_layers: 1,
},
);

let render_queue = world.get_resource::<RenderQueue>().unwrap();
render_queue.submit(std::iter::once(encoder.finish()));
}

// 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(feature = "webgl")]
Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ pub enum RenderTarget {
Window(WindowId),
/// Image to which the camera's view is rendered.
Image(Handle<Image>),
/// Buffered Image to which the camera's view is rendered.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this description could be clearer. The first image is used for rendering into and is then copied into the second image, right?

BufferedImage(Handle<Image>, Handle<Image>),
}

impl Default for RenderTarget {
Expand All @@ -268,6 +270,9 @@ impl RenderTarget {
RenderTarget::Image(image_handle) => {
images.get(image_handle).map(|image| &image.texture_view)
}
RenderTarget::BufferedImage(image_handle, _buffer_image_handle) => {
images.get(image_handle).map(|image| &image.texture_view)
}
}
}

Expand All @@ -292,6 +297,14 @@ impl RenderTarget {
scale_factor: 1.0,
}
}
RenderTarget::BufferedImage(image_handle, _buffered_image_handle) => {
let image = images.get(image_handle)?;
let Extent3d { width, height, .. } = image.texture_descriptor.size;
RenderTargetInfo {
physical_size: UVec2::new(width, height),
scale_factor: 1.0,
}
}
})
}
// Check if this render target is contained in the given changed windows or images.
Expand All @@ -303,6 +316,10 @@ impl RenderTarget {
match self {
RenderTarget::Window(window_id) => changed_window_ids.contains(window_id),
RenderTarget::Image(image_handle) => changed_image_handles.contains(&image_handle),
RenderTarget::BufferedImage(image_handle, buffered_image_handle) => {
changed_image_handles.contains(&image_handle)
|| changed_image_handles.contains(&buffered_image_handle)
}
}
}
}
Expand Down
166 changes: 166 additions & 0 deletions examples/3d/render_to_double_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! Shows how to render to a double buffered texture that can then be used on the same render layer.
use bevy::prelude::*;
use bevy::render::camera::{CameraPlugin, RenderTarget};

use bevy::render::render_resource::{
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
};

#[derive(Component, Default)]
pub struct CaptureCamera;

// Marks the first pass cube (rendered to a texture.)
#[derive(Component)]
struct FirstPassCube;

// Marks the main pass cube, to which the texture is applied.
#[derive(Component)]
struct MainPassCube;

fn main() {
App::new()
.insert_resource(Msaa { samples: 4 }) // Use 4x MSAA
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.0 / 5.0f32,
})
.add_plugins(DefaultPlugins)
.add_plugin(CameraPlugin::default())
.add_startup_system(setup)
.add_system(cube_rotator_system)
.add_system(rotator_system)
.run();
}

fn setup(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let size = Extent3d {
width: 512,
height: 512,
..Default::default()
};

// This is the texture that will be rendered to.
let mut image = Image {
texture_descriptor: TextureDescriptor {
label: None,
size,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8UnormSrgb,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::COPY_SRC
| TextureUsages::RENDER_ATTACHMENT,
Comment on lines +57 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one only needs RENDER_ATTACHMENT | COPY_SRC.

},
..Default::default()
};
image.resize(size);
let gpu_image = images.add(image);

// This is the buffered texture that copied to.
let mut buffered_image = Image {
texture_descriptor: TextureDescriptor {
label: None,
size,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8UnormSrgb,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::COPY_SRC
| TextureUsages::RENDER_ATTACHMENT,
Comment on lines +76 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one only needs COPY_DST | TEXTURE_BINDING.

},
..Default::default()
};
buffered_image.resize(size);
let gpu_buffered_image = images.add(buffered_image);

let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 0.25 }));
let cube_material_handle = materials.add(StandardMaterial {
base_color: Color::rgb(0.8, 0.7, 0.6),
reflectance: 0.02,
unlit: false,
..default()
});

// The cube that will be rendered to the texture.
commands
.spawn_bundle(PbrBundle {
mesh: cube_handle,
material: cube_material_handle,
transform: Transform::from_translation(Vec3::new(0.0, 0.25, 0.0)),
..default()
})
.insert(FirstPassCube);

commands.spawn_bundle(PointLightBundle {
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)),
..default()
});

let cube_size = 0.25;
let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size)));

// This material has the texture that has been rendered.
let material_handle = materials.add(StandardMaterial {
base_color_texture: Some(gpu_buffered_image.clone()),
reflectance: 0.02,
unlit: false,
..default()
});
// Main pass cube, with material containing the rendered first pass texture.
commands
.spawn_bundle(PbrBundle {
mesh: cube_handle,
material: material_handle,
transform: Transform {
translation: Vec3::new(0.0, 0.5, 0.0),
rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0),
..default()
},
..default()
})
.insert(MainPassCube);

commands
.spawn_bundle(Camera3dBundle {
transform: Transform::from_xyz(0.7, 0.7, 1.0)
.looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
..default()
})
.with_children(|parent| {
let render_target =
RenderTarget::BufferedImage(gpu_image.clone(), gpu_buffered_image.clone());
parent.spawn_bundle(Camera3dBundle {
camera: Camera {
target: render_target,
..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());
}
}