From 0cd2e89242e2a095adbc9f2c186563b66abd1441 Mon Sep 17 00:00:00 2001 From: DGriffin91 Date: Sat, 23 Apr 2022 16:19:03 -0700 Subject: [PATCH 01/11] bevy_camera: FrameCapturePlugin --- Cargo.toml | 7 + crates/bevy_camera/Cargo.toml | 22 +++ crates/bevy_camera/src/frame_capture.rs | 194 ++++++++++++++++++++++++ crates/bevy_camera/src/lib.rs | 3 + crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/lib.rs | 6 + examples/3d/frame_capture.rs | 139 +++++++++++++++++ 7 files changed, 372 insertions(+) create mode 100644 crates/bevy_camera/Cargo.toml create mode 100644 crates/bevy_camera/src/frame_capture.rs create mode 100644 crates/bevy_camera/src/lib.rs create mode 100644 examples/3d/frame_capture.rs diff --git a/Cargo.toml b/Cargo.toml index 62be72f770747..555e7a9e3fe9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ default = [ "bevy_audio", "bevy_gilrs", "bevy_winit", + "bevy_camera", "render", "png", "hdr", @@ -56,6 +57,7 @@ bevy_sprite = ["bevy_internal/bevy_sprite"] bevy_text = ["bevy_internal/bevy_text"] bevy_ui = ["bevy_internal/bevy_ui"] bevy_winit = ["bevy_internal/bevy_winit"] +bevy_camera = ["bevy_internal/bevy_camera"] # Tracing features trace_chrome = ["trace", "bevy_internal/trace_chrome"] @@ -121,6 +123,7 @@ bytemuck = "1.7" # Needed to poll Task examples futures-lite = "1.11.3" crossbeam-channel = "0.5.0" +image = { version = "0.23.12", default-features = false } [[example]] name = "hello_world" @@ -184,6 +187,10 @@ path = "examples/3d/3d_scene.rs" name = "3d_shapes" path = "examples/3d/shapes.rs" +[[example]] +name = "frame_capture" +path = "examples/3d/frame_capture.rs" + [[example]] name = "lighting" path = "examples/3d/lighting.rs" diff --git a/crates/bevy_camera/Cargo.toml b/crates/bevy_camera/Cargo.toml new file mode 100644 index 0000000000000..3c30e0e5c2363 --- /dev/null +++ b/crates/bevy_camera/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bevy_camera" +version = "0.8.0-dev" +edition = "2021" +description = "Provides camera utilities for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.8.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.8.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.8.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.8.0-dev" } +bevy_core = { path = "../bevy_core", version = "0.8.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.8.0-dev" } + + diff --git a/crates/bevy_camera/src/frame_capture.rs b/crates/bevy_camera/src/frame_capture.rs new file mode 100644 index 0000000000000..05d896c71c448 --- /dev/null +++ b/crates/bevy_camera/src/frame_capture.rs @@ -0,0 +1,194 @@ +use bevy_app::prelude::*; +use bevy_asset::{Assets, Handle}; +use bevy_core_pipeline::{draw_3d_graph, node, AlphaMask3d, Opaque3d, Transparent3d}; +use bevy_ecs::entity::Entity; +use bevy_ecs::prelude::{Component, World}; +use bevy_ecs::system::{Commands, Query}; + +use bevy_render::prelude::Image; +use bevy_render::render_asset::RenderAssets; +use bevy_render::render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, SlotValue}; +use bevy_render::render_phase::RenderPhase; +use bevy_render::render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, + ImageDataLayout, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, +}; +use bevy_render::renderer::{RenderContext, RenderDevice, RenderQueue}; +use bevy_render::{RenderApp, RenderStage}; + +// The name of the final node of the first pass. +pub const FRAME_CAPTURE_DRIVER: &str = "frame_capture_driver"; + +#[derive(Component, Clone)] +pub struct FrameCapture { + pub cpu_buffer: Buffer, + pub gpu_image: Handle, + pub width: u32, + pub height: u32, + pub camera: Option, + pub active: bool, +} + +impl FrameCapture { + pub fn new( + width: u32, + height: u32, + active: bool, + format: TextureFormat, + images: &mut Assets, + render_device: &RenderDevice, + ) -> Self { + let size = Extent3d { + width, + height, + ..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, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT, + }, + ..Default::default() + }; + image.resize(size); + + let gpu_image = images.add(image); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row(width as usize) * 4; + + let size = padded_bytes_per_row as u64 * height as u64; + + let cpu_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("Output Buffer"), + size, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + FrameCapture { + cpu_buffer, + gpu_image, + width, + height, + active, + camera: None, + } + } +} + +// Add 3D render phases for CAPTURE_CAMERA. +pub fn extract_camera_phases(mut commands: Commands, captures: Query<(Entity, &FrameCapture)>) { + for (entity, capture) in captures.iter() { + if capture.active { + let mut new_capture = capture.clone(); + new_capture.camera = Some(entity); + commands + .get_or_spawn(entity) + .insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )) + .insert(new_capture); + } + } +} + +// A node for the first pass camera that runs draw_3d_graph with this camera. +#[derive(Default)] +pub struct CaptureCameraDriver { + pub captures: Vec, +} + +impl render_graph::Node for CaptureCameraDriver { + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let gpu_images = world.get_resource::>().unwrap(); + + for capture in self.captures.iter() { + graph.run_sub_graph( + draw_3d_graph::NAME, + vec![SlotValue::Entity(capture.camera.unwrap())], + )?; + + let gpu_image = gpu_images.get(&capture.gpu_image).unwrap(); + let mut encoder = render_context + .render_device + .create_command_encoder(&CommandEncoderDescriptor::default()); + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row((gpu_image.size.width) as usize) * 4; + + let texture_extent = Extent3d { + width: gpu_image.size.width as u32, + height: gpu_image.size.height as u32, + depth_or_array_layers: 1, + }; + + encoder.copy_texture_to_buffer( + gpu_image.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &capture.cpu_buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new(padded_bytes_per_row as u32).unwrap(), + ), + rows_per_image: None, + }, + }, + texture_extent, + ); + let render_queue = world.get_resource::().unwrap(); + render_queue.submit(std::iter::once(encoder.finish())); + } + + Ok(()) + } + fn update(&mut self, world: &mut World) { + for cap in world.query::<&mut FrameCapture>().iter_mut(world) { + self.captures.push(cap.clone()); + } + } +} + +pub struct CapturePlugin; +impl Plugin for CapturePlugin { + fn build(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + // This will add 3D render phases for the capture camera. + render_app.add_system_to_stage(RenderStage::Extract, extract_camera_phases); + + let mut graph = render_app.world.get_resource_mut::().unwrap(); + + // Add a node for the capture. + graph.add_node(FRAME_CAPTURE_DRIVER, CaptureCameraDriver::default()); + + // The capture's dependencies include those of the main pass. + graph + .add_node_edge(node::MAIN_PASS_DEPENDENCIES, FRAME_CAPTURE_DRIVER) + .unwrap(); + + // Insert the capture node: CLEAR_PASS_DRIVER -> FRAME_CAPTURE_DRIVER -> MAIN_PASS_DRIVER + graph + .add_node_edge(node::CLEAR_PASS_DRIVER, FRAME_CAPTURE_DRIVER) + .unwrap(); + graph + .add_node_edge(FRAME_CAPTURE_DRIVER, node::MAIN_PASS_DRIVER) + .unwrap(); + } +} diff --git a/crates/bevy_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs new file mode 100644 index 0000000000000..fad8b7139d5d8 --- /dev/null +++ b/crates/bevy_camera/src/lib.rs @@ -0,0 +1,3 @@ +pub mod frame_capture; + +pub use frame_capture::*; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 7fccda37863d0..5612ef9dc789a 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -96,6 +96,7 @@ bevy_text = { path = "../bevy_text", optional = true, version = "0.8.0-dev" } bevy_ui = { path = "../bevy_ui", optional = true, version = "0.8.0-dev" } bevy_winit = { path = "../bevy_winit", optional = true, version = "0.8.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.8.0-dev" } +bevy_camera = { path = "../bevy_camera", optional = true, version = "0.8.0-dev" } [target.'cfg(target_os = "android")'.dependencies] # This version *must* be the same as the version used by winit, diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index cc63909ab3cb8..88cc72b5e8335 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -101,6 +101,12 @@ pub mod animation { pub use bevy_animation::*; } +#[cfg(feature = "bevy_camera")] +pub mod camera { + //! Provides types and plugins for audio playback. + pub use bevy_camera::*; +} + #[cfg(feature = "bevy_audio")] pub mod audio { //! Provides types and plugins for audio playback. diff --git a/examples/3d/frame_capture.rs b/examples/3d/frame_capture.rs new file mode 100644 index 0000000000000..a9948a1d99074 --- /dev/null +++ b/examples/3d/frame_capture.rs @@ -0,0 +1,139 @@ +use bevy::camera::{CapturePlugin, FrameCapture}; +use bevy::core_pipeline::RenderTargetClearColors; +use bevy::prelude::*; +use bevy::render::camera::{CameraTypePlugin, RenderTarget}; + +use bevy::render::render_resource::{MapMode, TextureFormat}; +use bevy::render::renderer::RenderDevice; + +#[derive(Component, Default)] +pub struct CaptureCamera1; + +#[derive(Component, Default)] +pub struct CaptureCamera2; + +fn main() { + App::new() + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1.0 / 5.0f32, + }) + .add_plugins(DefaultPlugins) + .add_plugin(CameraTypePlugin::::default()) + .add_plugin(CameraTypePlugin::::default()) + .add_plugin(CapturePlugin) + .add_startup_system(setup) + .add_system(animate_light_direction) + .add_system(save_img) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut images: ResMut>, + mut clear_colors: ResMut, + render_device: Res, +) { + commands.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0")); + + commands + .spawn_bundle(PerspectiveCameraBundle { + 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 capture = FrameCapture::new( + 512, + 512, + true, + TextureFormat::Rgba8UnormSrgb, + &mut images, + &render_device, + ); + let render_target = RenderTarget::Image(capture.gpu_image.clone()); + clear_colors.insert(render_target.clone(), Color::GRAY); + parent + .spawn_bundle(PerspectiveCameraBundle:: { + camera: Camera { + target: render_target, + ..default() + }, + ..PerspectiveCameraBundle::new() + }) + .insert(capture); + }) + .with_children(|parent| { + let capture = FrameCapture::new( + 512, + 512, + true, + TextureFormat::Rgba8UnormSrgb, + &mut images, + &render_device, + ); + let render_target = RenderTarget::Image(capture.gpu_image.clone()); + clear_colors.insert(render_target.clone(), Color::BISQUE); + parent + .spawn_bundle(PerspectiveCameraBundle:: { + camera: Camera { + target: render_target, + ..default() + }, + ..PerspectiveCameraBundle::new() + }) + .insert(capture); + }); + const HALF_SIZE: f32 = 1.0; + commands.spawn_bundle(DirectionalLightBundle { + directional_light: DirectionalLight { + shadow_projection: OrthographicProjection { + left: -HALF_SIZE, + right: HALF_SIZE, + bottom: -HALF_SIZE, + top: HALF_SIZE, + near: -10.0 * HALF_SIZE, + far: 10.0 * HALF_SIZE, + ..default() + }, + shadows_enabled: true, + ..default() + }, + ..default() + }); +} + +fn animate_light_direction( + time: Res