Path: blob/main/examples/shader_advanced/render_depth_to_texture.rs
9341 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 system to the Core3d schedule 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::{prepass::DepthPrepass, schedule::Core3d, Core3dSystems},24image::{ImageCompareFunction, ImageSampler, ImageSamplerDescriptor},25math::ops::{acos, atan2, sin_cos},26prelude::*,27render::{28camera::ExtractedCamera,29extract_resource::{ExtractResource, ExtractResourcePlugin},30render_asset::RenderAssets,31render_resource::{32AsBindGroup, Extent3d, Origin3d, TexelCopyTextureInfo, TextureAspect, TextureDimension,33TextureFormat,34},35renderer::{RenderContext, ViewQuery},36texture::GpuImage,37view::ViewDepthTexture,38RenderApp,39},40shader::ShaderRef,41};4243/// A marker component for a rotating cube.44#[derive(Component)]45struct RotatingCube;4647/// The material that displays the contents of the depth buffer.48///49/// This material is placed on the plane.50#[derive(Clone, Debug, Asset, TypePath, AsBindGroup)]51struct ShowDepthTextureMaterial {52/// A copy of the depth texture that the depth-only camera produced.53#[texture(0, sample_type = "depth")]54#[sampler(1, sampler_type = "comparison")]55depth_texture: Option<Handle<Image>>,56}5758/// Holds a copy of the depth buffer that the depth-only camera produces.59///60/// We need to make a copy for two reasons:61///62/// 1. The Bevy renderer automatically creates and maintains depth buffers on63/// its own. There's no mechanism to fetch the depth buffer for a camera outside64/// the render app. Thus it can't easily be attached to a material.65///66/// 2. `wgpu` doesn't allow applications to simultaneously render to and sample67/// from a standard depth texture, so a copy must be made regardless.68#[derive(Clone, Resource)]69struct DemoDepthTexture(Handle<Image>);7071/// [Spherical coordinates], used to implement the camera orbiting72/// functionality.73///74/// Note that these are in the mathematics convention, not the physics75/// convention. In a real application, one would probably use the physics76/// convention, but for familiarity's sake we stick to the most common77/// convention here.78///79/// [Spherical coordinates]: https://en.wikipedia.org/wiki/Spherical_coordinate_system80#[derive(Clone, Copy, Debug)]81struct SphericalCoordinates {82/// The radius, in world units.83radius: f32,84/// The elevation angle (latitude).85inclination: f32,86/// The azimuth angle (longitude).87azimuth: f32,88}8990/// The path to the shader that renders the depth texture.91static SHADER_ASSET_PATH: &str = "shaders/show_depth_texture_material.wgsl";9293/// The size in texels of a depth texture.94const DEPTH_TEXTURE_SIZE: u32 = 256;9596/// The rate at which the user can move the camera, in radians per second.97const CAMERA_MOVEMENT_SPEED: f32 = 2.0;9899/// The entry point.100fn main() {101let mut app = App::new();102103app.add_plugins(DefaultPlugins)104.add_plugins(MaterialPlugin::<ShowDepthTextureMaterial>::default())105.add_plugins(ExtractResourcePlugin::<DemoDepthTexture>::default())106.init_resource::<DemoDepthTexture>()107.add_systems(Startup, setup)108.add_systems(Update, rotate_cube)109.add_systems(Update, draw_camera_gizmo)110.add_systems(Update, move_camera);111112let render_app = app113.get_sub_app_mut(RenderApp)114.expect("Render app should be present");115116render_app.add_systems(117Core3d,118copy_depth_texture_system119.after(Core3dSystems::Prepass)120.before(Core3dSystems::MainPass),121);122123app.run();124}125126fn copy_depth_texture_system(127view: ViewQuery<(&ExtractedCamera, &ViewDepthTexture)>,128demo_depth_texture: Option<Res<DemoDepthTexture>>,129image_assets: Res<RenderAssets<GpuImage>>,130mut ctx: RenderContext,131) {132let Some(demo_depth_texture) = demo_depth_texture else {133return;134};135136let (camera, depth_texture) = view.into_inner();137138// Make sure we only run on the depth-only camera.139// We could make a marker component for that camera and extract it to140// the render world, but using `order` as a tag to tell the main camera141// and the depth-only camera apart works in a pinch.142if camera.order >= 0 {143return;144}145146let Some(demo_depth_image) = image_assets.get(demo_depth_texture.0.id()) else {147return;148};149150let command_encoder = ctx.command_encoder();151command_encoder.push_debug_group("copy depth to demo texture");152command_encoder.copy_texture_to_texture(153TexelCopyTextureInfo {154texture: &depth_texture.texture,155mip_level: 0,156origin: Origin3d::default(),157aspect: TextureAspect::DepthOnly,158},159TexelCopyTextureInfo {160texture: &demo_depth_image.texture,161mip_level: 0,162origin: Origin3d::default(),163aspect: TextureAspect::DepthOnly,164},165Extent3d {166width: DEPTH_TEXTURE_SIZE,167height: DEPTH_TEXTURE_SIZE,168depth_or_array_layers: 1,169},170);171command_encoder.pop_debug_group();172}173174/// Creates the scene.175fn setup(176mut commands: Commands,177mut meshes: ResMut<Assets<Mesh>>,178mut standard_materials: ResMut<Assets<StandardMaterial>>,179mut show_depth_texture_materials: ResMut<Assets<ShowDepthTextureMaterial>>,180demo_depth_texture: Res<DemoDepthTexture>,181) {182spawn_rotating_cube(&mut commands, &mut meshes, &mut standard_materials);183spawn_plane(184&mut commands,185&mut meshes,186&mut show_depth_texture_materials,187&demo_depth_texture,188);189spawn_light(&mut commands);190spawn_depth_only_camera(&mut commands);191spawn_main_camera(&mut commands);192spawn_instructions(&mut commands);193}194195/// Spawns the main rotating cube.196fn spawn_rotating_cube(197commands: &mut Commands,198meshes: &mut Assets<Mesh>,199standard_materials: &mut Assets<StandardMaterial>,200) {201let cube_handle = meshes.add(Cuboid::new(3.0, 3.0, 3.0));202let rotating_cube_material_handle = standard_materials.add(StandardMaterial {203base_color: Color::WHITE,204unlit: false,205..default()206});207commands.spawn((208Mesh3d(cube_handle.clone()),209MeshMaterial3d(rotating_cube_material_handle),210Transform::IDENTITY,211RotatingCube,212));213}214215// Spawns the plane that shows the depth texture.216fn spawn_plane(217commands: &mut Commands,218meshes: &mut Assets<Mesh>,219show_depth_texture_materials: &mut Assets<ShowDepthTextureMaterial>,220demo_depth_texture: &DemoDepthTexture,221) {222let plane_handle = meshes.add(Plane3d::new(Vec3::Z, Vec2::splat(2.0)));223let show_depth_texture_material = show_depth_texture_materials.add(ShowDepthTextureMaterial {224depth_texture: Some(demo_depth_texture.0.clone()),225});226commands.spawn((227Mesh3d(plane_handle),228MeshMaterial3d(show_depth_texture_material),229Transform::from_xyz(10.0, 4.0, 0.0).with_scale(Vec3::splat(2.5)),230));231}232233/// Spawns a light.234fn spawn_light(commands: &mut Commands) {235commands.spawn((PointLight::default(), Transform::from_xyz(5.0, 6.0, 7.0)));236}237238/// Spawns the depth-only camera.239fn spawn_depth_only_camera(commands: &mut Commands) {240commands.spawn((241Camera3d::default(),242Transform::from_xyz(-4.0, -5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),243Camera {244// Make sure that we render from this depth-only camera *before*245// rendering from the main camera.246order: -1,247..Camera::default()248},249// We specify no color render target, for maximum efficiency.250RenderTarget::None {251// When specifying no render target, we must manually specify252// the viewport size. Otherwise, Bevy won't know how big to make253// the depth buffer.254size: UVec2::splat(DEPTH_TEXTURE_SIZE),255},256// We need to disable multisampling or the depth texture will be257// multisampled, which adds complexity we don't care about for this258// demo.259Msaa::Off,260// Cameras with no render target render *nothing* by default. To get261// them to render something, we must add a prepass that specifies what262// we want to render: in this case, depth.263DepthPrepass,264));265}266267/// Spawns the main camera that renders to the window.268fn spawn_main_camera(commands: &mut Commands) {269commands.spawn((270Camera3d::default(),271Transform::from_xyz(5.0, 2.0, 30.0).looking_at(vec3(5.0, 2.0, 0.0), Vec3::Y),272// Disable antialiasing just for simplicity's sake.273Msaa::Off,274));275}276277/// Spawns the instructional text at the top of the screen.278fn spawn_instructions(commands: &mut Commands) {279commands.spawn((280Text::new("Use WASD to move the secondary camera"),281Node {282position_type: PositionType::Absolute,283top: px(12.0),284left: px(12.0),285..Node::default()286},287));288}289290/// Spins the cube a bit every frame.291fn rotate_cube(mut cubes: Query<&mut Transform, With<RotatingCube>>, time: Res<Time>) {292for mut transform in &mut cubes {293transform.rotate_x(1.5 * time.delta_secs());294transform.rotate_y(1.1 * time.delta_secs());295transform.rotate_z(-1.3 * time.delta_secs());296}297}298299impl Material for ShowDepthTextureMaterial {300fn fragment_shader() -> ShaderRef {301SHADER_ASSET_PATH.into()302}303}304305impl FromWorld for DemoDepthTexture {306fn from_world(world: &mut World) -> Self {307let mut images = world.resource_mut::<Assets<Image>>();308309// Create a new 32-bit floating point depth texture.310let mut depth_image = Image::new_uninit(311Extent3d {312width: DEPTH_TEXTURE_SIZE,313height: DEPTH_TEXTURE_SIZE,314depth_or_array_layers: 1,315},316TextureDimension::D2,317TextureFormat::Depth32Float,318RenderAssetUsages::default(),319);320321// Create a sampler. Note that this needs to specify a `compare`322// function in order to be compatible with depth textures.323depth_image.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {324label: Some("custom depth image sampler".to_owned()),325compare: Some(ImageCompareFunction::Always),326..ImageSamplerDescriptor::default()327});328329let depth_image_handle = images.add(depth_image);330DemoDepthTexture(depth_image_handle)331}332}333334impl ExtractResource for DemoDepthTexture {335type Source = Self;336337fn extract_resource(source: &Self::Source) -> Self {338// Share the `DemoDepthTexture` resource over to the render world so339// that our system can access it.340(*source).clone()341}342}343344/// Draws an outline of the depth texture on the screen.345fn draw_camera_gizmo(cameras: Query<(&Camera, &GlobalTransform)>, mut gizmos: Gizmos) {346for (camera, transform) in &cameras {347// As above, we use the order as a cheap tag to tell the depth texture348// apart from the main texture.349if camera.order >= 0 {350continue;351}352353// Draw a cone representing the camera.354gizmos.primitive_3d(355&Cone {356radius: 1.0,357height: 3.0,358},359Isometry3d::new(360transform.translation(),361// We have to rotate here because `Cone` primitives are oriented362// along +Y and cameras point along +Z.363transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),364),365LIME,366);367}368}369370/// Orbits the cube when WASD is pressed.371fn move_camera(372mut cameras: Query<(&Camera, &mut Transform)>,373keyboard: Res<ButtonInput<KeyCode>>,374time: Res<Time>,375) {376for (camera, mut transform) in &mut cameras {377// Only affect the depth camera.378if camera.order >= 0 {379continue;380}381382// Convert the camera's position from Cartesian to spherical coordinates.383let mut spherical_coords = SphericalCoordinates::from_cartesian(transform.translation);384385// Modify those spherical coordinates as appropriate.386let mut changed = false;387if keyboard.pressed(KeyCode::KeyW) {388spherical_coords.inclination -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;389changed = true;390}391if keyboard.pressed(KeyCode::KeyS) {392spherical_coords.inclination += time.delta_secs() * CAMERA_MOVEMENT_SPEED;393changed = true;394}395if keyboard.pressed(KeyCode::KeyA) {396spherical_coords.azimuth += time.delta_secs() * CAMERA_MOVEMENT_SPEED;397changed = true;398}399if keyboard.pressed(KeyCode::KeyD) {400spherical_coords.azimuth -= time.delta_secs() * CAMERA_MOVEMENT_SPEED;401changed = true;402}403404// If they were changed, convert from spherical coordinates back to405// Cartesian ones, and update the camera's transform.406if changed {407spherical_coords.inclination = spherical_coords.inclination.clamp(0.01, PI - 0.01);408transform.translation = spherical_coords.to_cartesian();409transform.look_at(Vec3::ZERO, Vec3::Y);410}411}412}413414impl SphericalCoordinates {415/// [Converts] from Cartesian coordinates to spherical coordinates.416///417/// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates418fn from_cartesian(p: Vec3) -> SphericalCoordinates {419let radius = p.length();420SphericalCoordinates {421radius,422inclination: acos(p.y / radius),423azimuth: atan2(p.z, p.x),424}425}426427/// [Converts] from spherical coordinates to Cartesian coordinates.428///429/// [Converts]: https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates430fn to_cartesian(self) -> Vec3 {431let (sin_inclination, cos_inclination) = sin_cos(self.inclination);432let (sin_azimuth, cos_azimuth) = sin_cos(self.azimuth);433self.radius434* vec3(435sin_inclination * cos_azimuth,436cos_inclination,437sin_inclination * sin_azimuth,438)439}440}441442443