diff --git a/Cargo.toml b/Cargo.toml index 54f99d37b5a77..224d5e34fa33c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -257,6 +257,7 @@ common_api = [ "bevy_anti_alias", "bevy_gltf", "bevy_pbr", + "bevy_render_debug", "bevy_post_process", "gltf_animation", ] @@ -323,6 +324,9 @@ bevy_gltf = ["bevy_internal/bevy_gltf"] # Adds PBR rendering bevy_pbr = ["bevy_internal/bevy_pbr"] +# Provides debug visualization for the PBR render pipeline +bevy_render_debug = ["bevy_internal/bevy_render_debug"] + # Provides picking functionality bevy_picking = ["bevy_internal/bevy_picking"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1b16fd9de4e85..6fb74fc8b19e5 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -246,6 +246,7 @@ bevy_pbr = [ "bevy_core_pipeline", "bevy_gizmos_render?/bevy_pbr", ] +bevy_render_debug = ["dep:bevy_render_debug", "bevy_pbr"] bevy_sprite_render = [ "dep:bevy_sprite_render", "bevy_sprite", @@ -507,6 +508,7 @@ bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0 "bevy_reflect", ] } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.18.0-dev" } +bevy_render_debug = { path = "../bevy_render_debug", optional = true, version = "0.18.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.18.0-dev" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.18.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.18.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index f6b00d36017c6..51fc52fcae10f 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -67,6 +67,8 @@ plugin_group! { bevy_ui_render:::UiRenderPlugin, #[cfg(feature = "bevy_pbr")] bevy_pbr:::PbrPlugin, + #[cfg(feature = "bevy_render_debug")] + bevy_render_debug:::RenderDebugOverlayPlugin, // NOTE: Load this after renderer initialization so that it knows about the supported // compressed texture formats. #[cfg(feature = "bevy_gltf")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index d1dab307fa7b6..37371ac62a405 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -74,6 +74,8 @@ pub use bevy_reflect as reflect; pub use bevy_remote as remote; #[cfg(feature = "bevy_render")] pub use bevy_render as render; +#[cfg(feature = "bevy_render_debug")] +pub use bevy_render_debug as render_debug; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; #[cfg(feature = "bevy_shader")] diff --git a/crates/bevy_render_debug/Cargo.toml b/crates/bevy_render_debug/Cargo.toml new file mode 100644 index 0000000000000..0234b92ee130d --- /dev/null +++ b/crates/bevy_render_debug/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bevy_render_debug" +version = "0.18.0-dev" +edition = "2024" +description = "Adds debug visualization to Bevy Engine" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.18.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.18.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.18.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.18.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.18.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.18.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.18.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.18.0-dev" } +bevy_shader = { path = "../bevy_shader", version = "0.18.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" } + +[lints] +workspace = true diff --git a/crates/bevy_render_debug/src/debug_overlay.wgsl b/crates/bevy_render_debug/src/debug_overlay.wgsl new file mode 100644 index 0000000000000..45d99e78c7853 --- /dev/null +++ b/crates/bevy_render_debug/src/debug_overlay.wgsl @@ -0,0 +1,117 @@ +#import bevy_pbr::mesh_view_bindings::view +#import bevy_pbr::mesh_view_bindings::depth_prepass_texture +#import bevy_pbr::mesh_view_bindings::normal_prepass_texture +#import bevy_pbr::mesh_view_bindings::motion_vector_prepass_texture +#import bevy_pbr::mesh_view_bindings::deferred_prepass_texture +#import bevy_pbr::view_transformations::depth_ndc_to_view_z +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::pbr_deferred_types::unpack_unorm4x8_ +#import bevy_pbr::pbr_deferred_types::unpack_unorm3x4_plus_unorm_20_ +#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ +#import bevy_pbr::utils::octahedral_decode + +struct DebugBufferConfig { + opacity: f32, + mip_level: u32, +} + +@group(1) @binding(0) var config: DebugBufferConfig; +@group(1) @binding(1) var background_texture: texture_2d; +@group(1) @binding(2) var background_sampler: sampler; + +#ifdef DEBUG_DEPTH_PYRAMID +@group(1) @binding(3) var depth_pyramid_texture: texture_2d; +@group(1) @binding(4) var depth_pyramid_sampler: sampler; +#endif + +@fragment +fn fragment(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { + let uv = frag_coord.xy / view.viewport.zw; + let background = textureSampleLevel(background_texture, background_sampler, uv, 0.0); + var output_color: vec4 = vec4(0.0); + +#ifdef DEBUG_DEPTH + #ifdef DEPTH_PREPASS + let depth = textureLoad(depth_prepass_texture, vec2(frag_coord.xy), 0); + output_color = vec4(vec3(depth), 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_NORMAL + #ifdef NORMAL_PREPASS + let normal_sample = textureLoad(normal_prepass_texture, vec2(frag_coord.xy), 0); + output_color = vec4(normal_sample.xyz, 1.0); + #else + #ifdef DEFERRED_PREPASS + let deferred = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let normal = octahedral_decode(unpack_24bit_normal(deferred.a)); + output_color = vec4(normal * 0.5 + 0.5, 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif + #endif +#endif + +#ifdef DEBUG_MOTION_VECTORS + #ifdef MOTION_VECTOR_PREPASS + let motion_vector = textureLoad(motion_vector_prepass_texture, vec2(frag_coord.xy), 0).rg; + // These motion vectors are stored in a format where 1.0 represents full-screen movement. + // We use a power curve to amplify small movements while keeping them centered. + let mapped_motion = sign(motion_vector) * pow(abs(motion_vector), vec2(0.2)) * 0.5 + 0.5; + output_color = vec4(mapped_motion, 0.5, 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_DEFERRED + #ifdef DEFERRED_PREPASS + let deferred = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + output_color = vec4(vec3(f32(deferred.x) / 255.0, f32(deferred.y) / 255.0, f32(deferred.z) / 255.0), 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_DEFERRED_BASE_COLOR + #ifdef DEFERRED_PREPASS + let deferred = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let base_rough = unpack_unorm4x8_(deferred.x); + output_color = vec4(pow(base_rough.rgb, vec3(2.2)), 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_DEFERRED_EMISSIVE + #ifdef DEFERRED_PREPASS + let deferred = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let emissive = rgb9e5_to_vec3_(deferred.y); + output_color = vec4(emissive, 1.0); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_DEFERRED_METALLIC_ROUGHNESS + #ifdef DEFERRED_PREPASS + let deferred = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let base_rough = unpack_unorm4x8_(deferred.x); + let props = unpack_unorm4x8_(deferred.z); + // R: Reflectance, G: Metallic, B: Occlusion, A: Perceptual Roughness + output_color = vec4(props.r, props.g, props.b, base_rough.a); + #else + output_color = vec4(1.0, 0.0, 1.0, 1.0); + #endif +#endif + +#ifdef DEBUG_DEPTH_PYRAMID + let depth_pyramid = textureSampleLevel(depth_pyramid_texture, depth_pyramid_sampler, uv, f32(config.mip_level)).r; + output_color = vec4(vec3(depth_pyramid), 1.0); +#endif + + let alpha = output_color.a * config.opacity; + return vec4(mix(background.rgb, output_color.rgb, alpha), 1.0); +} diff --git a/crates/bevy_render_debug/src/lib.rs b/crates/bevy_render_debug/src/lib.rs new file mode 100644 index 0000000000000..5100b63f5b575 --- /dev/null +++ b/crates/bevy_render_debug/src/lib.rs @@ -0,0 +1,730 @@ +//! Renderer debugging overlay + +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, Handle}; +use bevy_core_pipeline::{ + mip_generation::experimental::depth::ViewDepthPyramid, + oit::OrderIndependentTransparencySettingsOffset, FullscreenShader, +}; +use bevy_ecs::{ + component::Component, + entity::Entity, + message::{Message, MessageReader, MessageWriter}, + prelude::{Has, ReflectComponent}, + query::QueryItem, + reflect::ReflectResource, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::BevyDefault; +use bevy_input::{prelude::KeyCode, ButtonInput}; +use bevy_log::info; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_graph::{ + NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types, BindGroupEntries, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, + DynamicUniformBuffer, FragmentState, Operations, PipelineCache, RenderPassColorAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerDescriptor, ShaderStages, + ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, + TextureSampleType, VertexState, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::FallbackImage, + view::{Msaa, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::Shader; + +use bevy_pbr::{ + MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, +}; + +/// Adds a rendering debug overlay to visualize various renderer buffers. +#[derive(Default)] +pub struct RenderDebugOverlayPlugin; + +impl Plugin for RenderDebugOverlayPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "debug_overlay.wgsl"); + + app.register_type::() + .init_resource::() + .add_message::() + .add_plugins(( + ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::default(), + )) + .add_systems(bevy_app::Update, (handle_input, update_overlay).chain()); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::>() + .init_resource::() + .add_render_graph_node::>( + bevy_core_pipeline::core_3d::graph::Core3d, + RenderDebugOverlayLabel, + ) + .add_render_graph_edge( + bevy_core_pipeline::core_3d::graph::Core3d, + bevy_core_pipeline::core_3d::graph::Node3d::Tonemapping, + RenderDebugOverlayLabel, + ) + .add_systems( + Render, + ( + prepare_debug_overlay_pipelines.in_set(RenderSystems::Prepare), + prepare_debug_overlay_resources.in_set(RenderSystems::PrepareResources), + ), + ); + } +} + +/// Automatically attach keybinds to make render debug overlays available to users without code +/// changes when the feature is enabled. +pub fn handle_input( + keyboard: Res>, + mut events: MessageWriter, +) { + if keyboard.just_pressed(KeyCode::F1) { + events.write(RenderDebugOverlayEvent::CycleMode); + } + if keyboard.just_pressed(KeyCode::F2) { + events.write(RenderDebugOverlayEvent::CycleOpacity); + } +} + +/// Listen to messages to update the debug overlay configuration. +pub fn update_overlay( + mut commands: Commands, + mut events: MessageReader, + mut config_res: ResMut, + cameras: Query< + ( + Entity, + Option<&RenderDebugOverlay>, + Has, + Has, + Has, + Has, + Has, + Has, + ), + bevy_ecs::query::With, + >, +) { + let mut changed = false; + + for event in events.read() { + match event { + RenderDebugOverlayEvent::CycleMode => { + let modes = [ + RenderDebugMode::Depth, + RenderDebugMode::Normal, + RenderDebugMode::MotionVectors, + RenderDebugMode::Deferred, + RenderDebugMode::DeferredBaseColor, + RenderDebugMode::DeferredEmissive, + RenderDebugMode::DeferredMetallicRoughness, + RenderDebugMode::DepthPyramid { mip_level: 0 }, + ]; + + let is_supported = |mode: &RenderDebugMode| { + cameras.iter().any( + |(_, _, depth, normal, motion, deferred, occlusion, _ssr)| { + match mode { + RenderDebugMode::Depth => depth || deferred, + RenderDebugMode::Normal => normal || deferred, + RenderDebugMode::MotionVectors => motion, + RenderDebugMode::Deferred + | RenderDebugMode::DeferredBaseColor + | RenderDebugMode::DeferredEmissive + | RenderDebugMode::DeferredMetallicRoughness => deferred, + // We don't have a good way to check for DepthPyramid in the main + // world, but it usually depends on DepthPrepass. + // However, we can at least check if OcclusionCulling is present. + RenderDebugMode::DepthPyramid { .. } => depth && occlusion, + } + }, + ) + }; + + if !config_res.enabled { + for mode in modes { + if is_supported(&mode) { + config_res.enabled = true; + config_res.mode = mode; + break; + } + } + } else { + let current_index = modes + .iter() + .position(|m| { + core::mem::discriminant(m) == core::mem::discriminant(&config_res.mode) + }) + .unwrap_or(0); + + let mut next_mode = None; + + if let RenderDebugMode::DepthPyramid { mip_level } = config_res.mode + && mip_level < 7 + { + config_res.mode = RenderDebugMode::DepthPyramid { + mip_level: mip_level + 1, + }; + next_mode = Some(config_res.mode); + } + + if next_mode.is_none() { + for i in 1..modes.len() { + let idx = (current_index + i) % modes.len(); + if is_supported(&modes[idx]) { + next_mode = Some(modes[idx]); + break; + } + } + + if let Some(mode) = next_mode { + let next_index = modes + .iter() + .position(|m| { + core::mem::discriminant(m) == core::mem::discriminant(&mode) + }) + .unwrap(); + + if next_index <= current_index { + config_res.enabled = false; + } else { + config_res.mode = mode; + } + } else { + config_res.enabled = false; + } + } + } + changed = true; + + if config_res.enabled { + info!("Debug Overlay: {:?}", config_res.mode); + } else { + info!("Debug Overlay Disabled"); + } + } + RenderDebugOverlayEvent::CycleOpacity => { + config_res.opacity = if config_res.opacity < 0.5 { + 0.5 + } else if config_res.opacity < 0.8 { + 0.8 + } else if config_res.opacity < 1.0 { + 1.0 + } else { + 0.5 + }; + changed = true; + info!("Debug Overlay Opacity: {}", config_res.opacity); + } + } + } + + for (entity, existing_config, ..) in &cameras { + if existing_config.is_none() || (changed && Some(config_res.as_ref()) != existing_config) { + commands.entity(entity).insert(config_res.clone()); + } + } +} + +/// Configure the render debug overlay. +#[derive(Message, Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect)] +#[reflect(Debug, PartialEq, Hash)] +pub enum RenderDebugOverlayEvent { + /// Cycle to the next debug mode. + CycleMode, + /// Cycle to the next opacity level. + CycleOpacity, +} + +/// Configure the render debug overlay. +#[derive(Resource, Component, Clone, ExtractResource, ExtractComponent, Reflect, PartialEq)] +#[reflect(Resource, Component, Default)] +pub struct RenderDebugOverlay { + /// Enables or disables drawing the overlay. + pub enabled: bool, + /// The kind of data to write to the overlay. + pub mode: RenderDebugMode, + /// The opacity of the overlay, to allow seeing the rendered image underneath. + pub opacity: f32, +} + +impl Default for RenderDebugOverlay { + fn default() -> Self { + Self { + enabled: false, + mode: RenderDebugMode::Depth, + opacity: 1.0, + } + } +} + +/// The kind of renderer data to visualize. +#[expect(missing_docs, reason = "Enum variants are self-explanatory")] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect)] +pub enum RenderDebugMode { + #[default] + Depth, + Normal, + MotionVectors, + Deferred, + DeferredBaseColor, + DeferredEmissive, + DeferredMetallicRoughness, + DepthPyramid { + mip_level: u32, + }, +} + +#[derive(ShaderType)] +struct RenderDebugOverlayUniform { + pub opacity: f32, + pub mip_level: u32, +} + +#[derive(Resource, Default)] +struct RenderDebugOverlayUniforms { + pub uniforms: DynamicUniformBuffer, +} + +#[derive(Component)] +struct RenderDebugOverlayUniformOffset { + pub offset: u32, +} + +#[derive(Resource)] +struct RenderDebugOverlayPipeline { + shader: Handle, + mesh_view_layouts: MeshPipelineViewLayouts, + bind_group_layout: BindGroupLayout, + bind_group_layout_descriptor: BindGroupLayoutDescriptor, + sampler: Sampler, + fullscreen_vertex_shader: VertexState, +} + +impl FromWorld for RenderDebugOverlayPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let asset_server = world.resource::(); + let mesh_view_layouts = world.resource::().clone(); + let fullscreen_vertex_shader = world.resource::().to_vertex_state(); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + let bind_group_layout_descriptor = BindGroupLayoutDescriptor::new( + "debug_overlay_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + binding_types::uniform_buffer::(true), + binding_types::texture_2d(TextureSampleType::Float { filterable: true }), + binding_types::sampler( + bevy_render::render_resource::SamplerBindingType::Filtering, + ), + binding_types::texture_2d(TextureSampleType::Float { filterable: true }), + binding_types::sampler( + bevy_render::render_resource::SamplerBindingType::Filtering, + ), + ), + ), + ); + + let bind_group_layout = render_device.create_bind_group_layout( + bind_group_layout_descriptor.label.as_ref(), + &bind_group_layout_descriptor.entries, + ); + + Self { + shader: asset_server.load("embedded://bevy_render_debug/debug_overlay.wgsl"), + mesh_view_layouts, + bind_group_layout, + bind_group_layout_descriptor, + sampler, + fullscreen_vertex_shader, + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct RenderDebugOverlayPipelineKey { + mode: RenderDebugMode, + view_layout_key: MeshPipelineViewLayoutKey, +} + +impl SpecializedRenderPipeline for RenderDebugOverlayPipeline { + type Key = RenderDebugOverlayPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + match key.mode { + RenderDebugMode::Depth => { + shader_defs.push("DEBUG_DEPTH".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEPTH_PREPASS) + { + shader_defs.push("DEPTH_PREPASS".into()); + } + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::Normal => { + shader_defs.push("DEBUG_NORMAL".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::NORMAL_PREPASS) + { + shader_defs.push("NORMAL_PREPASS".into()); + } + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::MotionVectors => { + shader_defs.push("DEBUG_MOTION_VECTORS".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS) + { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + } + RenderDebugMode::Deferred => { + shader_defs.push("DEBUG_DEFERRED".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::DeferredBaseColor => { + shader_defs.push("DEBUG_DEFERRED_BASE_COLOR".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::DeferredEmissive => { + shader_defs.push("DEBUG_DEFERRED_EMISSIVE".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::DeferredMetallicRoughness => { + shader_defs.push("DEBUG_DEFERRED_METALLIC_ROUGHNESS".into()); + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) + { + shader_defs.push("DEFERRED_PREPASS".into()); + } + } + RenderDebugMode::DepthPyramid { .. } => shader_defs.push("DEBUG_DEPTH_PYRAMID".into()), + } + + if key + .view_layout_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + } + + let mesh_view_layout_descriptor = self + .mesh_view_layouts + .get_view_layout(key.view_layout_key) + .main_layout + .clone(); + + RenderPipelineDescriptor { + label: Some("debug_overlay_pipeline".into()), + layout: vec![ + mesh_view_layout_descriptor, + self.bind_group_layout_descriptor.clone(), + ], + vertex: self.fullscreen_vertex_shader.clone(), + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs, + entry_point: Some("fragment".into()), + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: bevy_render::render_resource::PrimitiveState::default(), + depth_stencil: None, + multisample: bevy_render::render_resource::MultisampleState::default(), + push_constant_ranges: vec![], + zero_initialize_workgroup_memory: false, + } + } +} + +fn prepare_debug_overlay_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + views: Query<( + Entity, + &RenderDebugOverlay, + &Msaa, + Has, + Has, + Has, + Has, + Has, + Has, + )>, +) { + for ( + entity, + config, + msaa, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + has_oit, + has_atmosphere, + ) in &views + { + if !config.enabled { + continue; + } + + let mut view_layout_key = MeshPipelineViewLayoutKey::from(*msaa); + if depth_prepass { + view_layout_key |= MeshPipelineViewLayoutKey::DEPTH_PREPASS; + } + if normal_prepass { + view_layout_key |= MeshPipelineViewLayoutKey::NORMAL_PREPASS; + } + if motion_vector_prepass { + view_layout_key |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS; + } + if deferred_prepass { + view_layout_key |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + } + if has_oit { + view_layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + if has_atmosphere { + view_layout_key |= MeshPipelineViewLayoutKey::ATMOSPHERE; + } + + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &pipeline, + RenderDebugOverlayPipelineKey { + mode: config.mode, + view_layout_key, + }, + ); + + commands + .entity(entity) + .insert(RenderDebugOverlayPipelineId(pipeline_id)); + } +} + +#[derive(Component)] +struct RenderDebugOverlayPipelineId(CachedRenderPipelineId); + +fn prepare_debug_overlay_resources( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut uniforms: ResMut, + views: Query<(Entity, &RenderDebugOverlay)>, +) { + let len = views.iter().len(); + if len == 0 { + return; + } + + let Some(mut writer) = uniforms + .uniforms + .get_writer(len, &render_device, &render_queue) + else { + return; + }; + + for (entity, config) in &views { + let offset = writer.write(&RenderDebugOverlayUniform { + opacity: config.opacity, + mip_level: if let RenderDebugMode::DepthPyramid { mip_level } = config.mode { + mip_level + } else { + 0 + }, + }); + + commands + .entity(entity) + .insert(RenderDebugOverlayUniformOffset { offset }); + } +} + +/// The render debug overlay. +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct RenderDebugOverlayLabel; + +#[derive(Default)] +struct RenderDebugOverlayNode; + +impl ViewNode for RenderDebugOverlayNode { + type ViewQuery = ( + &'static ViewTarget, + &'static RenderDebugOverlay, + &'static RenderDebugOverlayPipelineId, + &'static RenderDebugOverlayUniformOffset, + &'static MeshViewBindGroup, + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, + Has, + Option<&'static OrderIndependentTransparencySettingsOffset>, + Option<&'static ViewDepthPyramid>, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + target, + config, + pipeline_id, + uniform_offset, + mesh_view_bind_group, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + has_oit, + view_oit_offset, + depth_pyramid, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if !config.enabled { + return Ok(()); + } + + let pipeline_cache = world.resource::(); + let pipeline_res = world.resource::(); + let uniforms = world.resource::(); + let fallback_image = world.resource::(); + + let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else { + return Ok(()); + }; + + let Some(uniform_binding) = uniforms.uniforms.binding() else { + return Ok(()); + }; + + let post_process = target.post_process_write(); + + let depth_pyramid_view = if let Some(dp) = depth_pyramid { + &dp.all_mips + } else { + &fallback_image.d2.texture_view + }; + + let debug_bind_group = render_context.render_device().create_bind_group( + "debug_buffer_bind_group", + &pipeline_res.bind_group_layout, + &BindGroupEntries::sequential(( + uniform_binding, + post_process.source, + &pipeline_res.sampler, + depth_pyramid_view, + &pipeline_res.sampler, + )), + ); + + let pass_descriptor = RenderPassDescriptor { + label: Some("debug_buffer_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: bevy_render::render_resource::LoadOp::Clear(Default::default()), + store: bevy_render::render_resource::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + + let mut dynamic_offsets = vec![ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ]; + if has_oit && let Some(view_oit_offset) = view_oit_offset { + dynamic_offsets.push(view_oit_offset.offset); + } + + render_pass.set_bind_group(0, &mesh_view_bind_group.main, &dynamic_offsets); + render_pass.set_bind_group(1, &debug_bind_group, &[uniform_offset.offset]); + + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 231038b9e1f31..4fc7c405ea892 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -91,6 +91,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |bevy_post_process|Provides post process effects such as depth of field, bloom, chromatic aberration.| |bevy_remote|Enable the Bevy Remote Protocol| |bevy_render|Provides rendering functionality| +|bevy_render_debug|Provides debug visualization for the PBR render pipeline| |bevy_scene|Provides scene functionality| |bevy_shader|Provides shaders usable through asset handles.| |bevy_solari|Provides raytraced lighting (experimental)|