From 213839f50342ee64d25cad927af2a0e9b9bc02d7 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 16 Nov 2021 03:03:27 +0000 Subject: [PATCH] Add support for opaque, alpha mask, and alpha blend modes (#3072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Add depth prepass and support for opaque, alpha mask, and alpha blend modes for the 3D PBR target. ## Solution NOTE: This is based on top of #2861 frustum culling. Just lining it up to keep @cart loaded with the review train. 🚂 There are a lot of important details here. Big thanks to @cwfitzgerald of wgpu, naga, and rend3 fame for explaining how to do it properly! * An `AlphaMode` component is added that defines whether a material should be considered opaque, an alpha mask (with a cutoff value that defaults to 0.5, the same as glTF), or transparent and should be alpha blended * Two depth prepasses are added: * Opaque does a plain vertex stage * Alpha mask does the vertex stage but also a fragment stage that samples the colour for the fragment and discards if its alpha value is below the cutoff value * Both are sorted front to back, not that it matters for these passes. (Maybe there should be a way to skip sorting?) * Three main passes are added: * Opaque and alpha mask passes use a depth comparison function of Equal such that only the geometry that was closest is processed further, due to early-z testing * The transparent pass uses the Greater depth comparison function so that only transparent objects that are closer than anything opaque are rendered * The opaque fragment shading is as before except that alpha is explicitly set to 1.0 * Alpha mask fragment shading sets the alpha value to 1.0 if it is equal to or above the cutoff, as defined by glTF * Opaque and alpha mask are sorted front to back (again not that it matters as we will skip anything that is not equal... maybe sorting is no longer needed here?) * Transparent is sorted back to front. Transparent fragment shading uses the alpha blending over operator Co-authored-by: Carter Anderson --- pipelined/bevy_core_pipeline/src/lib.rs | 91 ++++++++++- .../bevy_core_pipeline/src/main_pass_3d.rs | 150 ++++++++++++----- pipelined/bevy_gltf2/src/loader.rs | 11 +- pipelined/bevy_pbr2/src/alpha.rs | 22 +++ pipelined/bevy_pbr2/src/lib.rs | 6 +- pipelined/bevy_pbr2/src/material.rs | 66 ++++---- pipelined/bevy_pbr2/src/render/mod.rs | 152 +++++++++++++----- pipelined/bevy_pbr2/src/render/pbr.wgsl | 18 +++ pipelined/bevy_render2/src/view/mod.rs | 23 ++- pipelined/bevy_sprite2/src/render/mod.rs | 13 +- 10 files changed, 421 insertions(+), 131 deletions(-) create mode 100644 pipelined/bevy_pbr2/src/alpha.rs diff --git a/pipelined/bevy_core_pipeline/src/lib.rs b/pipelined/bevy_core_pipeline/src/lib.rs index 5d9be70e7562c..7da6c588875f5 100644 --- a/pipelined/bevy_core_pipeline/src/lib.rs +++ b/pipelined/bevy_core_pipeline/src/lib.rs @@ -75,11 +75,15 @@ impl Plugin for CorePipelinePlugin { let render_app = app.sub_app(RenderApp); render_app .init_resource::>() + .init_resource::>() + .init_resource::>() .init_resource::>() .add_system_to_stage(RenderStage::Extract, extract_clear_color) .add_system_to_stage(RenderStage::Extract, extract_core_pipeline_camera_phases) .add_system_to_stage(RenderStage::Prepare, prepare_core_views_system) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::); let pass_node_2d = MainPass2dNode::new(&mut render_app.world); @@ -147,6 +151,76 @@ impl PhaseItem for Transparent2d { } } +pub struct Opaque3d { + pub distance: f32, + pub pipeline: CachedPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for Opaque3d { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } +} + +impl EntityPhaseItem for Opaque3d { + #[inline] + fn entity(&self) -> Entity { + self.entity + } +} + +impl CachedPipelinePhaseItem for Opaque3d { + #[inline] + fn cached_pipeline(&self) -> CachedPipelineId { + self.pipeline + } +} + +pub struct AlphaMask3d { + pub distance: f32, + pub pipeline: CachedPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for AlphaMask3d { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } +} + +impl EntityPhaseItem for AlphaMask3d { + #[inline] + fn entity(&self) -> Entity { + self.entity + } +} + +impl CachedPipelinePhaseItem for AlphaMask3d { + #[inline] + fn cached_pipeline(&self) -> CachedPipelineId { + self.pipeline + } +} + pub struct Transparent3d { pub distance: f32, pub pipeline: CachedPipelineId, @@ -203,9 +277,11 @@ pub fn extract_core_pipeline_camera_phases( } if let Some(camera_3d) = active_cameras.get(CameraPlugin::CAMERA_3D) { if let Some(entity) = camera_3d.entity { - commands - .get_or_spawn(entity) - .insert(RenderPhase::::default()); + commands.get_or_spawn(entity).insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )); } } } @@ -215,7 +291,14 @@ pub fn prepare_core_views_system( mut texture_cache: ResMut, msaa: Res, render_device: Res, - views_3d: Query<(Entity, &ExtractedView), With>>, + views_3d: Query< + (Entity, &ExtractedView), + ( + With>, + With>, + With>, + ), + >, ) { for (entity, view) in views_3d.iter() { let cached_texture = texture_cache.get( diff --git a/pipelined/bevy_core_pipeline/src/main_pass_3d.rs b/pipelined/bevy_core_pipeline/src/main_pass_3d.rs index 5ecc53eda80cb..b63bb80949b2b 100644 --- a/pipelined/bevy_core_pipeline/src/main_pass_3d.rs +++ b/pipelined/bevy_core_pipeline/src/main_pass_3d.rs @@ -1,12 +1,9 @@ -use crate::{ClearColor, Transparent3d}; +use crate::{AlphaMask3d, ClearColor, Opaque3d, Transparent3d}; use bevy_ecs::prelude::*; use bevy_render2::{ render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, - render_resource::{ - LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, - RenderPassDescriptor, - }, + render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor}, renderer::RenderContext, view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; @@ -14,6 +11,8 @@ use bevy_render2::{ pub struct MainPass3dNode { query: QueryState< ( + &'static RenderPhase, + &'static RenderPhase, &'static RenderPhase, &'static ViewTarget, &'static ViewDepthTexture, @@ -48,52 +47,119 @@ impl Node for MainPass3dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (transparent_phase, target, depth) = self + let (opaque_phase, alpha_mask_phase, transparent_phase, target, depth) = self .query .get_manual(world, view_entity) .expect("view entity should exist"); let clear_color = world.get_resource::().unwrap(); - let pass_descriptor = RenderPassDescriptor { - label: Some("main_pass_3d"), - color_attachments: &[RenderPassColorAttachment { - view: if let Some(sampled_target) = &target.sampled_target { - sampled_target - } else { - &target.view - }, - resolve_target: if target.sampled_target.is_some() { - Some(&target.view) - } else { - None - }, - ops: Operations { + + { + // Run the opaque pass, sorted front-to-back + // NOTE: Scoped to drop the mutable borrow of render_context + let pass_descriptor = RenderPassDescriptor { + label: Some("main_opaque_pass_3d"), + // NOTE: The opaque pass clears and initializes the color + // buffer as well as writing to it. + color_attachments: &[target.get_color_attachment(Operations { load: LoadOp::Clear(clear_color.0.into()), store: true, - }, - }], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - depth_ops: Some(Operations { - load: LoadOp::Clear(0.0), + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: The opaque main pass clears and writes to the depth buffer. + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: true, + }), + stencil_ops: None, + }), + }; + + let draw_functions = world.get_resource::>().unwrap(); + + let render_pass = render_context + .command_encoder + .begin_render_pass(&pass_descriptor); + let mut draw_functions = draw_functions.write(); + let mut tracked_pass = TrackedRenderPass::new(render_pass); + for item in opaque_phase.items.iter() { + let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); + draw_function.draw(world, &mut tracked_pass, view_entity, item); + } + } + + { + // Run the alpha mask pass, sorted front-to-back + // NOTE: Scoped to drop the mutable borrow of render_context + let pass_descriptor = RenderPassDescriptor { + label: Some("main_alpha_mask_pass_3d"), + // NOTE: The alpha_mask pass loads the color buffer as well as overwriting it where appropriate. + color_attachments: &[target.get_color_attachment(Operations { + load: LoadOp::Load, store: true, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: The alpha mask pass loads the depth buffer and possibly overwrites it + depth_ops: Some(Operations { + load: LoadOp::Load, + store: true, + }), + stencil_ops: None, }), - stencil_ops: None, - }), - }; - - let draw_functions = world - .get_resource::>() - .unwrap(); - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut draw_functions = draw_functions.write(); - let mut tracked_pass = TrackedRenderPass::new(render_pass); - for item in transparent_phase.items.iter() { - let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); - draw_function.draw(world, &mut tracked_pass, view_entity, item); + }; + + let draw_functions = world.get_resource::>().unwrap(); + + let render_pass = render_context + .command_encoder + .begin_render_pass(&pass_descriptor); + let mut draw_functions = draw_functions.write(); + let mut tracked_pass = TrackedRenderPass::new(render_pass); + for item in alpha_mask_phase.items.iter() { + let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); + draw_function.draw(world, &mut tracked_pass, view_entity, item); + } } + + { + // Run the transparent pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + let pass_descriptor = RenderPassDescriptor { + label: Some("main_transparent_pass_3d"), + // NOTE: The transparent pass loads the color buffer as well as overwriting it where appropriate. + color_attachments: &[target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: For the transparent pass we load the depth buffer but do not write to it. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. + depth_ops: Some(Operations { + load: LoadOp::Load, + store: false, + }), + stencil_ops: None, + }), + }; + + let draw_functions = world + .get_resource::>() + .unwrap(); + + let render_pass = render_context + .command_encoder + .begin_render_pass(&pass_descriptor); + let mut draw_functions = draw_functions.write(); + let mut tracked_pass = TrackedRenderPass::new(render_pass); + for item in transparent_phase.items.iter() { + let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); + draw_function.draw(world, &mut tracked_pass, view_entity, item); + } + } + Ok(()) } } diff --git a/pipelined/bevy_gltf2/src/loader.rs b/pipelined/bevy_gltf2/src/loader.rs index 6dba4b9e8ab97..9e15a7a4d99b8 100644 --- a/pipelined/bevy_gltf2/src/loader.rs +++ b/pipelined/bevy_gltf2/src/loader.rs @@ -6,7 +6,7 @@ use bevy_core::Name; use bevy_ecs::world::World; use bevy_log::warn; use bevy_math::{Mat4, Vec3}; -use bevy_pbr2::{PbrBundle, StandardMaterial}; +use bevy_pbr2::{AlphaMode, PbrBundle, StandardMaterial}; use bevy_render2::{ camera::{ Camera, CameraPlugin, CameraProjection, OrthographicProjection, PerspectiveProjection, @@ -438,6 +438,7 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle< emissive: Color::rgba(emissive[0], emissive[1], emissive[2], 1.0), emissive_texture, unlit: material.unlit(), + alpha_mode: alpha_mode(material), ..Default::default() }), ) @@ -649,6 +650,14 @@ fn get_primitive_topology(mode: Mode) -> Result { } } +fn alpha_mode(material: &Material) -> AlphaMode { + match material.alpha_mode() { + gltf::material::AlphaMode::Opaque => AlphaMode::Opaque, + gltf::material::AlphaMode::Mask => AlphaMode::Mask(material.alpha_cutoff().unwrap_or(0.5)), + gltf::material::AlphaMode::Blend => AlphaMode::Blend, + } +} + async fn load_buffers( gltf: &gltf::Gltf, load_context: &LoadContext<'_>, diff --git a/pipelined/bevy_pbr2/src/alpha.rs b/pipelined/bevy_pbr2/src/alpha.rs new file mode 100644 index 0000000000000..8dd5479291501 --- /dev/null +++ b/pipelined/bevy_pbr2/src/alpha.rs @@ -0,0 +1,22 @@ +use bevy_ecs::reflect::ReflectComponent; +use bevy_reflect::Reflect; + +// FIXME: This should probably be part of bevy_render2! +/// Alpha mode +#[derive(Debug, Reflect, Clone, PartialEq)] +#[reflect(Component)] +pub enum AlphaMode { + Opaque, + /// An alpha cutoff must be supplied where alpha values >= the cutoff + /// will be fully opaque and < will be fully transparent + Mask(f32), + Blend, +} + +impl Eq for AlphaMode {} + +impl Default for AlphaMode { + fn default() -> Self { + AlphaMode::Opaque + } +} diff --git a/pipelined/bevy_pbr2/src/lib.rs b/pipelined/bevy_pbr2/src/lib.rs index 7b17c99c1b780..b9d537aab2da0 100644 --- a/pipelined/bevy_pbr2/src/lib.rs +++ b/pipelined/bevy_pbr2/src/lib.rs @@ -1,8 +1,10 @@ +mod alpha; mod bundle; mod light; mod material; mod render; +pub use alpha::*; pub use bundle::*; pub use light::*; pub use material::*; @@ -10,7 +12,7 @@ pub use render::*; use bevy_app::prelude::*; use bevy_asset::{Assets, Handle, HandleUntyped}; -use bevy_core_pipeline::Transparent3d; +use bevy_core_pipeline::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy_ecs::prelude::*; use bevy_reflect::TypeUuid; use bevy_render2::{ @@ -109,6 +111,8 @@ impl Plugin for PbrPlugin { .init_resource::>(); let shadow_pass_node = ShadowPassNode::new(&mut render_app.world); + render_app.add_render_command::(); + render_app.add_render_command::(); render_app.add_render_command::(); render_app.add_render_command::(); let mut graph = render_app.world.get_resource_mut::().unwrap(); diff --git a/pipelined/bevy_pbr2/src/material.rs b/pipelined/bevy_pbr2/src/material.rs index 72f050402fd51..87a7622e3078f 100644 --- a/pipelined/bevy_pbr2/src/material.rs +++ b/pipelined/bevy_pbr2/src/material.rs @@ -1,4 +1,4 @@ -use crate::{PbrPipeline, StandardMaterialFlags}; +use crate::{AlphaMode, PbrPipeline, StandardMaterialFlags}; use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, Handle}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; @@ -7,9 +7,7 @@ use bevy_reflect::TypeUuid; use bevy_render2::{ color::Color, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, - render_resource::{ - BindGroup, Buffer, BufferInitDescriptor, BufferUsages, Sampler, TextureView, - }, + render_resource::{BindGroup, Buffer, BufferInitDescriptor, BufferUsages}, renderer::RenderDevice, texture::Image, }; @@ -47,6 +45,7 @@ pub struct StandardMaterial { pub occlusion_texture: Option>, pub double_sided: bool, pub unlit: bool, + pub alpha_mode: AlphaMode, } impl Default for StandardMaterial { @@ -73,6 +72,7 @@ impl Default for StandardMaterial { normal_map_texture: None, double_sided: false, unlit: false, + alpha_mode: AlphaMode::Opaque, } } } @@ -95,7 +95,7 @@ impl From> for StandardMaterial { } } -#[derive(Clone, AsStd140)] +#[derive(Clone, Default, AsStd140)] pub struct StandardMaterialUniformData { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. @@ -112,6 +112,9 @@ pub struct StandardMaterialUniformData { /// defaults to 0.5 which is mapped to 4% reflectance in the shader pub reflectance: f32, pub flags: u32, + /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, + /// and any below means fully transparent. + pub alpha_cutoff: f32, } pub struct StandardMaterialPlugin; @@ -127,7 +130,10 @@ impl Plugin for StandardMaterialPlugin { pub struct GpuStandardMaterial { pub buffer: Buffer, pub bind_group: BindGroup, + pub has_normal_map: bool, pub flags: StandardMaterialFlags, + pub base_color_texture: Option>, + pub alpha_mode: AlphaMode, } impl RenderAsset for StandardMaterial { @@ -148,7 +154,7 @@ impl RenderAsset for StandardMaterial { (render_device, pbr_pipeline, gpu_images): &mut SystemParamItem, ) -> Result> { let (base_color_texture_view, base_color_sampler) = if let Some(result) = - image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.base_color_texture) + pbr_pipeline.image_handle_to_texture(gpu_images, &material.base_color_texture) { result } else { @@ -156,7 +162,7 @@ impl RenderAsset for StandardMaterial { }; let (emissive_texture_view, emissive_sampler) = if let Some(result) = - image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.emissive_texture) + pbr_pipeline.image_handle_to_texture(gpu_images, &material.emissive_texture) { result } else { @@ -164,24 +170,21 @@ impl RenderAsset for StandardMaterial { }; let (metallic_roughness_texture_view, metallic_roughness_sampler) = if let Some(result) = - image_handle_to_view_sampler( - pbr_pipeline, - gpu_images, - &material.metallic_roughness_texture, - ) { + pbr_pipeline.image_handle_to_texture(gpu_images, &material.metallic_roughness_texture) + { result } else { return Err(PrepareAssetError::RetryNextUpdate(material)); }; let (normal_map_texture_view, normal_map_sampler) = if let Some(result) = - image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.normal_map_texture) + pbr_pipeline.image_handle_to_texture(gpu_images, &material.normal_map_texture) { result } else { return Err(PrepareAssetError::RetryNextUpdate(material)); }; let (occlusion_texture_view, occlusion_sampler) = if let Some(result) = - image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.occlusion_texture) + pbr_pipeline.image_handle_to_texture(gpu_images, &material.occlusion_texture) { result } else { @@ -206,9 +209,18 @@ impl RenderAsset for StandardMaterial { if material.unlit { flags |= StandardMaterialFlags::UNLIT; } - if material.normal_map_texture.is_some() { - flags |= StandardMaterialFlags::NORMAL_MAP_TEXTURE; - } + let has_normal_map = material.normal_map_texture.is_some(); + // NOTE: 0.5 is from the glTF default - do we want this? + let mut alpha_cutoff = 0.5; + match material.alpha_mode { + AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE, + AlphaMode::Mask(c) => { + alpha_cutoff = c; + flags |= StandardMaterialFlags::ALPHA_MODE_MASK + } + AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, + }; + let value = StandardMaterialUniformData { base_color: material.base_color.as_rgba_linear().into(), emissive: material.emissive.into(), @@ -216,6 +228,7 @@ impl RenderAsset for StandardMaterial { metallic: material.metallic, reflectance: material.reflectance, flags: flags.bits(), + alpha_cutoff, }; let value_std140 = value.as_std140(); @@ -279,22 +292,9 @@ impl RenderAsset for StandardMaterial { buffer, bind_group, flags, + has_normal_map, + base_color_texture: material.base_color_texture, + alpha_mode: material.alpha_mode, }) } } - -fn image_handle_to_view_sampler<'a>( - pbr_pipeline: &'a PbrPipeline, - gpu_images: &'a RenderAssets, - handle_option: &Option>, -) -> Option<(&'a TextureView, &'a Sampler)> { - if let Some(handle) = handle_option { - let gpu_image = gpu_images.get(handle)?; - Some((&gpu_image.texture_view, &gpu_image.sampler)) - } else { - Some(( - &pbr_pipeline.dummy_white_gpu_image.texture_view, - &pbr_pipeline.dummy_white_gpu_image.sampler, - )) - } -} diff --git a/pipelined/bevy_pbr2/src/render/mod.rs b/pipelined/bevy_pbr2/src/render/mod.rs index c1d08028c2439..3cc4c5650a00c 100644 --- a/pipelined/bevy_pbr2/src/render/mod.rs +++ b/pipelined/bevy_pbr2/src/render/mod.rs @@ -3,11 +3,11 @@ mod light; pub use light::*; use crate::{ - NotShadowCaster, NotShadowReceiver, StandardMaterial, StandardMaterialUniformData, + AlphaMode, NotShadowCaster, NotShadowReceiver, StandardMaterial, StandardMaterialUniformData, PBR_SHADER_HANDLE, }; use bevy_asset::Handle; -use bevy_core_pipeline::Transparent3d; +use bevy_core_pipeline::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, @@ -61,7 +61,9 @@ bitflags::bitflags! { const OCCLUSION_TEXTURE = (1 << 3); const DOUBLE_SIDED = (1 << 4); const UNLIT = (1 << 5); - const NORMAL_MAP_TEXTURE = (1 << 6); + const ALPHA_MODE_OPAQUE = (1 << 6); + const ALPHA_MODE_MASK = (1 << 7); + const ALPHA_MODE_BLEND = (1 << 8); const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -153,6 +155,24 @@ pub struct PbrPipeline { pub dummy_white_gpu_image: GpuImage, } +impl PbrPipeline { + pub fn image_handle_to_texture<'a>( + &'a self, + gpu_images: &'a RenderAssets, + handle_option: &Option>, + ) -> Option<(&'a TextureView, &'a Sampler)> { + if let Some(handle) = handle_option { + let gpu_image = gpu_images.get(handle)?; + Some((&gpu_image.texture_view, &gpu_image.sampler)) + } else { + Some(( + &self.dummy_white_gpu_image.texture_view, + &self.dummy_white_gpu_image.sampler, + )) + } + } +} + impl FromWorld for PbrPipeline { fn from_world(world: &mut World) -> Self { let render_device = world.get_resource::().unwrap(); @@ -426,6 +446,9 @@ bitflags::bitflags! { const NONE = 0; const VERTEX_TANGENTS = (1 << 0); const STANDARDMATERIAL_NORMAL_MAP = (1 << 1); + const OPAQUE_MAIN_PASS = (1 << 2); + const ALPHA_MASK_MAIN_PASS = (1 << 3); + const TRANSPARENT_MAIN_PASS = (1 << 4); const MSAA_RESERVED_BITS = PbrPipelineKey::MSAA_MASK_BITS << PbrPipelineKey::MSAA_SHIFT_BITS; } } @@ -511,6 +534,21 @@ impl SpecializedPipeline for PbrPipeline { if key.contains(PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP) { shader_defs.push(String::from("STANDARDMATERIAL_NORMAL_MAP")); } + let (label, blend, depth_write_enabled); + if key.contains(PbrPipelineKey::TRANSPARENT_MAIN_PASS) { + label = Some("transparent_pbr_pipeline".into()); + blend = Some(BlendState::ALPHA_BLENDING); + // For the transparent pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else { + label = Some("opaque_pbr_pipeline".into()); + blend = Some(BlendState::REPLACE); + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + } RenderPipelineDescriptor { vertex: VertexState { shader: PBR_SHADER_HANDLE.typed::(), @@ -528,18 +566,7 @@ impl SpecializedPipeline for PbrPipeline { entry_point: "fragment".into(), targets: vec![ColorTargetState { format: TextureFormat::bevy_default(), - blend: Some(BlendState { - color: BlendComponent { - src_factor: BlendFactor::SrcAlpha, - dst_factor: BlendFactor::OneMinusSrcAlpha, - operation: BlendOperation::Add, - }, - alpha: BlendComponent { - src_factor: BlendFactor::One, - dst_factor: BlendFactor::One, - operation: BlendOperation::Add, - }, - }), + blend, write_mask: ColorWrites::ALL, }], }), @@ -559,7 +586,7 @@ impl SpecializedPipeline for PbrPipeline { }, depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth32Float, - depth_write_enabled: true, + depth_write_enabled, depth_compare: CompareFunction::Greater, stencil: StencilState { front: StencilFaceState::IGNORE, @@ -578,7 +605,7 @@ impl SpecializedPipeline for PbrPipeline { mask: !0, alpha_to_coverage_enabled: false, }, - label: Some("pbr_pipeline".into()), + label, } } } @@ -614,7 +641,9 @@ pub struct PbrViewBindGroup { #[allow(clippy::too_many_arguments)] pub fn queue_meshes( mut commands: Commands, - transparent_3d_draw_functions: Res>, + opaque_draw_functions: Res>, + alpha_mask_draw_functions: Res>, + transparent_draw_functions: Res>, render_device: Res, pbr_pipeline: Res, shadow_pipeline: Res, @@ -631,6 +660,8 @@ pub fn queue_meshes( &ExtractedView, &ViewLights, &VisibleEntities, + &mut RenderPhase, + &mut RenderPhase, &mut RenderPhase, )>, ) { @@ -638,7 +669,15 @@ pub fn queue_meshes( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), ) { - for (entity, view, view_lights, visible_entities, mut transparent_phase) in views.iter_mut() + for ( + entity, + view, + view_lights, + visible_entities, + mut opaque_phase, + mut alpha_mask_phase, + mut transparent_phase, + ) in views.iter_mut() { let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ @@ -681,46 +720,85 @@ pub fn queue_meshes( value: view_bind_group, }); - let draw_pbr = transparent_3d_draw_functions + let draw_opaque_pbr = opaque_draw_functions.read().get_id::().unwrap(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions + .read() + .get_id::() + .unwrap(); + let draw_transparent_pbr = transparent_draw_functions .read() .get_id::() .unwrap(); - let view_matrix = view.transform.compute_matrix(); - let view_row_2 = view_matrix.row(2); + let inverse_view_matrix = view.transform.compute_matrix().inverse(); + let inverse_view_row_2 = inverse_view_matrix.row(2); for visible_entity in &visible_entities.entities { if let Ok((material_handle, mesh_handle, mesh_uniform)) = standard_material_meshes.get(visible_entity.entity) { let mut key = PbrPipelineKey::from_msaa_samples(msaa.samples); - if let Some(material) = render_materials.get(material_handle) { - if material - .flags - .contains(StandardMaterialFlags::NORMAL_MAP_TEXTURE) - { + let alpha_mode = if let Some(material) = render_materials.get(material_handle) { + if material.has_normal_map { key |= PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP; } + material.alpha_mode.clone() } else { continue; - } + }; if let Some(mesh) = render_meshes.get(mesh_handle) { if mesh.has_tangents { key |= PbrPipelineKey::VERTEX_TANGENTS; } } + key |= match alpha_mode { + AlphaMode::Opaque => PbrPipelineKey::OPAQUE_MAIN_PASS, + AlphaMode::Mask(_) => PbrPipelineKey::ALPHA_MASK_MAIN_PASS, + AlphaMode::Blend => PbrPipelineKey::TRANSPARENT_MAIN_PASS, + }; let pipeline_id = pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, key); - // NOTE: row 2 of the view matrix dotted with column 3 of the model matrix + // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix // gives the z component of translation of the mesh in view space - let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3)); - // TODO: currently there is only "transparent phase". this should pick transparent vs opaque according to the mesh material - transparent_phase.add(Transparent3d { - entity: visible_entity.entity, - draw_function: draw_pbr, - pipeline: pipeline_id, - distance: mesh_z, - }); + let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3)); + match alpha_mode { + AlphaMode::Opaque => { + opaque_phase.add(Opaque3d { + entity: visible_entity.entity, + draw_function: draw_opaque_pbr, + pipeline: pipeline_id, + // NOTE: Front-to-back ordering for opaque with ascending sort means near should have the + // lowest sort key and getting further away should increase. As we have + // -z in front of the camera, values in view space decrease away from the + // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering + distance: -mesh_z, + }); + } + AlphaMode::Mask(_) => { + alpha_mask_phase.add(AlphaMask3d { + entity: visible_entity.entity, + draw_function: draw_alpha_mask_pbr, + pipeline: pipeline_id, + // NOTE: Front-to-back ordering for alpha mask with ascending sort means near should have the + // lowest sort key and getting further away should increase. As we have + // -z in front of the camera, values in view space decrease away from the + // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering + distance: -mesh_z, + }); + } + AlphaMode::Blend => { + transparent_phase.add(Transparent3d { + entity: visible_entity.entity, + draw_function: draw_transparent_pbr, + pipeline: pipeline_id, + // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the + // lowest sort key and getting closer should increase. As we have + // -z in front of the camera, the largest distance is -far with values increasing toward the + // camera. As such we can just use mesh_z as the distance + distance: mesh_z, + }); + } + } } } } diff --git a/pipelined/bevy_pbr2/src/render/pbr.wgsl b/pipelined/bevy_pbr2/src/render/pbr.wgsl index 87435a450c0a6..caa09535a59bc 100644 --- a/pipelined/bevy_pbr2/src/render/pbr.wgsl +++ b/pipelined/bevy_pbr2/src/render/pbr.wgsl @@ -110,6 +110,7 @@ struct StandardMaterial { reflectance: f32; // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32; + alpha_cutoff: f32; }; let STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u; @@ -118,6 +119,9 @@ let STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u; let STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u; let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u; let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u; +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u; +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u; struct PointLight { projection: mat4x4; @@ -559,6 +563,20 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { #endif #endif + if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + output_color.a = 1.0; + } elseif ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { + if (output_color.a >= material.alpha_cutoff) { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + output_color.a = 1.0; + } else { + // NOTE: output_color.a < material.alpha_cutoff should not is not rendered + // NOTE: This and any other discards mean that early-z testing cannot be done! + discard; + } + } + var V: vec3; if (view.projection[3].w != 1.0) { // If the projection is not orthographic // Only valid for a perpective projection diff --git a/pipelined/bevy_render2/src/view/mod.rs b/pipelined/bevy_render2/src/view/mod.rs index 008a3336bbb07..81e2c6606e085 100644 --- a/pipelined/bevy_render2/src/view/mod.rs +++ b/pipelined/bevy_render2/src/view/mod.rs @@ -2,7 +2,10 @@ pub mod visibility; pub mod window; pub use visibility::*; -use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; +use wgpu::{ + Color, Extent3d, Operations, RenderPassColorAttachment, TextureDescriptor, TextureDimension, + TextureFormat, TextureUsages, +}; pub use window::*; use crate::{ @@ -83,6 +86,24 @@ pub struct ViewTarget { pub sampled_target: Option, } +impl ViewTarget { + pub fn get_color_attachment(&self, ops: Operations) -> RenderPassColorAttachment { + RenderPassColorAttachment { + view: if let Some(sampled_target) = &self.sampled_target { + sampled_target + } else { + &self.view + }, + resolve_target: if self.sampled_target.is_some() { + Some(&self.view) + } else { + None + }, + ops, + } + } +} + pub struct ViewDepthTexture { pub texture: Texture, pub view: TextureView, diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index f88a0ceca4973..0a0ddc620f0f3 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -133,18 +133,7 @@ impl SpecializedPipeline for SpritePipeline { entry_point: "fragment".into(), targets: vec![ColorTargetState { format: TextureFormat::bevy_default(), - blend: Some(BlendState { - color: BlendComponent { - src_factor: BlendFactor::SrcAlpha, - dst_factor: BlendFactor::OneMinusSrcAlpha, - operation: BlendOperation::Add, - }, - alpha: BlendComponent { - src_factor: BlendFactor::One, - dst_factor: BlendFactor::One, - operation: BlendOperation::Add, - }, - }), + blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, }], }),