From 81d57e129b507047ab165b1cee1975cd54ba100f Mon Sep 17 00:00:00 2001 From: Dusty DeWeese Date: Thu, 24 Feb 2022 00:40:24 +0000 Subject: [PATCH] Add capability to render to a texture (#3412) # Objective Will fix #3377 and #3254 ## Solution Use an enum to represent either a `WindowId` or `Handle` in place of `Camera::window`. Co-authored-by: Carter Anderson --- Cargo.toml | 4 + crates/bevy_core_pipeline/Cargo.toml | 1 + crates/bevy_core_pipeline/src/clear_pass.rs | 35 ++- crates/bevy_core_pipeline/src/lib.rs | 33 ++- crates/bevy_pbr/src/light.rs | 88 +++---- crates/bevy_render/src/camera/camera.rs | 107 +++++++- crates/bevy_render/src/camera/mod.rs | 18 +- crates/bevy_render/src/view/mod.rs | 69 +++-- .../src/view/visibility/render_layers.rs | 16 +- examples/3d/render_to_texture.rs | 242 ++++++++++++++++++ examples/README.md | 1 + examples/window/multiple_windows.rs | 4 +- 12 files changed, 499 insertions(+), 119 deletions(-) create mode 100644 examples/3d/render_to_texture.rs diff --git a/Cargo.toml b/Cargo.toml index 9f9f58246a..76130b0a1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,6 +212,10 @@ path = "examples/3d/spherical_area_lights.rs" name = "texture" path = "examples/3d/texture.rs" +[[example]] +name = "render_to_texture" +path = "examples/3d/render_to_texture.rs" + [[example]] name = "update_gltf_scene" path = "examples/3d/update_gltf_scene.rs" diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 1ebec225db..8ae43e7e55 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -19,4 +19,5 @@ bevy_asset = { path = "../bevy_asset", version = "0.6.0" } bevy_core = { path = "../bevy_core", version = "0.6.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.6.0" } bevy_render = { path = "../bevy_render", version = "0.6.0" } +bevy_utils = { path = "../bevy_utils", version = "0.6.0" } diff --git a/crates/bevy_core_pipeline/src/clear_pass.rs b/crates/bevy_core_pipeline/src/clear_pass.rs index 89c283aef8..ca24b15941 100644 --- a/crates/bevy_core_pipeline/src/clear_pass.rs +++ b/crates/bevy_core_pipeline/src/clear_pass.rs @@ -1,9 +1,11 @@ use std::collections::HashSet; -use crate::ClearColor; +use crate::{ClearColor, RenderTargetClearColors}; use bevy_ecs::prelude::*; use bevy_render::{ - camera::ExtractedCamera, + camera::{ExtractedCamera, RenderTarget}, + prelude::Image, + render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo}, render_resource::{ LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, @@ -47,21 +49,26 @@ impl Node for ClearPassNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let mut cleared_windows = HashSet::new(); + let mut cleared_targets = HashSet::new(); let clear_color = world.get_resource::().unwrap(); + let render_target_clear_colors = world.get_resource::().unwrap(); // This gets all ViewTargets and ViewDepthTextures and clears its attachments // TODO: This has the potential to clear the same target multiple times, if there // are multiple views drawing to the same target. This should be fixed when we make // clearing happen on "render targets" instead of "views" (see the TODO below for more context). for (target, depth, camera) in self.query.iter_manual(world) { + let mut color = &clear_color.0; if let Some(camera) = camera { - cleared_windows.insert(camera.window_id); + cleared_targets.insert(&camera.target); + if let Some(target_color) = render_target_clear_colors.get(&camera.target) { + color = target_color; + } } let pass_descriptor = RenderPassDescriptor { label: Some("clear_pass"), color_attachments: &[target.get_color_attachment(Operations { - load: LoadOp::Clear(clear_color.0.into()), + load: LoadOp::Clear((*color).into()), store: true, })], depth_stencil_attachment: depth.map(|depth| RenderPassDepthStencilAttachment { @@ -83,18 +90,28 @@ impl Node for ClearPassNode { // which will cause panics. The real fix here is to clear "render targets" directly // instead of "views". This should be removed once full RenderTargets are implemented. let windows = world.get_resource::().unwrap(); - for window in windows.values() { + let images = world.get_resource::>().unwrap(); + for target in render_target_clear_colors.colors.keys().cloned().chain( + windows + .values() + .map(|window| RenderTarget::Window(window.id)), + ) { // skip windows that have already been cleared - if cleared_windows.contains(&window.id) { + if cleared_targets.contains(&target) { continue; } let pass_descriptor = RenderPassDescriptor { label: Some("clear_pass"), color_attachments: &[RenderPassColorAttachment { - view: window.swap_chain_texture.as_ref().unwrap(), + view: target.get_texture_view(windows, images).unwrap(), resolve_target: None, ops: Operations { - load: LoadOp::Clear(clear_color.0.into()), + load: LoadOp::Clear( + (*render_target_clear_colors + .get(&target) + .unwrap_or(&clear_color.0)) + .into(), + ), store: true, }, }], diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index a00a4b734c..6e3c84761b 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -9,6 +9,8 @@ pub mod prelude { pub use crate::ClearColor; } +use bevy_utils::HashMap; + pub use clear_pass::*; pub use clear_pass_driver::*; pub use main_pass_2d::*; @@ -21,7 +23,7 @@ use bevy_app::{App, Plugin}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; use bevy_render::{ - camera::{ActiveCameras, CameraPlugin}, + camera::{ActiveCameras, CameraPlugin, RenderTarget}, color::Color, render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, render_phase::{ @@ -48,6 +50,20 @@ impl Default for ClearColor { } } +#[derive(Clone, Debug, Default)] +pub struct RenderTargetClearColors { + colors: HashMap, +} + +impl RenderTargetClearColors { + pub fn get(&self, target: &RenderTarget) -> Option<&Color> { + self.colors.get(target) + } + pub fn insert(&mut self, target: RenderTarget, color: Color) { + self.colors.insert(target, color); + } +} + // Plugins that contribute to the RenderGraph should use the following label conventions: // 1. Graph modules should have a NAME, input module, and node module (where relevant) // 2. The "top level" graph is the plugin module root. Just add things like `pub mod node` directly under the plugin module @@ -96,7 +112,8 @@ pub enum CorePipelineRenderSystems { impl Plugin for CorePipelinePlugin { fn build(&self, app: &mut App) { - app.init_resource::(); + app.init_resource::() + .init_resource::(); let render_app = match app.get_sub_app_mut(RenderApp) { Ok(render_app) => render_app, @@ -330,12 +347,22 @@ impl CachedPipelinePhaseItem for Transparent3d { } } -pub fn extract_clear_color(clear_color: Res, mut render_world: ResMut) { +pub fn extract_clear_color( + clear_color: Res, + clear_colors: Res, + mut render_world: ResMut, +) { // If the clear color has changed if clear_color.is_changed() { // Update the clear color resource in the render world render_world.insert_resource(clear_color.clone()); } + + // If the clear color has changed + if clear_colors.is_changed() { + // Update the clear color resource in the render world + render_world.insert_resource(clear_colors.clone()); + } } pub fn extract_core_pipeline_camera_phases( diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 61f416913e..d4bd8943a1 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -1,11 +1,13 @@ use std::collections::HashSet; +use bevy_asset::Assets; use bevy_ecs::prelude::*; use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_reflect::Reflect; use bevy_render::{ camera::{Camera, CameraProjection, OrthographicProjection}, color::Color, + prelude::Image, primitives::{Aabb, CubemapFrusta, Frustum, Sphere}, view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities}, }; @@ -354,62 +356,62 @@ const Z_SLICES: u32 = 24; pub fn add_clusters( mut commands: Commands, windows: Res, + images: Res>, cameras: Query<(Entity, &Camera), Without>, ) { for (entity, camera) in cameras.iter() { - let window = match windows.get(camera.window) { - Some(window) => window, - None => continue, - }; - let clusters = Clusters::from_screen_size_and_z_slices( - UVec2::new(window.physical_width(), window.physical_height()), - Z_SLICES, - ); - commands.entity(entity).insert(clusters); + if let Some(size) = camera.target.get_physical_size(&windows, &images) { + let clusters = Clusters::from_screen_size_and_z_slices(size, Z_SLICES); + commands.entity(entity).insert(clusters); + } } } -pub fn update_clusters(windows: Res, mut views: Query<(&Camera, &mut Clusters)>) { +pub fn update_clusters( + windows: Res, + images: Res>, + mut views: Query<(&Camera, &mut Clusters)>, +) { for (camera, mut clusters) in views.iter_mut() { let is_orthographic = camera.projection_matrix.w_axis.w == 1.0; let inverse_projection = camera.projection_matrix.inverse(); - let window = windows.get(camera.window).unwrap(); - let screen_size_u32 = UVec2::new(window.physical_width(), window.physical_height()); - // Don't update clusters if screen size is 0. - if screen_size_u32.x == 0 || screen_size_u32.y == 0 { - continue; - } - *clusters = - Clusters::from_screen_size_and_z_slices(screen_size_u32, clusters.axis_slices.z); - let screen_size = screen_size_u32.as_vec2(); - let tile_size_u32 = clusters.tile_size; - let tile_size = tile_size_u32.as_vec2(); + if let Some(screen_size_u32) = camera.target.get_physical_size(&windows, &images) { + // Don't update clusters if screen size is 0. + if screen_size_u32.x == 0 || screen_size_u32.y == 0 { + continue; + } + *clusters = + Clusters::from_screen_size_and_z_slices(screen_size_u32, clusters.axis_slices.z); + let screen_size = screen_size_u32.as_vec2(); + let tile_size_u32 = clusters.tile_size; + let tile_size = tile_size_u32.as_vec2(); - // Calculate view space AABBs - // NOTE: It is important that these are iterated in a specific order - // so that we can calculate the cluster index in the fragment shader! - // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan - // along z - let mut aabbs = Vec::with_capacity( - (clusters.axis_slices.y * clusters.axis_slices.x * clusters.axis_slices.z) as usize, - ); - for y in 0..clusters.axis_slices.y { - for x in 0..clusters.axis_slices.x { - for z in 0..clusters.axis_slices.z { - aabbs.push(compute_aabb_for_cluster( - clusters.near, - camera.far, - tile_size, - screen_size, - inverse_projection, - is_orthographic, - clusters.axis_slices, - UVec3::new(x, y, z), - )); + // Calculate view space AABBs + // NOTE: It is important that these are iterated in a specific order + // so that we can calculate the cluster index in the fragment shader! + // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan + // along z + let mut aabbs = Vec::with_capacity( + (clusters.axis_slices.y * clusters.axis_slices.x * clusters.axis_slices.z) as usize, + ); + for y in 0..clusters.axis_slices.y { + for x in 0..clusters.axis_slices.x { + for z in 0..clusters.axis_slices.z { + aabbs.push(compute_aabb_for_cluster( + clusters.near, + camera.far, + tile_size, + screen_size, + inverse_projection, + is_orthographic, + clusters.axis_slices, + UVec3::new(x, y, z), + )); + } } } + clusters.aabbs = aabbs; } - clusters.aabbs = aabbs; } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 4f73ebaf90..07e748fe82 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1,4 +1,8 @@ -use crate::camera::CameraProjection; +use crate::{ + camera::CameraProjection, prelude::Image, render_asset::RenderAssets, + render_resource::TextureView, view::ExtractedWindows, +}; +use bevy_asset::{AssetEvent, Assets, Handle}; use bevy_ecs::{ component::Component, entity::Entity, @@ -8,11 +12,13 @@ use bevy_ecs::{ reflect::ReflectComponent, system::{QuerySet, Res}, }; -use bevy_math::{Mat4, Vec2, Vec3}; +use bevy_math::{Mat4, UVec2, Vec2, Vec3}; use bevy_reflect::{Reflect, ReflectDeserialize}; use bevy_transform::components::GlobalTransform; +use bevy_utils::HashSet; use bevy_window::{WindowCreated, WindowId, WindowResized, Windows}; use serde::{Deserialize, Serialize}; +use wgpu::Extent3d; #[derive(Component, Default, Debug, Reflect)] #[reflect(Component)] @@ -20,13 +26,77 @@ pub struct Camera { pub projection_matrix: Mat4, pub name: Option, #[reflect(ignore)] - pub window: WindowId, + pub target: RenderTarget, #[reflect(ignore)] pub depth_calculation: DepthCalculation, pub near: f32, pub far: f32, } +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] +pub enum RenderTarget { + /// Window to which the camera's view is rendered. + Window(WindowId), + /// Image to which the camera's view is rendered. + Image(Handle), +} + +impl Default for RenderTarget { + fn default() -> Self { + Self::Window(Default::default()) + } +} + +impl RenderTarget { + pub fn get_texture_view<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + ) -> Option<&'a TextureView> { + match self { + RenderTarget::Window(window_id) => windows + .get(window_id) + .and_then(|window| window.swap_chain_texture.as_ref()), + RenderTarget::Image(image_handle) => { + images.get(image_handle).map(|image| &image.texture_view) + } + } + } + pub fn get_physical_size(&self, windows: &Windows, images: &Assets) -> Option { + match self { + RenderTarget::Window(window_id) => windows + .get(*window_id) + .map(|window| UVec2::new(window.physical_width(), window.physical_height())), + RenderTarget::Image(image_handle) => images.get(image_handle).map(|image| { + let Extent3d { width, height, .. } = image.texture_descriptor.size; + UVec2::new(width, height) + }), + } + } + pub fn get_logical_size(&self, windows: &Windows, images: &Assets) -> Option { + match self { + RenderTarget::Window(window_id) => windows + .get(*window_id) + .map(|window| Vec2::new(window.width(), window.height())), + RenderTarget::Image(image_handle) => images.get(image_handle).map(|image| { + let Extent3d { width, height, .. } = image.texture_descriptor.size; + Vec2::new(width as f32, height as f32) + }), + } + } + // Check if this render target is contained in the given changed windows or images. + fn is_changed( + &self, + changed_window_ids: &[WindowId], + changed_image_handles: &HashSet<&Handle>, + ) -> bool { + match self { + RenderTarget::Window(window_id) => changed_window_ids.contains(window_id), + RenderTarget::Image(image_handle) => changed_image_handles.contains(&image_handle), + } + } +} + #[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)] #[reflect_value(Serialize, Deserialize)] pub enum DepthCalculation { @@ -47,11 +117,11 @@ impl Camera { pub fn world_to_screen( &self, windows: &Windows, + images: &Assets, camera_transform: &GlobalTransform, world_position: Vec3, ) -> Option { - let window = windows.get(self.window)?; - let window_size = Vec2::new(window.width(), window.height()); + let window_size = self.target.get_logical_size(windows, images)?; // Build a transform to convert from world to NDC using camera data let world_to_ndc: Mat4 = self.projection_matrix * camera_transform.compute_matrix().inverse(); @@ -74,7 +144,9 @@ impl Camera { pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, + mut image_asset_events: EventReader>, windows: Res, + images: Res>, mut queries: QuerySet<( QueryState<(Entity, &mut Camera, &mut T)>, QueryState>, @@ -101,17 +173,30 @@ pub fn camera_system( changed_window_ids.push(event.id); } + let changed_image_handles: HashSet<&Handle> = image_asset_events + .iter() + .filter_map(|event| { + if let AssetEvent::Modified { handle } = event { + Some(handle) + } else { + None + } + }) + .collect(); + let mut added_cameras = vec![]; for entity in &mut queries.q1().iter() { added_cameras.push(entity); } for (entity, mut camera, mut camera_projection) in queries.q0().iter_mut() { - if let Some(window) = windows.get(camera.window) { - if changed_window_ids.contains(&window.id()) - || added_cameras.contains(&entity) - || camera_projection.is_changed() - { - camera_projection.update(window.width(), window.height()); + if camera + .target + .is_changed(&changed_window_ids, &changed_image_handles) + || added_cameras.contains(&entity) + || camera_projection.is_changed() + { + if let Some(size) = camera.target.get_logical_size(&windows, &images) { + camera_projection.update(size.x, size.y); camera.projection_matrix = camera_projection.get_projection_matrix(); camera.depth_calculation = camera_projection.depth_calculation(); } diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 5be7298689..35e3451a43 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -5,14 +5,17 @@ mod camera; mod projection; pub use active_cameras::*; +use bevy_asset::Assets; +use bevy_math::UVec2; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; -use bevy_window::{WindowId, Windows}; +use bevy_window::Windows; pub use bundle::*; pub use camera::*; pub use projection::*; use crate::{ + prelude::Image, primitives::Aabb, view::{ComputedVisibility, ExtractedView, Visibility, VisibleEntities}, RenderApp, RenderStage, @@ -68,14 +71,16 @@ pub struct ExtractedCameraNames { #[derive(Component, Debug)] pub struct ExtractedCamera { - pub window_id: WindowId, + pub target: RenderTarget, pub name: Option, + pub physical_size: Option, } fn extract_cameras( mut commands: Commands, active_cameras: Res, windows: Res, + images: Res>, query: Query<(Entity, &Camera, &GlobalTransform, &VisibleEntities)>, ) { let mut entities = HashMap::default(); @@ -84,18 +89,19 @@ fn extract_cameras( if let Some((entity, camera, transform, visible_entities)) = camera.entity.and_then(|e| query.get(e).ok()) { - if let Some(window) = windows.get(camera.window) { + if let Some(size) = camera.target.get_physical_size(&windows, &images) { entities.insert(name.clone(), entity); commands.get_or_spawn(entity).insert_bundle(( ExtractedCamera { - window_id: camera.window, + target: camera.target.clone(), name: camera.name.clone(), + physical_size: camera.target.get_physical_size(&windows, &images), }, ExtractedView { projection: camera.projection_matrix, transform: *transform, - width: window.physical_width().max(1), - height: window.physical_height().max(1), + width: size.x.max(1), + height: size.y.max(1), near: camera.near, far: camera.far, }, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index abee8bd1ae..95233083c6 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -10,6 +10,8 @@ pub use window::*; use crate::{ camera::{ExtractedCamera, ExtractedCameraNames}, + prelude::Image, + render_asset::RenderAssets, render_resource::{std140::AsStd140, DynamicUniformVec, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, TextureCache}, @@ -170,10 +172,12 @@ fn prepare_view_uniforms( .write_buffer(&render_device, &render_queue); } +#[allow(clippy::too_many_arguments)] fn prepare_view_targets( mut commands: Commands, camera_names: Res, windows: Res, + images: Res>, msaa: Res, render_device: Res, mut texture_cache: ResMut, @@ -185,41 +189,34 @@ fn prepare_view_targets( } else { continue; }; - let window = if let Some(window) = windows.get(&camera.window_id) { - window - } else { - continue; - }; - let swap_chain_texture = if let Some(texture) = &window.swap_chain_texture { - texture - } else { - continue; - }; - let sampled_target = if msaa.samples > 1 { - let sampled_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("sampled_color_attachment_texture"), - size: Extent3d { - width: window.physical_width, - height: window.physical_height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::bevy_default(), - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ); - Some(sampled_texture.default_view.clone()) - } else { - None - }; - - commands.entity(entity).insert(ViewTarget { - view: swap_chain_texture.clone(), - sampled_target, - }); + 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( + &render_device, + TextureDescriptor { + label: Some("sampled_color_attachment_texture"), + size: Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: TextureFormat::bevy_default(), + usage: TextureUsages::RENDER_ATTACHMENT, + }, + ); + Some(sampled_texture.default_view.clone()) + } else { + None + }; + commands.entity(entity).insert(ViewTarget { + view: texture_view.clone(), + sampled_target, + }); + } + } } } diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index 3315beed33..2d4f5312eb 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -49,17 +49,17 @@ impl RenderLayers { pub const TOTAL_LAYERS: usize = std::mem::size_of::() * 8; /// Create a new `RenderLayers` belonging to the given layer. - pub fn layer(n: Layer) -> Self { + pub const fn layer(n: Layer) -> Self { RenderLayers(0).with(n) } /// Create a new `RenderLayers` that belongs to all layers. - pub fn all() -> Self { + pub const fn all() -> Self { RenderLayers(u32::MAX) } /// Create a new `RenderLayers` that belongs to no layers. - pub fn none() -> Self { + pub const fn none() -> Self { RenderLayers(0) } @@ -75,9 +75,8 @@ impl RenderLayers { /// /// # Panics /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. - #[must_use] - pub fn with(mut self, layer: Layer) -> Self { - assert!(usize::from(layer) < Self::TOTAL_LAYERS); + pub const fn with(mut self, layer: Layer) -> Self { + assert!((layer as usize) < Self::TOTAL_LAYERS); self.0 |= 1 << layer; self } @@ -86,9 +85,8 @@ impl RenderLayers { /// /// # Panics /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. - #[must_use] - pub fn without(mut self, layer: Layer) -> Self { - assert!(usize::from(layer) < Self::TOTAL_LAYERS); + pub const fn without(mut self, layer: Layer) -> Self { + assert!((layer as usize) < Self::TOTAL_LAYERS); self.0 &= !(1 << layer); self } diff --git a/examples/3d/render_to_texture.rs b/examples/3d/render_to_texture.rs new file mode 100644 index 0000000000..e00a9efb10 --- /dev/null +++ b/examples/3d/render_to_texture.rs @@ -0,0 +1,242 @@ +use bevy::{ + core_pipeline::{ + draw_3d_graph, node, AlphaMask3d, Opaque3d, RenderTargetClearColors, Transparent3d, + }, + prelude::*, + reflect::TypeUuid, + render::{ + camera::{ActiveCameras, Camera, ExtractedCameraNames, RenderTarget}, + render_graph::{NodeRunError, RenderGraph, RenderGraphContext, SlotValue}, + render_phase::RenderPhase, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderContext, + view::RenderLayers, + RenderApp, RenderStage, + }, +}; + +// This handle will point at the texture to which we will render in the first pass. +pub const RENDER_IMAGE_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Image::TYPE_UUID, 13378939762009864029); + +// The name of the final node of the first pass. +pub const FIRST_PASS_DRIVER: &str = "first_pass_driver"; + +// The name of the camera that determines the view rendered in the first pass. +pub const FIRST_PASS_CAMERA: &str = "first_pass_camera"; + +fn main() { + let mut app = App::new(); + app.insert_resource(Msaa { samples: 4 }) // Use 4x MSAA + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(cube_rotator_system) + .add_system(rotator_system); + + let render_app = app.sub_app_mut(RenderApp); + + // 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::().unwrap(); + + // Add a node for the first pass. + graph.add_node(FIRST_PASS_DRIVER, FirstPassCameraDriver); + + // 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 FIRST_PASS_CAMERA. +fn extract_first_pass_camera_phases(mut commands: Commands, active_cameras: Res) { + if let Some(camera) = active_cameras.get(FIRST_PASS_CAMERA) { + if let Some(entity) = camera.entity { + commands.get_or_spawn(entity).insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + } +} + +// A node for the first pass camera that runs draw_3d_graph with this camera. +struct FirstPassCameraDriver; +impl bevy::render::render_graph::Node for FirstPassCameraDriver { + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let extracted_cameras = world.get_resource::().unwrap(); + if let Some(camera_3d) = extracted_cameras.entities.get(FIRST_PASS_CAMERA) { + graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(*camera_3d)])?; + } + Ok(()) + } +} + +// 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 setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut active_cameras: ResMut, + mut images: ResMut>, + mut clear_colors: ResMut, +) { + 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::Bgra8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + }, + ..Default::default() + }; + + // fill image.data with zeroes + image.resize(size); + + let image_handle = images.set(RENDER_IMAGE_HANDLE, image); + + let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 })); + let cube_material_handle = materials.add(StandardMaterial { + base_color: Color::rgb(0.8, 0.7, 0.6), + reflectance: 0.02, + unlit: false, + ..Default::default() + }); + + // 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 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.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 + let render_target = RenderTarget::Image(image_handle); + clear_colors.insert(render_target.clone(), Color::WHITE); + active_cameras.add(FIRST_PASS_CAMERA); + commands + .spawn_bundle(PerspectiveCameraBundle { + camera: Camera { + name: Some(FIRST_PASS_CAMERA.to_string()), + target: render_target, + ..Default::default() + }, + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::default(), Vec3::Y), + ..Default::default() + }) + .insert(first_pass_layer); + // NOTE: omitting the RenderLayers component for this camera may cause a validation error: + // + // thread 'main' panicked at 'wgpu error: Validation Error + // + // Caused by: + // In a RenderPass + // note: encoder = `` + // In a pass parameter + // note: command buffer = `` + // Attempted to use texture (5, 1, Metal) mips 0..1 layers 0..1 as a combination of COLOR_TARGET within a usage scope. + // + // This happens because the texture would be written and read in the same frame, which is not allowed. + // So either render layers must be used to avoid this, or the texture must be double buffered. + + let cube_size = 4.0; + 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(RENDER_IMAGE_HANDLE.typed()), + reflectance: 0.02, + unlit: false, + ..Default::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.0, 1.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