Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability to render to cpu buffer or double buffer #4572

Closed
wants to merge 11 commits into from
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());
}
}