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::ViewDepthStencilTexture`] 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::ViewDepthStencilTexture,
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.wesl";
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, &ViewDepthStencilTexture)>,
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        // Aspect for `copy_texture_to_texture` should refer to all aspect.
155        // See <https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-copytexturetotexture>
156        TexelCopyTextureInfo {
157            texture: depth_texture.texture(),
158            mip_level: 0,
159            origin: Origin3d::default(),
160            aspect: TextureAspect::All,
161        },
162        TexelCopyTextureInfo {
163            texture: &demo_depth_image.texture,
164            mip_level: 0,
165            origin: Origin3d::default(),
166            aspect: TextureAspect::All,
167        },
168        Extent3d {
169            width: DEPTH_TEXTURE_SIZE,
170            height: DEPTH_TEXTURE_SIZE,
171            depth_or_array_layers: 1,
172        },
173    );
174    command_encoder.pop_debug_group();
175}
176
177/// Creates the scene.
178fn setup(
179    mut commands: Commands,
180    mut meshes: ResMut<Assets<Mesh>>,
181    mut standard_materials: ResMut<Assets<StandardMaterial>>,
182    mut show_depth_texture_materials: ResMut<Assets<ShowDepthTextureMaterial>>,
183    demo_depth_texture: Res<DemoDepthTexture>,
184) {
185    spawn_rotating_cube(&mut commands, &mut meshes, &mut standard_materials);
186    spawn_plane(
187        &mut commands,
188        &mut meshes,
189        &mut show_depth_texture_materials,
190        &demo_depth_texture,
191    );
192    spawn_light(&mut commands);
193    spawn_depth_only_camera(&mut commands);
194    spawn_main_camera(&mut commands);
195    spawn_instructions(&mut commands);
196}
197
198/// Spawns the main rotating cube.
199fn spawn_rotating_cube(
200    commands: &mut Commands,
201    meshes: &mut Assets<Mesh>,
202    standard_materials: &mut Assets<StandardMaterial>,
203) {
204    let cube_handle = meshes.add(Cuboid::new(3.0, 3.0, 3.0));
205    let rotating_cube_material_handle = standard_materials.add(StandardMaterial {
206        base_color: Color::WHITE,
207        unlit: false,
208        ..default()
209    });
210    commands.spawn((
211        Mesh3d(cube_handle.clone()),
212        MeshMaterial3d(rotating_cube_material_handle),
213        Transform::IDENTITY,
214        RotatingCube,
215    ));
216}
217
218// Spawns the plane that shows the depth texture.
219fn spawn_plane(
220    commands: &mut Commands,
221    meshes: &mut Assets<Mesh>,
222    show_depth_texture_materials: &mut Assets<ShowDepthTextureMaterial>,
223    demo_depth_texture: &DemoDepthTexture,
224) {
225    let plane_handle = meshes.add(Plane3d::new(Vec3::Z, Vec2::splat(2.0)));
226    let show_depth_texture_material = show_depth_texture_materials.add(ShowDepthTextureMaterial {
227        depth_texture: Some(demo_depth_texture.0.clone()),
228    });
229    commands.spawn((
230        Mesh3d(plane_handle),
231        MeshMaterial3d(show_depth_texture_material),
232        Transform::from_xyz(10.0, 4.0, 0.0).with_scale(Vec3::splat(2.5)),
233    ));
234}
235
236/// Spawns a light.
237fn spawn_light(commands: &mut Commands) {
238    commands.spawn((PointLight::default(), Transform::from_xyz(5.0, 6.0, 7.0)));
239}
240
241/// Spawns the depth-only camera.
242fn spawn_depth_only_camera(commands: &mut Commands) {
243    commands.spawn((
244        Camera3d::default(),
245        Transform::from_xyz(-4.0, -5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
246        Camera {
247            // Make sure that we render from this depth-only camera *before*
248            // rendering from the main camera.
249            order: -1,
250            ..Camera::default()
251        },
252        // We specify no color render target, for maximum efficiency.
253        RenderTarget::None {
254            // When specifying no render target, we must manually specify
255            // the viewport size. Otherwise, Bevy won't know how big to make
256            // the depth buffer.
257            size: UVec2::splat(DEPTH_TEXTURE_SIZE),
258        },
259        // We need to disable multisampling or the depth texture will be
260        // multisampled, which adds complexity we don't care about for this
261        // demo.
262        Msaa::Off,
263        // Cameras with no render target render *nothing* by default. To get
264        // them to render something, we must add a prepass that specifies what
265        // we want to render: in this case, depth.
266        DepthPrepass,
267    ));
268}
269
270/// Spawns the main camera that renders to the window.
271fn spawn_main_camera(commands: &mut Commands) {
272    commands.spawn((
273        Camera3d::default(),
274        Transform::from_xyz(5.0, 2.0, 30.0).looking_at(vec3(5.0, 2.0, 0.0), Vec3::Y),
275        // Disable antialiasing just for simplicity's sake.
276        Msaa::Off,
277    ));
278}
279
280/// Spawns the instructional text at the top of the screen.
281fn spawn_instructions(commands: &mut Commands) {
282    commands.spawn((
283        Text::new("Use WASD to move the secondary camera"),
284        Node {
285            position_type: PositionType::Absolute,
286            top: px(12.0),
287            left: px(12.0),
288            ..Node::default()
289        },
290    ));
291}
292
293/// Spins the cube a bit every frame.
294fn rotate_cube(mut cubes: Query<&mut Transform, With<RotatingCube>>, time: Res<Time>) {
295    for mut transform in &mut cubes {
296        transform.rotate_x(1.5 * time.delta_secs());
297        transform.rotate_y(1.1 * time.delta_secs());
298        transform.rotate_z(-1.3 * time.delta_secs());
299    }
300}
301
302impl Material for ShowDepthTextureMaterial {
303    fn fragment_shader() -> ShaderRef {
304        SHADER_ASSET_PATH.into()
305    }
306}
307
308impl FromWorld for DemoDepthTexture {
309    fn from_world(world: &mut World) -> Self {
310        let mut images = world.resource_mut::<Assets<Image>>();
311
312        // Create a new 32-bit floating point depth texture.
313        let mut depth_image = Image::new_uninit(
314            Extent3d {
315                width: DEPTH_TEXTURE_SIZE,
316                height: DEPTH_TEXTURE_SIZE,
317                depth_or_array_layers: 1,
318            },
319            TextureDimension::D2,
320            TextureFormat::Depth32Float,
321            RenderAssetUsages::default(),
322        );
323
324        // Create a sampler. Note that this needs to specify a `compare`
325        // function in order to be compatible with depth textures.
326        depth_image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
327            label: Some("custom depth image sampler".to_owned()),
328            compare: Some(ImageCompareFunction::Always),
329            ..ImageSamplerDescriptor::default()
330        });
331
332        let depth_image_handle = images.add(depth_image);
333        DemoDepthTexture(depth_image_handle)
334    }
335}
336
337impl ExtractResource<RenderApp> for DemoDepthTexture {
338    type Source = Self;
339
340    fn extract_resource(source: &Self::Source) -> Self {
341        // Share the `DemoDepthTexture` resource over to the render world so
342        // that our system can access it.
343        (*source).clone()
344    }
345}
346
347/// Draws an outline of the depth texture on the screen.
348fn draw_camera_gizmo(cameras: Query<(&Camera, &GlobalTransform)>, mut gizmos: Gizmos) {
349    for (camera, transform) in &cameras {
350        // As above, we use the order as a cheap tag to tell the depth texture
351        // apart from the main texture.
352        if camera.order >= 0 {
353            continue;
354        }
355
356        // Draw a cone representing the camera.
357        gizmos.primitive_3d(
358            &Cone {
359                radius: 1.0,
360                height: 3.0,
361            },
362            Isometry3d::new(
363                transform.translation(),
364                // We have to rotate here because `Cone` primitives are oriented
365                // along +Y and cameras point along +Z.
366                transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
367            ),
368            LIME,
369        );
370    }
371}
372
373/// Orbits the cube when WASD is pressed.
374fn move_camera(
375    mut cameras: Query<(&Camera, &mut Transform)>,
376    keyboard: Res<ButtonInput<KeyCode>>,
377    time: Res<Time>,
378) {
379    for (camera, mut transform) in &mut cameras {
380        // Only affect the depth camera.
381        if camera.order >= 0 {
382            continue;
383        }
384
385        // Convert the camera's position from Cartesian to spherical coordinates.
386        let mut spherical_coords = SphericalCoordinates::from_cartesian(transform.translation);
387
388        // Modify those spherical coordinates as appropriate.
389        let mut changed = false;
390        if keyboard.pressed(KeyCode::KeyW) {
391            spherical_coords.inclination -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;
392            changed = true;
393        }
394        if keyboard.pressed(KeyCode::KeyS) {
395            spherical_coords.inclination += time.delta_secs() * CAMERA_MOVEMENT_SPEED;
396            changed = true;
397        }
398        if keyboard.pressed(KeyCode::KeyA) {
399            spherical_coords.azimuth += time.delta_secs() * CAMERA_MOVEMENT_SPEED;
400            changed = true;
401        }
402        if keyboard.pressed(KeyCode::KeyD) {
403            spherical_coords.azimuth -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;
404            changed = true;
405        }
406
407        // If they were changed, convert from spherical coordinates back to
408        // Cartesian ones, and update the camera's transform.
409        if changed {
410            spherical_coords.inclination = spherical_coords.inclination.clamp(0.01, PI - 0.01);
411            transform.translation = spherical_coords.to_cartesian();
412            transform.look_at(Vec3::ZERO, Vec3::Y);
413        }
414    }
415}
416
417impl SphericalCoordinates {
418    /// [Converts] from Cartesian coordinates to spherical coordinates.
419    ///
420    /// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
421    fn from_cartesian(p: Vec3) -> SphericalCoordinates {
422        let radius = p.length();
423        SphericalCoordinates {
424            radius,
425            inclination: acos(p.y / radius),
426            azimuth: atan2(p.z, p.x),
427        }
428    }
429
430    /// [Converts] from spherical coordinates to Cartesian coordinates.
431    ///
432    /// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
433    fn to_cartesian(self) -> Vec3 {
434        let (sin_inclination, cos_inclination) = sin_cos(self.inclination);
435        let (sin_azimuth, cos_azimuth) = sin_cos(self.azimuth);
436        self.radius
437            * vec3(
438                sin_inclination * cos_azimuth,
439                cos_inclination,
440                sin_inclination * sin_azimuth,
441            )
442    }
443}