diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index b295b4bb1cc89..21dc970a393c5 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -2,15 +2,18 @@ use crate::{ clear_color::{ClearColor, ClearColorConfig}, core_3d::{Camera3d, Opaque3d}, prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass}, + skybox::{SkyboxBindGroup, SkyboxPipelineId}, }; use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, - render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor}, + render_resource::{ + LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, + }, renderer::RenderContext, - view::{ExtractedView, ViewDepthTexture, ViewTarget}, + view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset}, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -30,6 +33,9 @@ pub struct MainOpaquePass3dNode { Option<&'static DepthPrepass>, Option<&'static NormalPrepass>, Option<&'static MotionVectorPrepass>, + Option<&'static SkyboxPipelineId>, + Option<&'static SkyboxBindGroup>, + &'static ViewUniformOffset, ), With, >, @@ -64,7 +70,10 @@ impl Node for MainOpaquePass3dNode { depth, depth_prepass, normal_prepass, - motion_vector_prepass + motion_vector_prepass, + skybox_pipeline, + skybox_bind_group, + view_uniform_offset, )) = self.query.get_manual(world, view_entity) else { // No window return Ok(()); @@ -75,6 +84,7 @@ impl Node for MainOpaquePass3dNode { #[cfg(feature = "trace")] let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); + // Setup render pass let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_opaque_pass_3d"), // NOTE: The opaque pass loads the color @@ -115,12 +125,26 @@ impl Node for MainOpaquePass3dNode { render_pass.set_camera_viewport(viewport); } + // Opaque draws opaque_phase.render(&mut render_pass, world, view_entity); + // Alpha draws if !alpha_mask_phase.items.is_empty() { alpha_mask_phase.render(&mut render_pass, world, view_entity); } + // Draw the skybox using a fullscreen triangle + if let (Some(skybox_pipeline), Some(skybox_bind_group)) = + (skybox_pipeline, skybox_bind_group) + { + let pipeline_cache = world.resource::(); + if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) { + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]); + render_pass.draw(0..3, 0..1); + } + } + Ok(()) } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 4f1f807477a26..0a4e5d3834085 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -52,6 +52,7 @@ use bevy_utils::{FloatOrd, HashMap}; use crate::{ prepass::{node::PrepassNode, DepthPrepass}, + skybox::SkyboxPlugin, tonemapping::TonemappingNode, upscaling::UpscalingNode, }; @@ -62,6 +63,7 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() + .add_plugin(SkyboxPlugin) .add_plugin(ExtractComponentPlugin::::default()); let render_app = match app.get_sub_app_mut(RenderApp) { diff --git a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/fullscreen.wgsl b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/fullscreen.wgsl index bc328269ab48c..04c3c494438ab 100644 --- a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/fullscreen.wgsl +++ b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/fullscreen.wgsl @@ -7,8 +7,26 @@ struct FullscreenVertexOutput { uv: vec2, }; +// This vertex shader produces the following, when drawn using indices 0..3: +// +// 1 | 0-----x.....2 +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | 1´ +// +--------------- +// -1 0 1 2 3 +// +// The axes are clip-space x and y. The region marked s is the visible region. +// The digits in the corners of the right-angled triangle are the vertex +// indices. +// +// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0. +// This means that the UV gets interpolated to 1,1 at the bottom-right corner +// of the clip-space rectangle that is at 1,-1 in clip space. @vertex fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput { + // See the explanation above for how this works let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; let clip_position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index b7692a2e51e3d..cc96506c1de4e 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -7,10 +7,13 @@ pub mod fullscreen_vertex_shader; pub mod fxaa; pub mod msaa_writeback; pub mod prepass; +mod skybox; mod taa; pub mod tonemapping; pub mod upscaling; +pub use skybox::Skybox; + /// Experimental features that are not yet finished. Please report any issues you encounter! pub mod experimental { pub mod taa { diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs new file mode 100644 index 0000000000000..6d805e0478cb5 --- /dev/null +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -0,0 +1,238 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Handle, HandleUntyped}; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::With, + schedule::IntoSystemConfigs, + system::{Commands, Query, Res, ResMut, Resource}, +}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::RenderAssets, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType, + CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, + DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState, + RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState, + TextureFormat, TextureSampleType, TextureViewDimension, VertexState, + }, + renderer::RenderDevice, + texture::{BevyDefault, Image}, + view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms}, + Render, RenderApp, RenderSet, +}; + +const SKYBOX_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 55594763423201); + +pub struct SkyboxPlugin; + +impl Plugin for SkyboxPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl); + + app.add_plugin(ExtractComponentPlugin::::default()); + + let render_app = match app.get_sub_app_mut(RenderApp) { + Ok(render_app) => render_app, + Err(_) => return, + }; + + let render_device = render_app.world.resource::().clone(); + + render_app + .insert_resource(SkyboxPipeline::new(&render_device)) + .init_resource::>() + .add_systems( + Render, + ( + prepare_skybox_pipelines.in_set(RenderSet::Prepare), + queue_skybox_bind_groups.in_set(RenderSet::Queue), + ), + ); + } +} + +/// Adds a skybox to a 3D camera, based on a cubemap texture. +/// +/// Note that this component does not (currently) affect the scene's lighting. +/// To do so, use `EnvironmentMapLight` alongside this component. +/// +/// See also . +#[derive(Component, ExtractComponent, Clone)] +pub struct Skybox(pub Handle); + +#[derive(Resource)] +struct SkyboxPipeline { + bind_group_layout: BindGroupLayout, +} + +impl SkyboxPipeline { + fn new(render_device: &RenderDevice) -> Self { + let bind_group_layout_descriptor = BindGroupLayoutDescriptor { + label: Some("skybox_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::VERTEX_FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }, + ], + }; + + Self { + bind_group_layout: render_device + .create_bind_group_layout(&bind_group_layout_descriptor), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct SkyboxPipelineKey { + hdr: bool, + samples: u32, +} + +impl SpecializedRenderPipeline for SkyboxPipeline { + type Key = SkyboxPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("skybox_pipeline".into()), + layout: vec![self.bind_group_layout.clone()], + push_constant_ranges: Vec::new(), + vertex: VertexState { + shader: SKYBOX_SHADER_HANDLE.typed(), + shader_defs: Vec::new(), + entry_point: "skybox_vertex".into(), + buffers: Vec::new(), + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(FragmentState { + shader: SKYBOX_SHADER_HANDLE.typed(), + shader_defs: Vec::new(), + entry_point: "skybox_fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(BlendState::REPLACE), + write_mask: ColorWrites::ALL, + })], + }), + } + } +} + +#[derive(Component)] +pub struct SkyboxPipelineId(pub CachedRenderPipelineId); + +fn prepare_skybox_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + msaa: Res, + views: Query<(Entity, &ExtractedView), With>, +) { + for (entity, view) in &views { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &pipeline, + SkyboxPipelineKey { + hdr: view.hdr, + samples: msaa.samples(), + }, + ); + + commands + .entity(entity) + .insert(SkyboxPipelineId(pipeline_id)); + } +} + +#[derive(Component)] +pub struct SkyboxBindGroup(pub BindGroup); + +fn queue_skybox_bind_groups( + mut commands: Commands, + pipeline: Res, + view_uniforms: Res, + images: Res>, + render_device: Res, + views: Query<(Entity, &Skybox)>, +) { + for (entity, skybox) in &views { + if let (Some(skybox), Some(view_uniforms)) = + (images.get(&skybox.0), view_uniforms.uniforms.binding()) + { + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + label: Some("skybox_bind_group"), + layout: &pipeline.bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&skybox.texture_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&skybox.sampler), + }, + BindGroupEntry { + binding: 2, + resource: view_uniforms, + }, + ], + }); + + commands.entity(entity).insert(SkyboxBindGroup(bind_group)); + } + } +} diff --git a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl new file mode 100644 index 0000000000000..963de3d192ac9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl @@ -0,0 +1,52 @@ +#import bevy_render::view + +@group(0) @binding(0) +var skybox: texture_cube; +@group(0) @binding(1) +var skybox_sampler: sampler; +@group(0) @binding(2) +var view: View; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec3, +}; + +// 3 | 2. +// 2 | : `. +// 1 | x-----x. +// 0 | | s | `. +// -1 | 0-----x.....1 +// +--------------- +// -1 0 1 2 3 +// +// The axes are clip-space x and y. The region marked s is the visible region. +// The digits in the corners of the right-angled triangle are the vertex +// indices. +@vertex +fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + // See the explanation above for how this works. + let clip_position = vec4( + f32(vertex_index & 1u), + f32((vertex_index >> 1u) & 1u), + 0.25, + 0.5 + ) * 4.0 - vec4(1.0); + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + // NOTE: The clip position has a w component equal to 1.0 so we don't need + // to apply a perspective divide to it before inverse-projecting it. + let world_position_homogeneous = view.inverse_view_proj * vec4(clip_position.xy, 1.0, 1.0); + let world_position = world_position_homogeneous.xyz / world_position_homogeneous.w; + + return VertexOutput(clip_position, world_position); +} + +@fragment +fn skybox_fragment(in: VertexOutput) -> @location(0) vec4 { + // The skybox cubemap is sampled along the direction from the camera world + // position, to the fragment world position on the near clipping plane + let ray_direction = in.world_position - view.world_position; + // cube maps are left-handed so we negate the z coordinate + return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)); +} diff --git a/crates/bevy_pbr/src/environment_map/environment_map.wgsl b/crates/bevy_pbr/src/environment_map/environment_map.wgsl index 1c225645a812c..80062dd69b250 100644 --- a/crates/bevy_pbr/src/environment_map/environment_map.wgsl +++ b/crates/bevy_pbr/src/environment_map/environment_map.wgsl @@ -21,8 +21,8 @@ fn environment_map_light( // Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform // because textureNumLevels() does not work on WebGL2 let radiance_level = perceptual_roughness * f32(lights.environment_map_smallest_specular_mip_level); - let irradiance = textureSample(environment_map_diffuse, environment_map_sampler, N).rgb; - let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, R, radiance_level).rgb; + let irradiance = textureSample(environment_map_diffuse, environment_map_sampler, vec3(N.xy, -N.z)).rgb; + let radiance = textureSampleLevel(environment_map_specular, environment_map_sampler, vec3(R.xy, -R.z), radiance_level).rgb; // Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf // Useful reference: https://bruop.github.io/ibl diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index 50df20539fb1f..0291eca9af571 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -4,22 +4,13 @@ use std::f32::consts::PI; use bevy::{ asset::LoadState, + core_pipeline::Skybox, input::mouse::MouseMotion, - pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, - reflect::TypeUuid, render::{ - mesh::MeshVertexBufferLayout, - render_asset::RenderAssets, - render_resource::{ - AsBindGroup, AsBindGroupError, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, - OwnedBindingResource, PreparedBindGroup, RenderPipelineDescriptor, SamplerBindingType, - ShaderRef, ShaderStages, SpecializedMeshPipelineError, TextureSampleType, - TextureViewDescriptor, TextureViewDimension, - }, + render_resource::{TextureViewDescriptor, TextureViewDimension}, renderer::RenderDevice, - texture::{CompressedImageFormats, FallbackImage}, + texture::CompressedImageFormats, }, }; @@ -45,7 +36,6 @@ const CUBEMAPS: &[(&str, CompressedImageFormats)] = &[ fn main() { App::new() .add_plugins(DefaultPlugins) - .add_plugin(MaterialPlugin::::default()) .add_systems(Startup, setup) .add_systems( Update, @@ -86,6 +76,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, CameraController::default(), + Skybox(skybox_handle.clone()), )); // ambient light @@ -145,13 +136,10 @@ fn cycle_cubemap_asset( } fn asset_loaded( - mut commands: Commands, asset_server: Res, mut images: ResMut>, - mut meshes: ResMut>, - mut cubemap_materials: ResMut>, mut cubemap: ResMut, - cubes: Query<&Handle>, + mut skyboxes: Query<&mut Skybox>, ) { if !cubemap.is_loaded && asset_server.get_load_state(cubemap.image_handle.clone_weak()) == LoadState::Loaded @@ -170,22 +158,8 @@ fn asset_loaded( }); } - // spawn cube - let mut updated = false; - for handle in cubes.iter() { - if let Some(material) = cubemap_materials.get_mut(handle) { - updated = true; - material.base_color_texture = Some(cubemap.image_handle.clone_weak()); - } - } - if !updated { - commands.spawn(MaterialMeshBundle:: { - mesh: meshes.add(Mesh::from(shape::Cube { size: 10000.0 })), - material: cubemap_materials.add(CubemapMaterial { - base_color_texture: Some(cubemap.image_handle.clone_weak()), - }), - ..default() - }); + for mut skybox in &mut skyboxes { + skybox.0 = cubemap.image_handle.clone(); } cubemap.is_loaded = true; @@ -201,97 +175,6 @@ fn animate_light_direction( } } -#[derive(Debug, Clone, TypeUuid)] -#[uuid = "9509a0f8-3c05-48ee-a13e-a93226c7f488"] -struct CubemapMaterial { - base_color_texture: Option>, -} - -impl Material for CubemapMaterial { - fn fragment_shader() -> ShaderRef { - "shaders/cubemap_unlit.wgsl".into() - } - - fn specialize( - _pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - _layout: &MeshVertexBufferLayout, - _key: MaterialPipelineKey, - ) -> Result<(), SpecializedMeshPipelineError> { - descriptor.primitive.cull_mode = None; - Ok(()) - } -} - -impl AsBindGroup for CubemapMaterial { - type Data = (); - - fn as_bind_group( - &self, - layout: &BindGroupLayout, - render_device: &RenderDevice, - images: &RenderAssets, - _fallback_image: &FallbackImage, - ) -> Result, AsBindGroupError> { - let base_color_texture = self - .base_color_texture - .as_ref() - .ok_or(AsBindGroupError::RetryNextUpdate)?; - let image = images - .get(base_color_texture) - .ok_or(AsBindGroupError::RetryNextUpdate)?; - let bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&image.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&image.sampler), - }, - ], - label: Some("cubemap_texture_material_bind_group"), - layout, - }); - - Ok(PreparedBindGroup { - bind_group, - bindings: vec![ - OwnedBindingResource::TextureView(image.texture_view.clone()), - OwnedBindingResource::Sampler(image.sampler.clone()), - ], - data: (), - }) - } - - fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { - render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - // Cubemap Base Color Texture - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - multisampled: false, - sample_type: TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::Cube, - }, - count: None, - }, - // Cubemap Base Color Texture Sampler - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::Filtering), - count: None, - }, - ], - label: None, - }) - } -} - #[derive(Component)] pub struct CameraController { pub enabled: bool,