Skip to main content

render_depth_to_texture/
render_depth_to_texture.rs

1//! Demonstrates how to use depth-only cameras.
2//!
3//! A *depth-only camera* is a camera that renders only to a depth buffer, not
4//! to a color buffer. That depth buffer can then be used in shaders for various
5//! special effects.
6//!
7//! To create a depth-only camera, we create a [`Camera3d`] and set its
8//! [`RenderTarget`] to [`RenderTarget::None`] to disable creation of a color
9//! buffer. Then we add a system to the Core3d schedule that copies the
10//! [`bevy::render::view::ViewDepthTexture`] that Bevy creates for that camera
11//! to a texture. This texture can then be attached to a material and sampled in
12//! the shader.
13//!
14//! This demo consists of a rotating cube with a depth-only camera pointed at
15//! it. The depth texture from the depth-only camera appears on a plane. You can
16//! use the WASD keys to make the depth-only camera orbit around the cube.
17
18use std::f32::consts::{FRAC_PI_2, PI};
19
20use bevy::{
21    asset::RenderAssetUsages,
22    camera::RenderTarget,
23    color::palettes::css::LIME,
24    core_pipeline::{prepass::DepthPrepass, schedule::Core3d, Core3dSystems},
25    image::{ImageCompareFunction, ImageSampler, ImageSamplerDescriptor},
26    math::ops::{acos, atan2, sin_cos},
27    prelude::*,
28    render::{
29        camera::ExtractedCamera,
30        extract_resource::{ExtractResource, ExtractResourcePlugin},
31        render_asset::RenderAssets,
32        render_resource::{
33            AsBindGroup, Extent3d, Origin3d, TexelCopyTextureInfo, TextureAspect, TextureDimension,
34            TextureFormat,
35        },
36        renderer::{RenderContext, ViewQuery},
37        texture::GpuImage,
38        view::ViewDepthTexture,
39        RenderApp,
40    },
41    shader::ShaderRef,
42};
43
44/// A marker component for a rotating cube.
45#[derive(Component)]
46struct RotatingCube;
47
48/// The material that displays the contents of the depth buffer.
49///
50/// This material is placed on the plane.
51#[derive(Clone, Debug, Asset, TypePath, AsBindGroup)]
52struct ShowDepthTextureMaterial {
53    /// A copy of the depth texture that the depth-only camera produced.
54    #[texture(0, sample_type = "depth")]
55    #[sampler(1, sampler_type = "comparison")]
56    depth_texture: Option<Handle<Image>>,
57}
58
59/// Holds a copy of the depth buffer that the depth-only camera produces.
60///
61/// We need to make a copy for two reasons:
62///
63/// 1. The Bevy renderer automatically creates and maintains depth buffers on
64///    its own. There's no mechanism to fetch the depth buffer for a camera outside
65///    the render app. Thus it can't easily be attached to a material.
66///
67/// 2. `wgpu` doesn't allow applications to simultaneously render to and sample
68///    from a standard depth texture, so a copy must be made regardless.
69#[derive(Clone, Resource)]
70struct DemoDepthTexture(Handle<Image>);
71
72/// [Spherical coordinates], used to implement the camera orbiting
73/// functionality.
74///
75/// Note that these are in the mathematics convention, not the physics
76/// convention. In a real application, one would probably use the physics
77/// convention, but for familiarity's sake we stick to the most common
78/// convention here.
79///
80/// [Spherical coordinates]: https://en.wikipedia.org/wiki/Spherical_coordinate_system
81#[derive(Clone, Copy, Debug)]
82struct SphericalCoordinates {
83    /// The radius, in world units.
84    radius: f32,
85    /// The elevation angle (latitude).
86    inclination: f32,
87    /// The azimuth angle (longitude).
88    azimuth: f32,
89}
90
91/// The path to the shader that renders the depth texture.
92static SHADER_ASSET_PATH: &str = "shaders/show_depth_texture_material.wgsl";
93
94/// The size in texels of a depth texture.
95const DEPTH_TEXTURE_SIZE: u32 = 256;
96
97/// The rate at which the user can move the camera, in radians per second.
98const CAMERA_MOVEMENT_SPEED: f32 = 2.0;
99
100/// The entry point.
101fn main() {
102    let mut app = App::new();
103
104    app.add_plugins(DefaultPlugins)
105        .add_plugins(MaterialPlugin::<ShowDepthTextureMaterial>::default())
106        .add_plugins(ExtractResourcePlugin::<DemoDepthTexture>::default())
107        .init_resource::<DemoDepthTexture>()
108        .add_systems(Startup, setup)
109        .add_systems(Update, rotate_cube)
110        .add_systems(Update, draw_camera_gizmo)
111        .add_systems(Update, move_camera);
112
113    let render_app = app
114        .get_sub_app_mut(RenderApp)
115        .expect("Render app should be present");
116
117    render_app.add_systems(
118        Core3d,
119        copy_depth_texture_system
120            .after(Core3dSystems::Prepass)
121            .before(Core3dSystems::MainPass),
122    );
123
124    app.run();
125}
126
127fn copy_depth_texture_system(
128    view: ViewQuery<(&ExtractedCamera, &ViewDepthTexture)>,
129    demo_depth_texture: Option<Res<DemoDepthTexture>>,
130    image_assets: Res<RenderAssets<GpuImage>>,
131    mut ctx: RenderContext,
132) {
133    let Some(demo_depth_texture) = demo_depth_texture else {
134        return;
135    };
136
137    let (camera, depth_texture) = view.into_inner();
138
139    // Make sure we only run on the depth-only camera.
140    // We could make a marker component for that camera and extract it to
141    // the render world, but using `order` as a tag to tell the main camera
142    // and the depth-only camera apart works in a pinch.
143    if camera.order >= 0 {
144        return;
145    }
146
147    let Some(demo_depth_image) = image_assets.get(demo_depth_texture.0.id()) else {
148        return;
149    };
150
151    let command_encoder = ctx.command_encoder();
152    command_encoder.push_debug_group("copy depth to demo texture");
153    command_encoder.copy_texture_to_texture(
154        TexelCopyTextureInfo {
155            texture: &depth_texture.texture,
156            mip_level: 0,
157            origin: Origin3d::default(),
158            aspect: TextureAspect::DepthOnly,
159        },
160        TexelCopyTextureInfo {
161            texture: &demo_depth_image.texture,
162            mip_level: 0,
163            origin: Origin3d::default(),
164            aspect: TextureAspect::DepthOnly,
165        },
166        Extent3d {
167            width: DEPTH_TEXTURE_SIZE,
168            height: DEPTH_TEXTURE_SIZE,
169            depth_or_array_layers: 1,
170        },
171    );
172    command_encoder.pop_debug_group();
173}
174
175/// Creates the scene.
176fn setup(
177    mut commands: Commands,
178    mut meshes: ResMut<Assets<Mesh>>,
179    mut standard_materials: ResMut<Assets<StandardMaterial>>,
180    mut show_depth_texture_materials: ResMut<Assets<ShowDepthTextureMaterial>>,
181    demo_depth_texture: Res<DemoDepthTexture>,
182) {
183    spawn_rotating_cube(&mut commands, &mut meshes, &mut standard_materials);
184    spawn_plane(
185        &mut commands,
186        &mut meshes,
187        &mut show_depth_texture_materials,
188        &demo_depth_texture,
189    );
190    spawn_light(&mut commands);
191    spawn_depth_only_camera(&mut commands);
192    spawn_main_camera(&mut commands);
193    spawn_instructions(&mut commands);
194}
195
196/// Spawns the main rotating cube.
197fn spawn_rotating_cube(
198    commands: &mut Commands,
199    meshes: &mut Assets<Mesh>,
200    standard_materials: &mut Assets<StandardMaterial>,
201) {
202    let cube_handle = meshes.add(Cuboid::new(3.0, 3.0, 3.0));
203    let rotating_cube_material_handle = standard_materials.add(StandardMaterial {
204        base_color: Color::WHITE,
205        unlit: false,
206        ..default()
207    });
208    commands.spawn((
209        Mesh3d(cube_handle.clone()),
210        MeshMaterial3d(rotating_cube_material_handle),
211        Transform::IDENTITY,
212        RotatingCube,
213    ));
214}
215
216// Spawns the plane that shows the depth texture.
217fn spawn_plane(
218    commands: &mut Commands,
219    meshes: &mut Assets<Mesh>,
220    show_depth_texture_materials: &mut Assets<ShowDepthTextureMaterial>,
221    demo_depth_texture: &DemoDepthTexture,
222) {
223    let plane_handle = meshes.add(Plane3d::new(Vec3::Z, Vec2::splat(2.0)));
224    let show_depth_texture_material = show_depth_texture_materials.add(ShowDepthTextureMaterial {
225        depth_texture: Some(demo_depth_texture.0.clone()),
226    });
227    commands.spawn((
228        Mesh3d(plane_handle),
229        MeshMaterial3d(show_depth_texture_material),
230        Transform::from_xyz(10.0, 4.0, 0.0).with_scale(Vec3::splat(2.5)),
231    ));
232}
233
234/// Spawns a light.
235fn spawn_light(commands: &mut Commands) {
236    commands.spawn((PointLight::default(), Transform::from_xyz(5.0, 6.0, 7.0)));
237}
238
239/// Spawns the depth-only camera.
240fn spawn_depth_only_camera(commands: &mut Commands) {
241    commands.spawn((
242        Camera3d::default(),
243        Transform::from_xyz(-4.0, -5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
244        Camera {
245            // Make sure that we render from this depth-only camera *before*
246            // rendering from the main camera.
247            order: -1,
248            ..Camera::default()
249        },
250        // We specify no color render target, for maximum efficiency.
251        RenderTarget::None {
252            // When specifying no render target, we must manually specify
253            // the viewport size. Otherwise, Bevy won't know how big to make
254            // the depth buffer.
255            size: UVec2::splat(DEPTH_TEXTURE_SIZE),
256        },
257        // We need to disable multisampling or the depth texture will be
258        // multisampled, which adds complexity we don't care about for this
259        // demo.
260        Msaa::Off,
261        // Cameras with no render target render *nothing* by default. To get
262        // them to render something, we must add a prepass that specifies what
263        // we want to render: in this case, depth.
264        DepthPrepass,
265    ));
266}
267
268/// Spawns the main camera that renders to the window.
269fn spawn_main_camera(commands: &mut Commands) {
270    commands.spawn((
271        Camera3d::default(),
272        Transform::from_xyz(5.0, 2.0, 30.0).looking_at(vec3(5.0, 2.0, 0.0), Vec3::Y),
273        // Disable antialiasing just for simplicity's sake.
274        Msaa::Off,
275    ));
276}
277
278/// Spawns the instructional text at the top of the screen.
279fn spawn_instructions(commands: &mut Commands) {
280    commands.spawn((
281        Text::new("Use WASD to move the secondary camera"),
282        Node {
283            position_type: PositionType::Absolute,
284            top: px(12.0),
285            left: px(12.0),
286            ..Node::default()
287        },
288    ));
289}
290
291/// Spins the cube a bit every frame.
292fn rotate_cube(mut cubes: Query<&mut Transform, With<RotatingCube>>, time: Res<Time>) {
293    for mut transform in &mut cubes {
294        transform.rotate_x(1.5 * time.delta_secs());
295        transform.rotate_y(1.1 * time.delta_secs());
296        transform.rotate_z(-1.3 * time.delta_secs());
297    }
298}
299
300impl Material for ShowDepthTextureMaterial {
301    fn fragment_shader() -> ShaderRef {
302        SHADER_ASSET_PATH.into()
303    }
304}
305
306impl FromWorld for DemoDepthTexture {
307    fn from_world(world: &mut World) -> Self {
308        let mut images = world.resource_mut::<Assets<Image>>();
309
310        // Create a new 32-bit floating point depth texture.
311        let mut depth_image = Image::new_uninit(
312            Extent3d {
313                width: DEPTH_TEXTURE_SIZE,
314                height: DEPTH_TEXTURE_SIZE,
315                depth_or_array_layers: 1,
316            },
317            TextureDimension::D2,
318            TextureFormat::Depth32Float,
319            RenderAssetUsages::default(),
320        );
321
322        // Create a sampler. Note that this needs to specify a `compare`
323        // function in order to be compatible with depth textures.
324        depth_image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
325            label: Some("custom depth image sampler".to_owned()),
326            compare: Some(ImageCompareFunction::Always),
327            ..ImageSamplerDescriptor::default()
328        });
329
330        let depth_image_handle = images.add(depth_image);
331        DemoDepthTexture(depth_image_handle)
332    }
333}
334
335impl ExtractResource for DemoDepthTexture {
336    type Source = Self;
337
338    fn extract_resource(source: &Self::Source) -> Self {
339        // Share the `DemoDepthTexture` resource over to the render world so
340        // that our system can access it.
341        (*source).clone()
342    }
343}
344
345/// Draws an outline of the depth texture on the screen.
346fn draw_camera_gizmo(cameras: Query<(&Camera, &GlobalTransform)>, mut gizmos: Gizmos) {
347    for (camera, transform) in &cameras {
348        // As above, we use the order as a cheap tag to tell the depth texture
349        // apart from the main texture.
350        if camera.order >= 0 {
351            continue;
352        }
353
354        // Draw a cone representing the camera.
355        gizmos.primitive_3d(
356            &Cone {
357                radius: 1.0,
358                height: 3.0,
359            },
360            Isometry3d::new(
361                transform.translation(),
362                // We have to rotate here because `Cone` primitives are oriented
363                // along +Y and cameras point along +Z.
364                transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
365            ),
366            LIME,
367        );
368    }
369}
370
371/// Orbits the cube when WASD is pressed.
372fn move_camera(
373    mut cameras: Query<(&Camera, &mut Transform)>,
374    keyboard: Res<ButtonInput<KeyCode>>,
375    time: Res<Time>,
376) {
377    for (camera, mut transform) in &mut cameras {
378        // Only affect the depth camera.
379        if camera.order >= 0 {
380            continue;
381        }
382
383        // Convert the camera's position from Cartesian to spherical coordinates.
384        let mut spherical_coords = SphericalCoordinates::from_cartesian(transform.translation);
385
386        // Modify those spherical coordinates as appropriate.
387        let mut changed = false;
388        if keyboard.pressed(KeyCode::KeyW) {
389            spherical_coords.inclination -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;
390            changed = true;
391        }
392        if keyboard.pressed(KeyCode::KeyS) {
393            spherical_coords.inclination += time.delta_secs() * CAMERA_MOVEMENT_SPEED;
394            changed = true;
395        }
396        if keyboard.pressed(KeyCode::KeyA) {
397            spherical_coords.azimuth += time.delta_secs() * CAMERA_MOVEMENT_SPEED;
398            changed = true;
399        }
400        if keyboard.pressed(KeyCode::KeyD) {
401            spherical_coords.azimuth -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;
402            changed = true;
403        }
404
405        // If they were changed, convert from spherical coordinates back to
406        // Cartesian ones, and update the camera's transform.
407        if changed {
408            spherical_coords.inclination = spherical_coords.inclination.clamp(0.01, PI - 0.01);
409            transform.translation = spherical_coords.to_cartesian();
410            transform.look_at(Vec3::ZERO, Vec3::Y);
411        }
412    }
413}
414
415impl SphericalCoordinates {
416    /// [Converts] from Cartesian coordinates to spherical coordinates.
417    ///
418    /// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
419    fn from_cartesian(p: Vec3) -> SphericalCoordinates {
420        let radius = p.length();
421        SphericalCoordinates {
422            radius,
423            inclination: acos(p.y / radius),
424            azimuth: atan2(p.z, p.x),
425        }
426    }
427
428    /// [Converts] from spherical coordinates to Cartesian coordinates.
429    ///
430    /// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
431    fn to_cartesian(self) -> Vec3 {
432        let (sin_inclination, cos_inclination) = sin_cos(self.inclination);
433        let (sin_azimuth, cos_azimuth) = sin_cos(self.azimuth);
434        self.radius
435            * vec3(
436                sin_inclination * cos_azimuth,
437                cos_inclination,
438                sin_inclination * sin_azimuth,
439            )
440    }
441}