Path: blob/main/examples/shader_advanced/render_depth_to_texture.rs
6849 views
//! Demonstrates how to use depth-only cameras.1//!2//! A *depth-only camera* is a camera that renders only to a depth buffer, not3//! to a color buffer. That depth buffer can then be used in shaders for various4//! special effects.5//!6//! To create a depth-only camera, we create a [`Camera3d`] and set its7//! [`RenderTarget`] to [`RenderTarget::None`] to disable creation of a color8//! buffer. Then we add a new node to the render graph that copies the9//! [`bevy::render::view::ViewDepthTexture`] that Bevy creates for that camera10//! to a texture. This texture can then be attached to a material and sampled in11//! the shader.12//!13//! This demo consists of a rotating cube with a depth-only camera pointed at14//! it. The depth texture from the depth-only camera appears on a plane. You can15//! use the WASD keys to make the depth-only camera orbit around the cube.1617use std::f32::consts::{FRAC_PI_2, PI};1819use bevy::{20asset::RenderAssetUsages,21camera::RenderTarget,22color::palettes::css::LIME,23core_pipeline::{24core_3d::graph::{Core3d, Node3d},25prepass::DepthPrepass,26},27ecs::{query::QueryItem, system::lifetimeless::Read},28image::{ImageCompareFunction, ImageSampler, ImageSamplerDescriptor},29math::ops::{acos, atan2, sin_cos},30prelude::*,31render::{32camera::ExtractedCamera,33extract_resource::{ExtractResource, ExtractResourcePlugin},34render_asset::RenderAssets,35render_graph::{36NodeRunError, RenderGraphContext, RenderGraphExt as _, RenderLabel, ViewNode,37ViewNodeRunner,38},39render_resource::{40AsBindGroup, CommandEncoderDescriptor, Extent3d, Origin3d, TexelCopyTextureInfo,41TextureAspect, TextureDimension, TextureFormat,42},43renderer::RenderContext,44texture::GpuImage,45view::ViewDepthTexture,46RenderApp,47},48shader::ShaderRef,49};5051/// A marker component for a rotating cube.52#[derive(Component)]53struct RotatingCube;5455/// The material that displays the contents of the depth buffer.56///57/// This material is placed on the plane.58#[derive(Clone, Debug, Asset, TypePath, AsBindGroup)]59struct ShowDepthTextureMaterial {60/// A copy of the depth texture that the depth-only camera produced.61#[texture(0, sample_type = "depth")]62#[sampler(1, sampler_type = "comparison")]63depth_texture: Option<Handle<Image>>,64}6566/// A label for the render node that copies the depth buffer from that of the67/// camera to the [`DemoDepthTexture`].68#[derive(Clone, PartialEq, Eq, Hash, Debug, RenderLabel)]69struct CopyDepthTexturePass;7071/// The render node that copies the depth buffer from that of the camera to the72/// [`DemoDepthTexture`].73#[derive(Default)]74struct CopyDepthTextureNode;7576/// Holds a copy of the depth buffer that the depth-only camera produces.77///78/// We need to make a copy for two reasons:79///80/// 1. The Bevy renderer automatically creates and maintains depth buffers on81/// its own. There's no mechanism to fetch the depth buffer for a camera outside82/// the render app. Thus it can't easily be attached to a material.83///84/// 2. `wgpu` doesn't allow applications to simultaneously render to and sample85/// from a standard depth texture, so a copy must be made regardless.86#[derive(Clone, Resource)]87struct DemoDepthTexture(Handle<Image>);8889/// [Spherical coordinates], used to implement the camera orbiting90/// functionality.91///92/// Note that these are in the mathematics convention, not the physics93/// convention. In a real application, one would probably use the physics94/// convention, but for familiarity's sake we stick to the most common95/// convention here.96///97/// [Spherical coordinates]: https://en.wikipedia.org/wiki/Spherical_coordinate_system98#[derive(Clone, Copy, Debug)]99struct SphericalCoordinates {100/// The radius, in world units.101radius: f32,102/// The elevation angle (latitude).103inclination: f32,104/// The azimuth angle (longitude).105azimuth: f32,106}107108/// The path to the shader that renders the depth texture.109static SHADER_ASSET_PATH: &str = "shaders/show_depth_texture_material.wgsl";110111/// The size in texels of a depth texture.112const DEPTH_TEXTURE_SIZE: u32 = 256;113114/// The rate at which the user can move the camera, in radians per second.115const CAMERA_MOVEMENT_SPEED: f32 = 2.0;116117/// The entry point.118fn main() {119let mut app = App::new();120121app.add_plugins(DefaultPlugins)122.add_plugins(MaterialPlugin::<ShowDepthTextureMaterial>::default())123.add_plugins(ExtractResourcePlugin::<DemoDepthTexture>::default())124.init_resource::<DemoDepthTexture>()125.add_systems(Startup, setup)126.add_systems(Update, rotate_cube)127.add_systems(Update, draw_camera_gizmo)128.add_systems(Update, move_camera);129130// Add the `CopyDepthTextureNode` to the render app.131let render_app = app132.get_sub_app_mut(RenderApp)133.expect("Render app should be present");134render_app.add_render_graph_node::<ViewNodeRunner<CopyDepthTextureNode>>(135Core3d,136CopyDepthTexturePass,137);138// We have the texture copy operation run in between the prepasses and139// the opaque pass. Since the depth rendering is part of the prepass, this140// is a reasonable time to perform the operation.141render_app.add_render_graph_edges(142Core3d,143(144Node3d::EndPrepasses,145CopyDepthTexturePass,146Node3d::MainOpaquePass,147),148);149150app.run();151}152153/// Creates the scene.154fn setup(155mut commands: Commands,156mut meshes: ResMut<Assets<Mesh>>,157mut standard_materials: ResMut<Assets<StandardMaterial>>,158mut show_depth_texture_materials: ResMut<Assets<ShowDepthTextureMaterial>>,159demo_depth_texture: Res<DemoDepthTexture>,160) {161spawn_rotating_cube(&mut commands, &mut meshes, &mut standard_materials);162spawn_plane(163&mut commands,164&mut meshes,165&mut show_depth_texture_materials,166&demo_depth_texture,167);168spawn_light(&mut commands);169spawn_depth_only_camera(&mut commands);170spawn_main_camera(&mut commands);171spawn_instructions(&mut commands);172}173174/// Spawns the main rotating cube.175fn spawn_rotating_cube(176commands: &mut Commands,177meshes: &mut Assets<Mesh>,178standard_materials: &mut Assets<StandardMaterial>,179) {180let cube_handle = meshes.add(Cuboid::new(3.0, 3.0, 3.0));181let rotating_cube_material_handle = standard_materials.add(StandardMaterial {182base_color: Color::WHITE,183unlit: false,184..default()185});186commands.spawn((187Mesh3d(cube_handle.clone()),188MeshMaterial3d(rotating_cube_material_handle),189Transform::IDENTITY,190RotatingCube,191));192}193194// Spawns the plane that shows the depth texture.195fn spawn_plane(196commands: &mut Commands,197meshes: &mut Assets<Mesh>,198show_depth_texture_materials: &mut Assets<ShowDepthTextureMaterial>,199demo_depth_texture: &DemoDepthTexture,200) {201let plane_handle = meshes.add(Plane3d::new(Vec3::Z, Vec2::splat(2.0)));202let show_depth_texture_material = show_depth_texture_materials.add(ShowDepthTextureMaterial {203depth_texture: Some(demo_depth_texture.0.clone()),204});205commands.spawn((206Mesh3d(plane_handle),207MeshMaterial3d(show_depth_texture_material),208Transform::from_xyz(10.0, 4.0, 0.0).with_scale(Vec3::splat(2.5)),209));210}211212/// Spawns a light.213fn spawn_light(commands: &mut Commands) {214commands.spawn((PointLight::default(), Transform::from_xyz(5.0, 6.0, 7.0)));215}216217/// Spawns the depth-only camera.218fn spawn_depth_only_camera(commands: &mut Commands) {219commands.spawn((220Camera3d::default(),221Transform::from_xyz(-4.0, -5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),222Camera {223// We specify no color render target, for maximum efficiency.224target: RenderTarget::None {225// When specifying no render target, we must manually specify226// the viewport size. Otherwise, Bevy won't know how big to make227// the depth buffer.228size: UVec2::splat(DEPTH_TEXTURE_SIZE),229},230// Make sure that we render from this depth-only camera *before*231// rendering from the main camera.232order: -1,233..Camera::default()234},235// We need to disable multisampling or the depth texture will be236// multisampled, which adds complexity we don't care about for this237// demo.238Msaa::Off,239// Cameras with no render target render *nothing* by default. To get240// them to render something, we must add a prepass that specifies what241// we want to render: in this case, depth.242DepthPrepass,243));244}245246/// Spawns the main camera that renders to the window.247fn spawn_main_camera(commands: &mut Commands) {248commands.spawn((249Camera3d::default(),250Transform::from_xyz(5.0, 2.0, 30.0).looking_at(vec3(5.0, 2.0, 0.0), Vec3::Y),251// Disable antialiasing just for simplicity's sake.252Msaa::Off,253));254}255256/// Spawns the instructional text at the top of the screen.257fn spawn_instructions(commands: &mut Commands) {258commands.spawn((259Text::new("Use WASD to move the secondary camera"),260Node {261position_type: PositionType::Absolute,262top: Val::Px(12.0),263left: Val::Px(12.0),264..Node::default()265},266));267}268269/// Spins the cube a bit every frame.270fn rotate_cube(mut cubes: Query<&mut Transform, With<RotatingCube>>, time: Res<Time>) {271for mut transform in &mut cubes {272transform.rotate_x(1.5 * time.delta_secs());273transform.rotate_y(1.1 * time.delta_secs());274transform.rotate_z(-1.3 * time.delta_secs());275}276}277278impl Material for ShowDepthTextureMaterial {279fn fragment_shader() -> ShaderRef {280SHADER_ASSET_PATH.into()281}282}283284impl ViewNode for CopyDepthTextureNode {285type ViewQuery = (Read<ExtractedCamera>, Read<ViewDepthTexture>);286287fn run<'w>(288&self,289_: &mut RenderGraphContext,290render_context: &mut RenderContext<'w>,291(camera, depth_texture): QueryItem<'w, '_, Self::ViewQuery>,292world: &'w World,293) -> Result<(), NodeRunError> {294// Make sure we only run on the depth-only camera.295// We could make a marker component for that camera and extract it to296// the render world, but using `order` as a tag to tell the main camera297// and the depth-only camera apart works in a pinch.298if camera.order >= 0 {299return Ok(());300}301302// Grab the texture we're going to copy to.303let demo_depth_texture = world.resource::<DemoDepthTexture>();304let image_assets = world.resource::<RenderAssets<GpuImage>>();305let Some(demo_depth_image) = image_assets.get(demo_depth_texture.0.id()) else {306return Ok(());307};308309// Perform the copy.310render_context.add_command_buffer_generation_task(move |render_device| {311let mut command_encoder =312render_device.create_command_encoder(&CommandEncoderDescriptor {313label: Some("copy depth to demo texture command encoder"),314});315command_encoder.push_debug_group("copy depth to demo texture");316317// Copy from the view's depth texture to the destination depth318// texture.319command_encoder.copy_texture_to_texture(320TexelCopyTextureInfo {321texture: &depth_texture.texture,322mip_level: 0,323origin: Origin3d::default(),324aspect: TextureAspect::DepthOnly,325},326TexelCopyTextureInfo {327texture: &demo_depth_image.texture,328mip_level: 0,329origin: Origin3d::default(),330aspect: TextureAspect::DepthOnly,331},332Extent3d {333width: DEPTH_TEXTURE_SIZE,334height: DEPTH_TEXTURE_SIZE,335depth_or_array_layers: 1,336},337);338339command_encoder.pop_debug_group();340command_encoder.finish()341});342343Ok(())344}345}346347impl FromWorld for DemoDepthTexture {348fn from_world(world: &mut World) -> Self {349let mut images = world.resource_mut::<Assets<Image>>();350351// Create a new 32-bit floating point depth texture.352let mut depth_image = Image::new_uninit(353Extent3d {354width: DEPTH_TEXTURE_SIZE,355height: DEPTH_TEXTURE_SIZE,356depth_or_array_layers: 1,357},358TextureDimension::D2,359TextureFormat::Depth32Float,360RenderAssetUsages::default(),361);362363// Create a sampler. Note that this needs to specify a `compare`364// function in order to be compatible with depth textures.365depth_image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {366label: Some("custom depth image sampler".to_owned()),367compare: Some(ImageCompareFunction::Always),368..ImageSamplerDescriptor::default()369});370371let depth_image_handle = images.add(depth_image);372DemoDepthTexture(depth_image_handle)373}374}375376impl ExtractResource for DemoDepthTexture {377type Source = Self;378379fn extract_resource(source: &Self::Source) -> Self {380// Share the `DemoDepthTexture` resource over to the render world so381// that our `CopyDepthTextureNode` can access it.382(*source).clone()383}384}385386/// Draws an outline of the depth texture on the screen.387fn draw_camera_gizmo(cameras: Query<(&Camera, &GlobalTransform)>, mut gizmos: Gizmos) {388for (camera, transform) in &cameras {389// As above, we use the order as a cheap tag to tell the depth texture390// apart from the main texture.391if camera.order >= 0 {392continue;393}394395// Draw a cone representing the camera.396gizmos.primitive_3d(397&Cone {398radius: 1.0,399height: 3.0,400},401Isometry3d::new(402transform.translation(),403// We have to rotate here because `Cone` primitives are oriented404// along +Y and cameras point along +Z.405transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),406),407LIME,408);409}410}411412/// Orbits the cube when WASD is pressed.413fn move_camera(414mut cameras: Query<(&Camera, &mut Transform)>,415keyboard: Res<ButtonInput<KeyCode>>,416time: Res<Time>,417) {418for (camera, mut transform) in &mut cameras {419// Only affect the depth camera.420if camera.order >= 0 {421continue;422}423424// Convert the camera's position from Cartesian to spherical coordinates.425let mut spherical_coords = SphericalCoordinates::from_cartesian(transform.translation);426427// Modify those spherical coordinates as appropriate.428let mut changed = false;429if keyboard.pressed(KeyCode::KeyW) {430spherical_coords.inclination -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;431changed = true;432}433if keyboard.pressed(KeyCode::KeyS) {434spherical_coords.inclination += time.delta_secs() * CAMERA_MOVEMENT_SPEED;435changed = true;436}437if keyboard.pressed(KeyCode::KeyA) {438spherical_coords.azimuth += time.delta_secs() * CAMERA_MOVEMENT_SPEED;439changed = true;440}441if keyboard.pressed(KeyCode::KeyD) {442spherical_coords.azimuth -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;443changed = true;444}445446// If they were changed, convert from spherical coordinates back to447// Cartesian ones, and update the camera's transform.448if changed {449spherical_coords.inclination = spherical_coords.inclination.clamp(0.01, PI - 0.01);450transform.translation = spherical_coords.to_cartesian();451transform.look_at(Vec3::ZERO, Vec3::Y);452}453}454}455456impl SphericalCoordinates {457/// [Converts] from Cartesian coordinates to spherical coordinates.458///459/// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates460fn from_cartesian(p: Vec3) -> SphericalCoordinates {461let radius = p.length();462SphericalCoordinates {463radius,464inclination: acos(p.y / radius),465azimuth: atan2(p.z, p.x),466}467}468469/// [Converts] from spherical coordinates to Cartesian coordinates.470///471/// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates472fn to_cartesian(self) -> Vec3 {473let (sin_inclination, cos_inclination) = sin_cos(self.inclination);474let (sin_azimuth, cos_azimuth) = sin_cos(self.azimuth);475self.radius476* vec3(477sin_inclination * cos_azimuth,478cos_inclination,479sin_inclination * sin_azimuth,480)481}482}483484485