From 44928e0df49a202c201a6962775e6883cafebb7e Mon Sep 17 00:00:00 2001 From: Marco Buono Date: Tue, 31 Oct 2023 17:59:02 -0300 Subject: [PATCH] `StandardMaterial` Light Transmission (#8015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Screenshot 2023-04-26 at 01 07 34 This PR adds both diffuse and specular light transmission capabilities to the `StandardMaterial`, with support for screen space refractions. This enables realistically representing a wide range of real-world materials, such as: - Glass; (Including frosted glass) - Transparent and translucent plastics; - Various liquids and gels; - Gemstones; - Marble; - Wax; - Paper; - Leaves; - Porcelain. Unlike existing support for transparency, light transmission does not rely on fixed function alpha blending, and therefore works with both `AlphaMode::Opaque` and `AlphaMode::Mask` materials. ## Solution - Introduces a number of transmission related fields in the `StandardMaterial`; - For specular transmission: - Adds logic to take a view main texture snapshot after the opaque phase; (in order to perform screen space refractions) - Introduces a new `Transmissive3d` phase to the renderer, to which all meshes with `transmission > 0.0` materials are sent. - Calculates a light exit point (of the approximate mesh volume) using `ior` and `thickness` properties - Samples the snapshot texture with an adaptive number of taps across a `roughness`-controlled radius enabling “blurry” refractions - For diffuse transmission: - Approximates transmitted diffuse light by using a second, flipped + displaced, diffuse-only Lambertian lobe for each light source. ## To Do - [x] Figure out where `fresnel_mix()` is taking place, if at all, and where `dielectric_specular` is being calculated, if at all, and update them to use the `ior` value (Not a blocker, just a nice-to-have for more correct BSDF) - To the _best of my knowledge, this is now taking place, after 964340cdd. The fresnel mix is actually "split" into two parts in our implementation, one `(1 - fresnel(...))` in the transmission, and `fresnel()` in the light implementations. A surface with more reflectance now will produce slightly dimmer transmission towards the grazing angle, as more of the light gets reflected. - [x] Add `transmission_texture` - [x] Add `diffuse_transmission_texture` - [x] Add `thickness_texture` - [x] Add `attenuation_distance` and `attenuation_color` - [x] Connect values to glTF loader - [x] `transmission` and `transmission_texture` - [x] `thickness` and `thickness_texture` - [x] `ior` - [ ] `diffuse_transmission` and `diffuse_transmission_texture` (needs upstream support in `gltf` crate, not a blocker) - [x] Add support for multiple screen space refraction “steps” - [x] Conditionally create no transmission snapshot texture at all if `steps == 0` - [x] Conditionally enable/disable screen space refraction transmission snapshots - [x] Read from depth pre-pass to prevent refracting pixels in front of the light exit point - [x] Use `interleaved_gradient_noise()` function for sampling blur in a way that benefits from TAA - [x] Drill down a TAA `#define`, tweak some aspects of the effect conditionally based on it - [x] Remove const array that's crashing under HLSL (unless a new `naga` release with https://github.com/gfx-rs/naga/pull/2496 comes out before we merge this) - [ ] Look into alternatives to the `switch` hack for dynamically indexing the const array (might not be needed, compilers seem to be decent at expanding it) - [ ] Add pipeline keys for gating transmission (do we really want/need this?) - [x] Tweak some material field/function names? ## A Note on Texture Packing _This was originally added as a comment to the `specular_transmission_texture`, `thickness_texture` and `diffuse_transmission_texture` documentation, I removed it since it was more confusing than helpful, and will likely be made redundant/will need to be updated once we have a better infrastructure for preprocessing assets_ Due to how channels are mapped, you can more efficiently use a single shared texture image for configuring the following: - R - `specular_transmission_texture` - G - `thickness_texture` - B - _unused_ - A - `diffuse_transmission_texture` The `KHR_materials_diffuse_transmission` glTF extension also defines a `diffuseTransmissionColorTexture`, that _we don't currently support_. One might choose to pack the intensity and color textures together, using RGB for the color and A for the intensity, in which case this packing advice doesn't really apply. --- ## Changelog - Added a new `Transmissive3d` render phase for rendering specular transmissive materials with screen space refractions - Added rendering support for transmitted environment map light on the `StandardMaterial` as a fallback for screen space refractions - Added `diffuse_transmission`, `specular_transmission`, `thickness`, `ior`, `attenuation_distance` and `attenuation_color` to the `StandardMaterial` - Added `diffuse_transmission_texture`, `specular_transmission_texture`, `thickness_texture` to the `StandardMaterial`, gated behind a new `pbr_transmission_textures` cargo feature (off by default, for maximum hardware compatibility) - Added `Camera3d::screen_space_specular_transmission_steps` for controlling the number of “layers of transparency” rendered for transmissive objects - Added a `TransmittedShadowReceiver` component for enabling shadows in (diffusely) transmitted light. (disabled by default, as it requires carefully setting up the `thickness` to avoid self-shadow artifacts) - Added support for the `KHR_materials_transmission`, `KHR_materials_ior` and `KHR_materials_volume` glTF extensions - Renamed items related to temporal jitter for greater consistency ## Migration Guide - `SsaoPipelineKey::temporal_noise` has been renamed to `SsaoPipelineKey::temporal_jitter` - The `TAA` shader def (controlled by the presence of the `TemporalAntiAliasSettings` component in the camera) has been replaced with the `TEMPORAL_JITTER` shader def (controlled by the presence of the `TemporalJitter` component in the camera) - `MeshPipelineKey::TAA` has been replaced by `MeshPipelineKey::TEMPORAL_JITTER` - The `TEMPORAL_NOISE` shader def has been consolidated with `TEMPORAL_JITTER` --- Cargo.toml | 13 + .../src/core_3d/camera_3d.rs | 58 ++ .../core_3d/main_transmissive_pass_3d_node.rs | 148 ++++ crates/bevy_core_pipeline/src/core_3d/mod.rs | 183 ++++- .../src/tonemapping/tonemapping_shared.wgsl | 9 + crates/bevy_gltf/Cargo.toml | 6 + crates/bevy_gltf/src/loader.rs | 63 ++ crates/bevy_internal/Cargo.toml | 3 + crates/bevy_pbr/Cargo.toml | 1 + crates/bevy_pbr/src/extended_material.rs | 4 + crates/bevy_pbr/src/lib.rs | 7 + crates/bevy_pbr/src/light.rs | 14 + crates/bevy_pbr/src/material.rs | 84 ++- crates/bevy_pbr/src/pbr_material.rs | 215 +++++- crates/bevy_pbr/src/prepass/mod.rs | 6 + .../bevy_pbr/src/prepass/prepass_utils.wgsl | 9 +- crates/bevy_pbr/src/render/mesh.rs | 50 +- crates/bevy_pbr/src/render/mesh_types.wgsl | 1 + .../bevy_pbr/src/render/mesh_view_bindings.rs | 42 +- .../src/render/mesh_view_bindings.wgsl | 4 +- crates/bevy_pbr/src/render/pbr_bindings.wgsl | 8 + crates/bevy_pbr/src/render/pbr_fragment.wgsl | 34 +- crates/bevy_pbr/src/render/pbr_functions.wgsl | 146 +++- .../bevy_pbr/src/render/pbr_transmission.wgsl | 181 +++++ crates/bevy_pbr/src/render/pbr_types.wgsl | 16 + .../bevy_pbr/src/render/shadow_sampling.wgsl | 28 +- crates/bevy_pbr/src/render/utils.wgsl | 19 + crates/bevy_pbr/src/ssao/gtao.wgsl | 2 +- crates/bevy_pbr/src/ssao/mod.rs | 8 +- crates/bevy_render/src/batching/mod.rs | 6 +- crates/bevy_render/src/render_phase/mod.rs | 3 + docs/cargo_features.md | 1 + examples/3d/transmission.rs | 664 ++++++++++++++++++ examples/README.md | 1 + 34 files changed, 1977 insertions(+), 60 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs create mode 100644 crates/bevy_pbr/src/render/pbr_transmission.wgsl create mode 100644 examples/3d/transmission.rs diff --git a/Cargo.toml b/Cargo.toml index d8b90c43f9aeb..453e18a1485dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -260,6 +260,9 @@ shader_format_glsl = ["bevy_internal/shader_format_glsl"] # Enable support for shaders in SPIR-V shader_format_spirv = ["bevy_internal/shader_format_spirv"] +# Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs +pbr_transmission_textures = ["bevy_internal/pbr_transmission_textures"] + # Enable some limitations to be able to use WebGL2. If not enabled, it will default to WebGPU in Wasm. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. webgl2 = ["bevy_internal/webgl"] @@ -800,6 +803,16 @@ description = "Demonstrates transparency in 3d" category = "3D Rendering" wasm = true +[[example]] +name = "transmission" +path = "examples/3d/transmission.rs" + +[package.metadata.example.transmission] +name = "Transmission" +description = "Showcases light transmission in the PBR material" +category = "3D Rendering" +wasm = true + [[example]] name = "two_passes" path = "examples/3d/two_passes.rs" diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index f8f01286cc84f..33579994c9aca 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -25,6 +25,31 @@ pub struct Camera3d { pub depth_load_op: Camera3dDepthLoadOp, /// The texture usages for the depth texture created for the main 3d pass. pub depth_texture_usages: Camera3dDepthTextureUsage, + /// How many individual steps should be performed in the [`Transmissive3d`](crate::core_3d::Transmissive3d) pass. + /// + /// Roughly corresponds to how many “layers of transparency” are rendered for screen space + /// specular transmissive objects. Each step requires making one additional + /// texture copy, so it's recommended to keep this number to a resonably low value. Defaults to `1`. + /// + /// ### Notes + /// + /// - No copies will be performed if there are no transmissive materials currently being rendered, + /// regardless of this setting. + /// - Setting this to `0` disables the screen-space refraction effect entirely, and falls + /// back to refracting only the environment map light's texture. + /// - If set to more than `0`, any opaque [`clear_color`](Camera3d::clear_color) will obscure the environment + /// map light's texture, preventing it from being visible “through” transmissive materials. If you'd like + /// to still have the environment map show up in your refractions, you can set the clear color's alpha to `0.0`. + /// Keep in mind that depending on the platform and your window settings, this may cause the window to become + /// transparent. + pub screen_space_specular_transmission_steps: usize, + /// The quality of the screen space specular transmission blur effect, applied to whatever's “behind” transmissive + /// objects when their `roughness` is greater than `0.0`. + /// + /// Higher qualities are more GPU-intensive. + /// + /// **Note:** You can get better-looking results at any quality level by enabling TAA. See: [`TemporalAntiAliasPlugin`](crate::experimental::taa::TemporalAntiAliasPlugin). + pub screen_space_specular_transmission_quality: ScreenSpaceTransmissionQuality, } impl Default for Camera3d { @@ -33,6 +58,8 @@ impl Default for Camera3d { clear_color: ClearColorConfig::Default, depth_load_op: Default::default(), depth_texture_usages: TextureUsages::RENDER_ATTACHMENT.into(), + screen_space_specular_transmission_steps: 1, + screen_space_specular_transmission_quality: Default::default(), } } } @@ -77,6 +104,37 @@ impl From for LoadOp { } } +/// The quality of the screen space transmission blur effect, applied to whatever's “behind” transmissive +/// objects when their `roughness` is greater than `0.0`. +/// +/// Higher qualities are more GPU-intensive. +/// +/// **Note:** You can get better-looking results at any quality level by enabling TAA. See: [`TemporalAntiAliasPlugin`](crate::experimental::taa::TemporalAntiAliasPlugin). +#[derive(Resource, Default, Clone, Copy, Reflect, PartialEq, PartialOrd, Debug)] +#[reflect(Resource)] +pub enum ScreenSpaceTransmissionQuality { + /// Best performance at the cost of quality. Suitable for lower end GPUs. (e.g. Mobile) + /// + /// `num_taps` = 4 + Low, + + /// A balanced option between quality and performance. + /// + /// `num_taps` = 8 + #[default] + Medium, + + /// Better quality. Suitable for high end GPUs. (e.g. Desktop) + /// + /// `num_taps` = 16 + High, + + /// The highest quality, suitable for non-realtime rendering. (e.g. Pre-rendered cinematics and photo mode) + /// + /// `num_taps` = 32 + Ultra, +} + #[derive(Bundle)] pub struct Camera3dBundle { pub camera: Camera, diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs new file mode 100644 index 0000000000000..18c04e0d5a828 --- /dev/null +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -0,0 +1,148 @@ +use super::{Camera3d, ViewTransmissionTexture}; +use crate::core_3d::Transmissive3d; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::RenderPhase, + render_resource::{ + Extent3d, LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor, + }, + renderer::RenderContext, + view::{ViewDepthTexture, ViewTarget}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use std::ops::Range; + +/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`] [`RenderPhase`]. +#[derive(Default)] +pub struct MainTransmissivePass3dNode; + +impl ViewNode for MainTransmissivePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static Camera3d, + &'static RenderPhase, + &'static ViewTarget, + Option<&'static ViewTransmissionTexture>, + &'static ViewDepthTexture, + ); + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (camera, camera_3d, transmissive_phase, target, transmission, depth): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + + let physical_target_size = camera.physical_target_size.unwrap(); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("main_transmissive_pass_3d"), + // NOTE: The transmissive pass loads the color buffer as well as overwriting it where appropriate. + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: The transmissive main pass loads the depth buffer and possibly overwrites it + depth_ops: Some(Operations { + load: LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }; + + // Run the transmissive pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered(); + + if !transmissive_phase.items.is_empty() { + let screen_space_specular_transmission_steps = + camera_3d.screen_space_specular_transmission_steps; + if screen_space_specular_transmission_steps > 0 { + let transmission = + transmission.expect("`ViewTransmissionTexture` should exist at this point"); + + // `transmissive_phase.items` are depth sorted, so we split them into N = `screen_space_specular_transmission_steps` + // ranges, rendering them back-to-front in multiple steps, allowing multiple levels of transparency. + // + // Note: For the sake of simplicity, we currently split items evenly among steps. In the future, we + // might want to use a more sophisticated heuristic (e.g. based on view bounds, or with an exponential + // falloff so that nearby objects have more levels of transparency available to them) + for range in split_range( + 0..transmissive_phase.items.len(), + screen_space_specular_transmission_steps, + ) { + // Copy the main texture to the transmission texture, allowing to use the color output of the + // previous step (or of the `Opaque3d` phase, for the first step) as a transmissive color input + render_context.command_encoder().copy_texture_to_texture( + target.main_texture().as_image_copy(), + transmission.texture.as_image_copy(), + Extent3d { + width: physical_target_size.x, + height: physical_target_size.y, + depth_or_array_layers: 1, + }, + ); + + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor.clone()); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // render items in range + transmissive_phase.render_range(&mut render_pass, world, view_entity, range); + } + } else { + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + transmissive_phase.render(&mut render_pass, world, view_entity); + } + } + + Ok(()) + } +} + +/// Splits a [`Range`] into at most `max_num_splits` sub-ranges without overlaps +/// +/// Properly takes into account remainders of inexact divisions (by adding extra +/// elements to the initial sub-ranges as needed) +fn split_range(range: Range, max_num_splits: usize) -> impl Iterator> { + let len = range.end - range.start; + assert!(len > 0, "to be split, a range must not be empty"); + assert!(max_num_splits > 0, "max_num_splits must be at least 1"); + let num_splits = max_num_splits.min(len); + let step = len / num_splits; + let mut rem = len % num_splits; + let mut start = range.start; + + (0..num_splits).map(move |_| { + let extra = if rem > 0 { + rem -= 1; + 1 + } else { + 0 + }; + let end = (start + step + extra).min(range.end); + let result = start..end; + start = end; + result + }) +} diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index b0bee381cc275..f7404efbd7654 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -1,5 +1,6 @@ mod camera_3d; mod main_opaque_pass_3d_node; +mod main_transmissive_pass_3d_node; mod main_transparent_pass_3d_node; pub mod graph { @@ -15,6 +16,7 @@ pub mod graph { pub const END_PREPASSES: &str = "end_prepasses"; pub const START_MAIN_PASS: &str = "start_main_pass"; pub const MAIN_OPAQUE_PASS: &str = "main_opaque_pass"; + pub const MAIN_TRANSMISSIVE_PASS: &str = "main_transmissive_pass"; pub const MAIN_TRANSPARENT_PASS: &str = "main_transparent_pass"; pub const END_MAIN_PASS: &str = "end_main_pass"; pub const BLOOM: &str = "bloom"; @@ -48,17 +50,18 @@ use bevy_render::{ RenderPhase, }, render_resource::{ - CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, TextureFormat, - TextureUsages, + CachedRenderPipelineId, Extent3d, FilterMode, Sampler, SamplerDescriptor, Texture, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, }, renderer::RenderDevice, - texture::TextureCache, - view::ViewDepthTexture, + texture::{BevyDefault, TextureCache}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{nonmax::NonMaxU32, tracing::warn, FloatOrd, HashMap}; use crate::{ + core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode, deferred::{ copy_lighting_id::CopyDeferredLightingIdNode, node::DeferredGBufferPrepassNode, AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, @@ -91,6 +94,7 @@ impl Plugin for Core3dPlugin { render_app .init_resource::>() .init_resource::>() + .init_resource::>() .init_resource::>() .init_resource::>() .init_resource::>() @@ -103,12 +107,14 @@ impl Plugin for Core3dPlugin { ( sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), sort_phase_system::.in_set(RenderSet::PhaseSort), prepare_core_3d_depth_textures.in_set(RenderSet::PrepareResources), + prepare_core_3d_transmission_textures.in_set(RenderSet::PrepareResources), prepare_prepass_textures.in_set(RenderSet::PrepareResources), ), ); @@ -131,6 +137,10 @@ impl Plugin for Core3dPlugin { CORE_3D, MAIN_OPAQUE_PASS, ) + .add_render_graph_node::>( + CORE_3D, + MAIN_TRANSMISSIVE_PASS, + ) .add_render_graph_node::>( CORE_3D, MAIN_TRANSPARENT_PASS, @@ -148,6 +158,7 @@ impl Plugin for Core3dPlugin { END_PREPASSES, START_MAIN_PASS, MAIN_OPAQUE_PASS, + MAIN_TRANSMISSIVE_PASS, MAIN_TRANSPARENT_PASS, END_MAIN_PASS, TONEMAPPING, @@ -282,6 +293,78 @@ impl CachedRenderPipelinePhaseItem for AlphaMask3d { } } +pub struct Transmissive3d { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, +} + +impl PhaseItem for Transmissive3d { + // NOTE: Values increase towards the camera. Back-to-front ordering for transmissive means we need an ascending sort. + type SortKey = FloatOrd; + + /// For now, automatic batching is disabled for transmissive items because their rendering is + /// split into multiple steps depending on [`Camera3d::screen_space_specular_transmission_steps`], + /// which the batching system doesn't currently know about. + /// + /// Having batching enabled would cause the same item to be drawn multiple times across different + /// steps, whenever the batching range crossed a step boundary. + /// + /// Eventually, we could add support for this by having the batching system break up the batch ranges + /// using the same logic as the transmissive pass, but for now it's simpler to just disable batching. + const AUTOMATIC_BATCHING: bool = false; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } +} + +impl CachedRenderPipelinePhaseItem for Transmissive3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + pub struct Transparent3d { pub distance: f32, pub pipeline: CachedRenderPipelineId, @@ -352,6 +435,7 @@ pub fn extract_core_3d_camera_phases( commands.get_or_spawn(entity).insert(( RenderPhase::::default(), RenderPhase::::default(), + RenderPhase::::default(), RenderPhase::::default(), )); } @@ -424,6 +508,7 @@ pub fn prepare_core_3d_depth_textures( ( With>, With>, + With>, With>, ), >, @@ -484,6 +569,96 @@ pub fn prepare_core_3d_depth_textures( } } +#[derive(Component)] +pub struct ViewTransmissionTexture { + pub texture: Texture, + pub view: TextureView, + pub sampler: Sampler, +} + +pub fn prepare_core_3d_transmission_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + views_3d: Query< + ( + Entity, + &ExtractedCamera, + &Camera3d, + &ExtractedView, + &RenderPhase, + ), + ( + With>, + With>, + With>, + ), + >, +) { + let mut textures = HashMap::default(); + for (entity, camera, camera_3d, view, transmissive_3d_phase) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + // Don't prepare a transmission texture if the number of steps is set to 0 + if camera_3d.screen_space_specular_transmission_steps == 0 { + continue; + } + + // Don't prepare a transmission texture if there are no transmissive items to render + if transmissive_3d_phase.items.is_empty() { + continue; + } + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST; + + // The size of the transmission texture + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let format = if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let descriptor = TextureDescriptor { + label: Some("view_transmission_texture"), + size, + mip_level_count: 1, + sample_count: 1, // No need for MSAA, as we'll only copy the main texture here + dimension: TextureDimension::D2, + format, + usage, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("view_transmission_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + + commands.entity(entity).insert(ViewTransmissionTexture { + texture: cached_texture.texture, + view: cached_texture.default_view, + sampler, + }); + } +} + // Disable MSAA and warn if using deferred rendering pub fn check_msaa( mut msaa: ResMut, diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index 92da49b8242b8..494d86900def7 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -316,3 +316,12 @@ fn tone_mapping(in: vec4, color_grading: ColorGrading) -> vec4 { return vec4(color, in.a); } +// This is an **incredibly crude** approximation of the inverse of the tone mapping function. +// We assume here that there's a simple linear relationship between the input and output +// which is not true at all, but useful to at least preserve the overall luminance of colors +// when sampling from an already tonemapped image. (e.g. for transmissive materials when HDR is off) +fn approximate_inverse_tone_mapping(in: vec4, color_grading: ColorGrading) -> vec4 { + let out = tone_mapping(in, color_grading); + let approximate_ratio = length(in.rgb) / length(out.rgb); + return vec4(in.rgb * approximate_ratio, in.a); +} diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index eac57697e66aa..354c5ac07574c 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -8,6 +8,9 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] +[features] +pbr_transmission_textures = [] + [dependencies] # bevy bevy_animation = { path = "../bevy_animation", version = "0.12.0-dev", optional = true } @@ -30,6 +33,9 @@ bevy_utils = { path = "../bevy_utils", version = "0.12.0-dev" } # other gltf = { version = "1.3.0", default-features = false, features = [ "KHR_lights_punctual", + "KHR_materials_transmission", + "KHR_materials_ior", + "KHR_materials_volume", "KHR_materials_unlit", "KHR_materials_emissive_strength", "extras", diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index fa3085e287cf1..abdbc1a3cbe50 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -763,6 +763,56 @@ fn load_material( texture_handle(load_context, &info.texture()) }); + #[cfg(feature = "pbr_transmission_textures")] + let (specular_transmission, specular_transmission_texture) = + material.transmission().map_or((0.0, None), |transmission| { + let transmission_texture: Option> = transmission + .transmission_texture() + .map(|transmission_texture| { + // TODO: handle transmission_texture.tex_coord() (the *set* index for the right texcoords) + texture_handle(load_context, &transmission_texture.texture()) + }); + + (transmission.transmission_factor(), transmission_texture) + }); + + #[cfg(not(feature = "pbr_transmission_textures"))] + let specular_transmission = material + .transmission() + .map_or(0.0, |transmission| transmission.transmission_factor()); + + #[cfg(feature = "pbr_transmission_textures")] + let (thickness, thickness_texture, attenuation_distance, attenuation_color) = material + .volume() + .map_or((0.0, None, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + let thickness_texture: Option> = + volume.thickness_texture().map(|thickness_texture| { + // TODO: handle thickness_texture.tex_coord() (the *set* index for the right texcoords) + texture_handle(load_context, &thickness_texture.texture()) + }); + + ( + volume.thickness_factor(), + thickness_texture, + volume.attenuation_distance(), + volume.attenuation_color(), + ) + }); + + #[cfg(not(feature = "pbr_transmission_textures"))] + let (thickness, attenuation_distance, attenuation_color) = + material + .volume() + .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + ( + volume.thickness_factor(), + volume.attenuation_distance(), + volume.attenuation_color(), + ) + }); + + let ior = material.ior().unwrap_or(1.5); + StandardMaterial { base_color: Color::rgba_linear(color[0], color[1], color[2], color[3]), base_color_texture, @@ -782,6 +832,19 @@ fn load_material( emissive: Color::rgb_linear(emissive[0], emissive[1], emissive[2]) * material.emissive_strength().unwrap_or(1.0), emissive_texture, + specular_transmission, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture, + thickness, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture, + ior, + attenuation_distance, + attenuation_color: Color::rgb_linear( + attenuation_color[0], + attenuation_color[1], + attenuation_color[2], + ), unlit: material.unlit(), alpha_mode: alpha_mode(material), ..Default::default() diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 5dac70366d10c..9600e7d0539ff 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -72,6 +72,9 @@ x11 = ["bevy_winit/x11"] # enable rendering of font glyphs using subpixel accuracy subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] +# Transmission textures in `StandardMaterial`: +pbr_transmission_textures = ["bevy_pbr?/pbr_transmission_textures", "bevy_gltf?/pbr_transmission_textures"] + # Optimise for WebGL2 webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl", "bevy_gizmos?/webgl", "bevy_sprite?/webgl"] diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index dc2cdeb1235d2..c7c9345474bdb 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [features] webgl = [] +pbr_transmission_textures = [] [dependencies] # bevy diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 040d049a56d7a..096c3f6315d1b 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -199,6 +199,10 @@ impl Material for ExtendedMaterial { B::depth_bias(&self.base) } + fn reads_view_transmission_texture(&self) -> bool { + B::reads_view_transmission_texture(&self.base) + } + fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { B::opaque_render_method(&self.base) } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 0623d38b65d31..c73424c86a091 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -73,6 +73,7 @@ pub const PBR_BINDINGS_SHADER_HANDLE: Handle = Handle::weak_from_u128(56 pub const UTILS_HANDLE: Handle = Handle::weak_from_u128(1900548483293416725); pub const CLUSTERED_FORWARD_HANDLE: Handle = Handle::weak_from_u128(166852093121196815); pub const PBR_LIGHTING_HANDLE: Handle = Handle::weak_from_u128(14170772752254856967); +pub const PBR_TRANSMISSION_HANDLE: Handle = Handle::weak_from_u128(77319684653223658032); pub const SHADOWS_HANDLE: Handle = Handle::weak_from_u128(11350275143789590502); pub const SHADOW_SAMPLING_HANDLE: Handle = Handle::weak_from_u128(3145627513789590502); pub const PBR_FRAGMENT_HANDLE: Handle = Handle::weak_from_u128(2295049283805286543); @@ -135,6 +136,12 @@ impl Plugin for PbrPlugin { "render/pbr_lighting.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PBR_TRANSMISSION_HANDLE, + "render/pbr_transmission.wgsl", + Shader::from_wgsl + ); load_internal_asset!( app, SHADOWS_HANDLE, diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 9850d21f8c80e..599bd352933f4 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -598,9 +598,23 @@ impl Default for AmbientLight { #[reflect(Component, Default)] pub struct NotShadowCaster; /// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) not receive shadows. +/// +/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will +/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled, +/// even when [`TransmittedShadowReceiver`] is being used. #[derive(Component, Reflect, Default)] #[reflect(Component, Default)] pub struct NotShadowReceiver; +/// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0` +/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”) +/// +/// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness) +/// (and potentially even baking a thickness texture!) to match the geometry of the mesh, in order to avoid self-shadow artifacts. +/// +/// **Note:** Using [`NotShadowReceiver`] overrides this component. +#[derive(Component, Reflect, Default)] +#[reflect(Component, Default)] +pub struct TransmittedShadowReceiver; /// Add this component to a [`Camera3d`](bevy_core_pipeline::core_3d::Camera3d) /// to control how to anti-alias shadow edges. diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 5a119c42b5d2d..d5884bb65530c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -2,8 +2,10 @@ use crate::*; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ - core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, - experimental::taa::TemporalAntiAliasSettings, + core_3d::{ + AlphaMask3d, Camera3d, Opaque3d, ScreenSpaceTransmissionQuality, Transmissive3d, + Transparent3d, + }, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, }; @@ -15,6 +17,7 @@ use bevy_ecs::{ use bevy_reflect::Reflect; use bevy_render::{ camera::Projection, + camera::TemporalJitter, extract_instances::{ExtractInstancesPlugin, ExtractedInstances}, extract_resource::ExtractResource, mesh::{Mesh, MeshVertexBufferLayout}, @@ -124,6 +127,15 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { 0.0 } + #[inline] + /// Returns whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + fn reads_view_transmission_texture(&self) -> bool { + false + } + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader /// will be used. /// @@ -203,6 +215,7 @@ where render_app .init_resource::>() .add_render_command::>() + .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() @@ -418,10 +431,30 @@ const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { } } +const fn screen_space_specular_transmission_pipeline_key( + screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality, +) -> MeshPipelineKey { + match screen_space_transmissive_blur_quality { + ScreenSpaceTransmissionQuality::Low => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW + } + ScreenSpaceTransmissionQuality::Medium => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM + } + ScreenSpaceTransmissionQuality::High => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH + } + ScreenSpaceTransmissionQuality::Ultra => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA + } + } +} + #[allow(clippy::too_many_arguments)] pub fn queue_material_meshes( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, + transmissive_draw_functions: Res>, transparent_draw_functions: Res>, material_pipeline: Res>, mut pipelines: ResMut>>, @@ -446,10 +479,12 @@ pub fn queue_material_meshes( Has, Has, ), - Option<&TemporalAntiAliasSettings>, + Option<&Camera3d>, + Option<&TemporalJitter>, Option<&Projection>, &mut RenderPhase, &mut RenderPhase, + &mut RenderPhase, &mut RenderPhase, )>, ) where @@ -464,15 +499,18 @@ pub fn queue_material_meshes( shadow_filter_method, ssao, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), - taa_settings, + camera_3d, + temporal_jitter, projection, mut opaque_phase, mut alpha_mask_phase, + mut transmissive_phase, mut transparent_phase, ) in &mut views { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); let draw_transparent_pbr = transparent_draw_functions.read().id::>(); let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) @@ -494,9 +532,10 @@ pub fn queue_material_meshes( view_key |= MeshPipelineKey::DEFERRED_PREPASS; } - if taa_settings.is_some() { - view_key |= MeshPipelineKey::TAA; + if temporal_jitter.is_some() { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; } + let environment_map_loaded = environment_map.is_some_and(|map| map.is_loaded(&images)); if environment_map_loaded { @@ -534,6 +573,11 @@ pub fn queue_material_meshes( if ssao.is_some() { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } + if let Some(camera_3d) = camera_3d { + view_key |= screen_space_specular_transmission_pipeline_key( + camera_3d.screen_space_specular_transmission_quality, + ); + } let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { @@ -588,7 +632,16 @@ pub fn queue_material_meshes( + material.properties.depth_bias; match material.properties.alpha_mode { AlphaMode::Opaque => { - if forward { + if material.properties.reads_view_transmission_texture { + transmissive_phase.add(Transmissive3d { + entity: *visible_entity, + draw_function: draw_transmissive_pbr, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } else if forward { opaque_phase.add(Opaque3d { entity: *visible_entity, draw_function: draw_opaque_pbr, @@ -600,7 +653,16 @@ pub fn queue_material_meshes( } } AlphaMode::Mask(_) => { - if forward { + if material.properties.reads_view_transmission_texture { + transmissive_phase.add(Transmissive3d { + entity: *visible_entity, + draw_function: draw_transmissive_pbr, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + dynamic_offset: None, + }); + } else if forward { alpha_mask_phase.add(AlphaMask3d { entity: *visible_entity, draw_function: draw_alpha_mask_pbr, @@ -688,6 +750,11 @@ pub struct MaterialProperties { /// for meshes with equal depth, to avoid z-fighting. /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. pub depth_bias: f32, + /// Whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + pub reads_view_transmission_texture: bool, } /// Data prepared for a [`Material`] instance. @@ -863,6 +930,7 @@ fn prepare_material( properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), + reads_view_transmission_texture: material.reads_view_transmission_texture(), render_method: method, }, }) diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index a68c1358897c8..77257bc8daadb 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -138,6 +138,152 @@ pub struct StandardMaterial { #[doc(alias = "specular_intensity")] pub reflectance: f32, + /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”) + /// + /// Implemented as a second, flipped [Lambertian diffuse](https://en.wikipedia.org/wiki/Lambertian_reflectance) lobe, + /// which provides an inexpensive but plausible approximation of translucency for thin dieletric objects (e.g. paper, + /// leaves, some fabrics) or thicker volumetric materials with short scattering distances (e.g. porcelain, wax). + /// + /// For specular transmission usecases with refraction (e.g. glass) use the [`StandardMaterial::specular_transmission`] and + /// [`StandardMaterial::ior`] properties instead. + /// + /// - When set to `0.0` (the default) no diffuse light is transmitted; + /// - When set to `1.0` all diffuse light is transmitted through the material; + /// - Values higher than `0.5` will cause more diffuse light to be transmitted than reflected, resulting in a “darker” + /// appearance on the side facing the light than the opposite side. (e.g. plant leaves) + /// + /// ## Notes + /// + /// - The material's [`StandardMaterial::base_color`] also modulates the transmitted light; + /// - To receive transmitted shadows on the diffuse transmission lobe (i.e. the “backside”) of the material, + /// use the [`TransmittedShadowReceiver`] component. + #[doc(alias = "translucency")] + pub diffuse_transmission: f32, + + /// A map that modulates diffuse transmission via its alpha channel. Multiplied by [`StandardMaterial::diffuse_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::diffuse_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[texture(17)] + #[sampler(18)] + #[cfg(feature = "pbr_transmission_textures")] + pub diffuse_transmission_texture: Option>, + + /// The amount of light transmitted _specularly_ through the material (i.e. via refraction) + /// + /// - When set to `0.0` (the default) no light is transmitted. + /// - When set to `1.0` all light is transmitted through the material. + /// + /// The material's [`StandardMaterial::base_color`] also modulates the transmitted light. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::thickness`], [`StandardMaterial::ior`] and [`StandardMaterial::perceptual_roughness`]. + /// + /// ## Performance + /// + /// Specular transmission is implemented as a relatively expensive screen-space effect that allows ocluded objects to be seen through the material, + /// with distortion and blur effects. + /// + /// - [`Camera3d::screen_space_specular_transmission_steps`](bevy_core_pipeline::core_3d::Camera3d::screen_space_specular_transmission_steps) can be used to enable transmissive objects + /// to be seen through other transmissive objects, at the cost of additional draw calls and texture copies; (Use with caution!) + /// - If a simplified approximation of specular transmission using only environment map lighting is sufficient, consider setting + /// [`Camera3d::screen_space_specular_transmission_steps`](bevy_core_pipeline::core_3d::Camera3d::screen_space_specular_transmission_steps) to `0`. + /// - If purely diffuse light transmission is needed, (i.e. “translucency”) consider using [`StandardMaterial::diffuse_transmission`] instead, + /// for a much less expensive effect. + /// - Specular transmission is rendered before alpha blending, so any material with [`AlphaMode::Blend`], [`AlphaMode::Premultiplied`], [`AlphaMode::Add`] or [`AlphaMode::Multiply`] + /// won't be visible through specular transmissive materials. + #[doc(alias = "refraction")] + pub specular_transmission: f32, + + /// A map that modulates specular transmission via its red channel. Multiplied by [`StandardMaterial::specular_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::specular_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[texture(13)] + #[sampler(14)] + #[cfg(feature = "pbr_transmission_textures")] + pub specular_transmission_texture: Option>, + + /// Thickness of the volume beneath the material surface. + /// + /// When set to `0.0` (the default) the material appears as an infinitely-thin film, + /// transmitting light without distorting it. + /// + /// When set to any other value, the material distorts light like a thick lens. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::ior`], or with + /// [`StandardMaterial::diffuse_transmission`]. + #[doc(alias = "volume")] + #[doc(alias = "thin_walled")] + pub thickness: f32, + + /// A map that modulates thickness via its green channel. Multiplied by [`StandardMaterial::thickness`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::thickness`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[texture(15)] + #[sampler(16)] + #[cfg(feature = "pbr_transmission_textures")] + pub thickness_texture: Option>, + + /// The [index of refraction](https://en.wikipedia.org/wiki/Refractive_index) of the material. + /// + /// Defaults to 1.5. + /// + /// | Material | Index of Refraction | + /// |:----------------|:---------------------| + /// | Vacuum | 1 | + /// | Air | 1.00 | + /// | Ice | 1.31 | + /// | Water | 1.33 | + /// | Eyes | 1.38 | + /// | Quartz | 1.46 | + /// | Olive Oil | 1.47 | + /// | Honey | 1.49 | + /// | Acrylic | 1.49 | + /// | Window Glass | 1.52 | + /// | Polycarbonate | 1.58 | + /// | Flint Glass | 1.69 | + /// | Ruby | 1.71 | + /// | Glycerine | 1.74 | + /// | Saphire | 1.77 | + /// | Cubic Zirconia | 2.15 | + /// | Diamond | 2.42 | + /// | Moissanite | 2.65 | + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::thickness`]. + #[doc(alias = "index_of_refraction")] + #[doc(alias = "refraction_index")] + #[doc(alias = "refractive_index")] + pub ior: f32, + + /// How far, on average, light travels through the volume beneath the material's + /// surface before being absorbed. + /// + /// Defaults to [`f32::INFINITY`], i.e. light is never absorbed. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_color`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_distance")] + #[doc(alias = "extinction_distance")] + pub attenuation_distance: f32, + + /// The resulting (non-absorbed) color after white light travels through the attenuation distance. + /// + /// Defaults to [`Color::WHITE`], i.e. no change. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_distance`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_color")] + #[doc(alias = "extinction_color")] + pub attenuation_color: Color, + /// Used to fake the lighting of bumps and dents on a material. /// /// A typical usage would be faking cobblestones on a flat plane mesh in 3D. @@ -343,6 +489,18 @@ impl Default for StandardMaterial { // Expressed in a linear scale and equivalent to 4% reflectance see // reflectance: 0.5, + diffuse_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_texture: None, + specular_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture: None, + thickness: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture: None, + ior: 1.5, + attenuation_color: Color::WHITE, + attenuation_distance: f32::INFINITY, occlusion_texture: None, normal_map_texture: None, flip_normal_map_y: false, @@ -401,6 +559,10 @@ bitflags::bitflags! { const FLIP_NORMAL_MAP_Y = (1 << 7); const FOG_ENABLED = (1 << 8); const DEPTH_MAP = (1 << 9); // Used for parallax mapping + const SPECULAR_TRANSMISSION_TEXTURE = (1 << 10); + const THICKNESS_TEXTURE = (1 << 11); + const DIFFUSE_TRANSMISSION_TEXTURE = (1 << 12); + const ATTENUATION_ENABLED = (1 << 13); const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. @@ -435,6 +597,18 @@ pub struct StandardMaterialUniform { /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] /// defaults to 0.5 which is mapped to 4% reflectance in the shader pub reflectance: f32, + /// Amount of diffuse light transmitted through the material + pub diffuse_transmission: f32, + /// Amount of specular light transmitted through the material + pub specular_transmission: f32, + /// Thickness of the volume underneath the material surface + pub thickness: f32, + /// Index of Refraction + pub ior: f32, + /// How far light travels through the volume underneath the material surface before being absorbed + pub attenuation_distance: f32, + /// Color white light takes after travelling through the attenuation distance underneath the material surface + pub attenuation_color: Vec4, /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. pub flags: u32, /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, @@ -481,6 +655,18 @@ impl AsBindGroupShaderType for StandardMaterial { if self.depth_map.is_some() { flags |= StandardMaterialFlags::DEPTH_MAP; } + #[cfg(feature = "pbr_transmission_textures")] + { + if self.specular_transmission_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TRANSMISSION_TEXTURE; + } + if self.thickness_texture.is_some() { + flags |= StandardMaterialFlags::THICKNESS_TEXTURE; + } + if self.diffuse_transmission_texture.is_some() { + flags |= StandardMaterialFlags::DIFFUSE_TRANSMISSION_TEXTURE; + } + } let has_normal_map = self.normal_map_texture.is_some(); if has_normal_map { let normal_map_id = self.normal_map_texture.as_ref().map(|h| h.id()).unwrap(); @@ -514,12 +700,22 @@ impl AsBindGroupShaderType for StandardMaterial { AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, }; + if self.attenuation_distance.is_finite() { + flags |= StandardMaterialFlags::ATTENUATION_ENABLED; + } + StandardMaterialUniform { base_color: self.base_color.as_linear_rgba_f32().into(), emissive: self.emissive.as_linear_rgba_f32().into(), roughness: self.perceptual_roughness, metallic: self.metallic, reflectance: self.reflectance, + diffuse_transmission: self.diffuse_transmission, + specular_transmission: self.specular_transmission, + thickness: self.thickness, + ior: self.ior, + attenuation_distance: self.attenuation_distance, + attenuation_color: self.attenuation_color.as_linear_rgba_f32().into(), flags: flags.bits(), alpha_cutoff, parallax_depth_scale: self.parallax_depth_scale, @@ -602,8 +798,25 @@ impl Material for StandardMaterial { self.depth_bias } + #[inline] + fn reads_view_transmission_texture(&self) -> bool { + self.specular_transmission > 0.0 + } + #[inline] fn opaque_render_method(&self) -> OpaqueRendererMethod { - self.opaque_render_method + match self.opaque_render_method { + // For now, diffuse transmission doesn't work under deferred rendering as we don't pack + // the required data into the GBuffer. If this material is set to `Auto`, we report it as + // `Forward` so that it's rendered correctly, even when the `DefaultOpaqueRendererMethod` + // is set to `Deferred`. + // + // If the developer explicitly sets the `OpaqueRendererMethod` to `Deferred`, we assume + // they know what they're doing and don't override it. + OpaqueRendererMethod::Auto if self.diffuse_transmission > 0.0 => { + OpaqueRendererMethod::Forward + } + other => other, + } } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index c627f84a8c10a..00e9e4f2f3b39 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -818,6 +818,12 @@ pub fn queue_prepass_material_meshes( | AlphaMode::Multiply => continue, } + if material.properties.reads_view_transmission_texture { + // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` + // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + continue; + } + let forward = match material.properties.render_method { OpaqueRendererMethod::Forward => true, OpaqueRendererMethod::Deferred => false, diff --git a/crates/bevy_pbr/src/prepass/prepass_utils.wgsl b/crates/bevy_pbr/src/prepass/prepass_utils.wgsl index 2d8ec615076af..42f403cd13d3a 100644 --- a/crates/bevy_pbr/src/prepass/prepass_utils.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_utils.wgsl @@ -5,11 +5,10 @@ #ifdef DEPTH_PREPASS fn prepass_depth(frag_coord: vec4, sample_index: u32) -> f32 { #ifdef MULTISAMPLED - let depth_sample = textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); -#else - let depth_sample = textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), 0); -#endif - return depth_sample; + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else // MULTISAMPLED + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), 0); +#endif // MULTISAMPLED } #endif // DEPTH_PREPASS diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index bc0925a164319..8c87d27129c2d 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,7 +1,7 @@ use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, AssetId, Handle}; use bevy_core_pipeline::{ - core_3d::{AlphaMask3d, Opaque3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, + core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, }; use bevy_derive::{Deref, DerefMut}; @@ -132,6 +132,7 @@ impl Plugin for MeshRenderPlugin { ( ( batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, batch_and_prepare_render_phase::, @@ -221,12 +222,13 @@ impl From<&MeshTransforms> for MeshUniform { bitflags::bitflags! { #[repr(transparent)] pub struct MeshFlags: u32 { - const SHADOW_RECEIVER = (1 << 0); + const SHADOW_RECEIVER = (1 << 0); + const TRANSMITTED_SHADOW_RECEIVER = (1 << 1); // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, // then the flag should be set, else it should not be set. - const SIGN_DETERMINANT_MODEL_3X3 = (1 << 31); - const NONE = 0; - const UNINITIALIZED = 0xFFFF; + const SIGN_DETERMINANT_MODEL_3X3 = (1 << 31); + const NONE = 0; + const UNINITIALIZED = 0xFFFF; } } @@ -257,6 +259,7 @@ pub fn extract_meshes( Option<&PreviousGlobalTransform>, &Handle, Has, + Has, Has, Has, )>, @@ -270,6 +273,7 @@ pub fn extract_meshes( previous_transform, handle, not_receiver, + transmitted_receiver, not_caster, no_automatic_batching, )| { @@ -283,6 +287,9 @@ pub fn extract_meshes( } else { MeshFlags::SHADOW_RECEIVER }; + if transmitted_receiver { + flags |= MeshFlags::TRANSMITTED_SHADOW_RECEIVER; + } if transform.matrix3.determinant().is_sign_positive() { flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; } @@ -487,7 +494,7 @@ bitflags::bitflags! { const ENVIRONMENT_MAP = (1 << 8); const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 9); const DEPTH_CLAMP_ORTHO = (1 << 10); - const TAA = (1 << 11); + const TEMPORAL_JITTER = (1 << 11); const MORPH_TARGETS = (1 << 12); const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 @@ -514,6 +521,11 @@ bitflags::bitflags! { const VIEW_PROJECTION_PERSPECTIVE = 1 << Self::VIEW_PROJECTION_SHIFT_BITS; const VIEW_PROJECTION_ORTHOGRAPHIC = 2 << Self::VIEW_PROJECTION_SHIFT_BITS; const VIEW_PROJECTION_RESERVED = 3 << Self::VIEW_PROJECTION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS = Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM = 1 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH = 2 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA = 3 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; } } @@ -541,6 +553,10 @@ impl MeshPipelineKey { const VIEW_PROJECTION_SHIFT_BITS: u32 = Self::SHADOW_FILTER_METHOD_SHIFT_BITS - Self::VIEW_PROJECTION_MASK_BITS.count_ones(); + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS: u32 = 0b11; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS: u32 = Self::VIEW_PROJECTION_SHIFT_BITS + - Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS.count_ones(); + pub fn from_msaa_samples(msaa_samples: u32) -> Self { let msaa_bits = (msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; @@ -661,6 +677,10 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(5)); } + if cfg!(feature = "pbr_transmission_textures") { + shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); + } + let mut bind_group_layout = vec![self.get_view_layout(key.into()).clone()]; if key.msaa_samples() > 1 { @@ -794,8 +814,8 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("ENVIRONMENT_MAP".into()); } - if key.contains(MeshPipelineKey::TAA) { - shader_defs.push("TAA".into()); + if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { + shader_defs.push("TEMPORAL_JITTER".into()); } let shadow_filter_method = @@ -808,6 +828,20 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into()); } + let blur_quality = + key.intersection(MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS); + + shader_defs.push(ShaderDefVal::Int( + "SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS".into(), + match blur_quality { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW => 4, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM => 8, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH => 16, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA => 32, + _ => unreachable!(), // Not possible, since the mask is 2 bits, and we've covered all 4 cases + }, + )); + let format = if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 7412de7a8a5f7..0da870acfe6e7 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -29,5 +29,6 @@ struct MorphWeights { #endif const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u; +const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 2u; // 2^31 - if the flag is set, the sign is positive, else it is negative const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u; diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index ba66561c6d785..853c253209a68 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -1,6 +1,7 @@ use std::array; use bevy_core_pipeline::{ + core_3d::ViewTransmissionTexture, prepass::ViewPrepassTextures, tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, @@ -20,7 +21,7 @@ use bevy_render::{ TextureFormat, TextureSampleType, TextureViewDimension, }, renderer::RenderDevice, - texture::{BevyDefault, FallbackImageCubemap, FallbackImageMsaa, Image}, + texture::{BevyDefault, FallbackImageCubemap, FallbackImageMsaa, FallbackImageZero, Image}, view::{Msaa, ViewUniform, ViewUniforms}, }; @@ -295,6 +296,7 @@ fn layout_entries( let tonemapping_lut_entries = get_lut_bind_group_layout_entries([15, 16]); entries.extend_from_slice(&tonemapping_lut_entries); + // Prepass if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || (cfg!(all(feature = "webgl", target_arch = "wasm32")) && !layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)) @@ -305,6 +307,26 @@ fn layout_entries( )); } + // View Transmission Texture + entries.extend_from_slice(&[ + BindGroupLayoutEntry { + binding: 21, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + multisampled: false, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 22, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + ]); + entries } @@ -356,13 +378,15 @@ pub fn prepare_mesh_view_bind_groups( &ViewClusterBindings, Option<&ScreenSpaceAmbientOcclusionTextures>, Option<&ViewPrepassTextures>, + Option<&ViewTransmissionTexture>, Option<&EnvironmentMapLight>, &Tonemapping, )>, - (images, mut fallback_images, fallback_cubemap): ( + (images, mut fallback_images, fallback_cubemap, fallback_image_zero): ( Res>, FallbackImageMsaa, Res, + Res, ), msaa: Res, globals_buffer: Res, @@ -387,6 +411,7 @@ pub fn prepare_mesh_view_bind_groups( cluster_bindings, ssao_textures, prepass_textures, + transmission_texture, environment_map, tonemapping, ) in &views @@ -443,7 +468,18 @@ pub fn prepare_mesh_view_bind_groups( { entries = entries.extend_with_indices(((index, binding),)); } - } + }; + + let transmission_view = transmission_texture + .map(|transmission| &transmission.view) + .unwrap_or(&fallback_image_zero.texture_view); + + let transmission_sampler = transmission_texture + .map(|transmission| &transmission.sampler) + .unwrap_or(&fallback_image_zero.sampler); + + entries = + entries.extend_with_indices(((21, transmission_view), (22, transmission_sampler))); commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 1e863f4207f5c..6f4293d6d61ca 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -44,7 +44,6 @@ @group(0) @binding(16) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED - #ifdef DEPTH_PREPASS @group(0) @binding(17) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS @@ -72,3 +71,6 @@ #ifdef DEFERRED_PREPASS @group(0) @binding(20) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS + +@group(0) @binding(21) var view_transmission_texture: texture_2d; +@group(0) @binding(22) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/pbr_bindings.wgsl b/crates/bevy_pbr/src/render/pbr_bindings.wgsl index fc5cdd280c2b9..c0e060ecab2a8 100644 --- a/crates/bevy_pbr/src/render/pbr_bindings.wgsl +++ b/crates/bevy_pbr/src/render/pbr_bindings.wgsl @@ -15,3 +15,11 @@ @group(1) @binding(10) var normal_map_sampler: sampler; @group(1) @binding(11) var depth_map_texture: texture_2d; @group(1) @binding(12) var depth_map_sampler: sampler; +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED +@group(1) @binding(13) var specular_transmission_texture: texture_2d; +@group(1) @binding(14) var specular_transmission_sampler: sampler; +@group(1) @binding(15) var thickness_texture: texture_2d; +@group(1) @binding(16) var thickness_sampler: sampler; +@group(1) @binding(17) var diffuse_transmission_texture: texture_2d; +@group(1) @binding(18) var diffuse_transmission_sampler: sampler; +#endif diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index d783d3bca5d15..6472f80d81524 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -100,11 +100,13 @@ fn pbr_input_from_standard_material( // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { - pbr_input.material.reflectance = pbr_bindings::material.reflectance; + pbr_input.material.ior = pbr_bindings::material.ior; + pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color; + pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance; pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff; - // emissive + // emissive // TODO use .a for exposure compensation in HDR var emissive: vec4 = pbr_bindings::material.emissive; #ifdef VERTEX_UVS @@ -128,6 +130,34 @@ fn pbr_input_from_standard_material( pbr_input.material.metallic = metallic; pbr_input.material.perceptual_roughness = perceptual_roughness; + var specular_transmission: f32 = pbr_bindings::material.specular_transmission; +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { + specular_transmission *= textureSample(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv).r; + } +#endif + pbr_input.material.specular_transmission = specular_transmission; + + var thickness: f32 = pbr_bindings::material.thickness; +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { + thickness *= textureSample(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv).g; + } +#endif + // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) + thickness *= length( + (transpose(mesh[in.instance_index].model) * vec4(pbr_input.N, 0.0)).xyz + ); + pbr_input.material.thickness = thickness; + + var diffuse_transmission = pbr_bindings::material.diffuse_transmission; +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { + diffuse_transmission *= textureSample(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv).a; + } +#endif + pbr_input.material.diffuse_transmission = diffuse_transmission; + // occlusion // TODO: Split into diffuse/specular occlusion? var occlusion: vec3 = vec3(1.0); diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 6b8c65459d338..9b4668f3e75ec 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -6,10 +6,12 @@ mesh_view_bindings as view_bindings, mesh_view_types, lighting, + transmission, clustered_forward as clustering, shadows, ambient, - mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT, + mesh_types::{MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT}, + utils::E, } #ifdef ENVIRONMENT_MAP @@ -18,7 +20,6 @@ #import bevy_core_pipeline::tonemapping::{screen_space_dither, powsafe, tone_mapping} - fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) -> vec4 { var color = output_color; let alpha_mode = material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; @@ -156,6 +157,12 @@ fn apply_pbr_lighting( let metallic = in.material.metallic; let perceptual_roughness = in.material.perceptual_roughness; let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let ior = in.material.ior; + let thickness = in.material.thickness; + let diffuse_transmission = in.material.diffuse_transmission; + let specular_transmission = in.material.specular_transmission; + + let specular_transmissive_color = specular_transmission * in.material.base_color.rgb; let occlusion = in.occlusion; @@ -167,8 +174,14 @@ fn apply_pbr_lighting( let reflectance = in.material.reflectance; let F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; - // Diffuse strength inversely related to metallicity - let diffuse_color = output_color.rgb * (1.0 - metallic); + // Diffuse strength is inversely related to metallicity, specular and diffuse transmission + let diffuse_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * (1.0 - diffuse_transmission); + + // Diffuse transmissive strength is inversely related to metallicity and specular transmission, but directly related to diffuse transmission + let diffuse_transmissive_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * diffuse_transmission; + + // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness + let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; let R = reflect(-in.V, in.N); @@ -176,6 +189,9 @@ fn apply_pbr_lighting( var direct_light: vec3 = vec3(0.0); + // Transmitted Light (Specular and Diffuse) + var transmitted_light: vec3 = vec3(0.0); + let view_z = dot(vec4( view_bindings::view.inverse_view[0].z, view_bindings::view.inverse_view[1].z, @@ -195,6 +211,25 @@ fn apply_pbr_lighting( } let light_contrib = lighting::point_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); direct_light += light_contrib * shadow; + + if diffuse_transmission > 0.0 { + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // f_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); + } + let light_contrib = lighting::point_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + transmitted_light += light_contrib * transmitted_shadow; + } } // Spot lights (direct) @@ -208,6 +243,25 @@ fn apply_pbr_lighting( } let light_contrib = lighting::spot_light(in.world_position.xyz, light_id, roughness, NdotV, in.N, in.V, R, F0, f_ab, diffuse_color); direct_light += light_contrib * shadow; + + if diffuse_transmission > 0.0 { + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // f_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::point_lights.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); + } + let light_contrib = lighting::spot_light(diffuse_transmissive_lobe_world_position.xyz, light_id, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + transmitted_light += light_contrib * transmitted_shadow; + } } // directional lights (direct) @@ -223,22 +277,104 @@ fn apply_pbr_lighting( light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z); #endif direct_light += light_contrib * shadow; + + if diffuse_transmission > 0.0 { + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // f_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_directional_shadow(i, diffuse_transmissive_lobe_world_position, -in.world_normal, view_z); + } + let light_contrib = lighting::directional_light(i, 1.0, 1.0, -in.N, -in.V, vec3(0.0), vec3(0.0), vec2(0.1), diffuse_transmissive_color); + transmitted_light += light_contrib * transmitted_shadow; + } } // Ambient light (indirect) var indirect_light = ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, occlusion); + if diffuse_transmission > 0.0 { + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // perceptual_roughness = 1.0; + // NdotV = 1.0; + // F0 = vec3(0.0) + // occlusion = vec3(1.0) + transmitted_light += ambient::ambient_light(diffuse_transmissive_lobe_world_position, -in.N, -in.V, 1.0, diffuse_transmissive_color, vec3(0.0), 1.0, vec3(1.0)); + } + // Environment map light (indirect) #ifdef ENVIRONMENT_MAP let environment_light = environment_map::environment_map_light(perceptual_roughness, roughness, diffuse_color, NdotV, f_ab, in.N, R, F0); indirect_light += (environment_light.diffuse * occlusion) + environment_light.specular; + + // we'll use the specular component of the transmitted environment + // light in the call to `specular_transmissive_light()` below + var specular_transmitted_environment_light = vec3(0.0); + + if diffuse_transmission > 0.0 || specular_transmission > 0.0 { + // NOTE: We use the diffuse transmissive color, inverted normal and view vectors, + // and the following simplified values for the transmitted environment light contribution + // approximation: + // + // diffuse_color = vec3(1.0) // later we use `diffuse_transmissive_color` and `specular_transmissive_color` + // NdotV = 1.0; + // R = T // see definition below + // F0 = vec3(1.0) + // occlusion = 1.0 + // + // (This one is slightly different from the other light types above, because the environment + // map light returns both diffuse and specular components separately, and we want to use both) + + let T = -normalize( + in.V + // start with view vector at entry point + refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point + ); // normalize to find exit point view vector + + let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light(perceptual_roughness, roughness, vec3(1.0), 1.0, f_ab, -in.N, T, vec3(1.0)); + transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; + specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; + } +#else + // If there's no environment map light, there's no transmitted environment + // light specular component, so we can just hardcode it to zero. + let specular_transmitted_environment_light = vec3(0.0); #endif let emissive_light = emissive.rgb * output_color.a; + if specular_transmission > 0.0 { + transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; + } + + if (in.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT) != 0u { + // We reuse the `atmospheric_fog()` function here, as it's fundamentally + // equivalent to the attenuation that takes place inside the material volume, + // and will allow us to eventually hook up subsurface scattering more easily + var attenuation_fog: mesh_view_types::Fog; + attenuation_fog.base_color.a = 1.0; + attenuation_fog.be = pow(1.0 - in.material.attenuation_color.rgb, vec3(E)) / in.material.attenuation_distance; + // TODO: Add the subsurface scattering factor below + // attenuation_fog.bi = /* ... */ + transmitted_light = bevy_pbr::fog::atmospheric_fog( + attenuation_fog, vec4(transmitted_light, 1.0), thickness, + vec3(0.0) // TODO: Pass in (pre-attenuated) scattered light contribution here + ).rgb; + } + // Total light output_color = vec4( - direct_light + indirect_light + emissive_light, + transmitted_light + direct_light + indirect_light + emissive_light, output_color.a ); diff --git a/crates/bevy_pbr/src/render/pbr_transmission.wgsl b/crates/bevy_pbr/src/render/pbr_transmission.wgsl new file mode 100644 index 0000000000000..61720ba93e6aa --- /dev/null +++ b/crates/bevy_pbr/src/render/pbr_transmission.wgsl @@ -0,0 +1,181 @@ +#define_import_path bevy_pbr::transmission + +#import bevy_pbr::{ + lighting, + prepass_utils, + utils::{PI, interleaved_gradient_noise}, + utils, + mesh_view_bindings as view_bindings, +}; + +#import bevy_core_pipeline::tonemapping::{ + approximate_inverse_tone_mapping +}; + +fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { + // Calculate the ratio between refaction indexes. Assume air/vacuum for the space outside the mesh + let eta = 1.0 / ior; + + // Calculate incidence vector (opposite to view vector) and its dot product with the mesh normal + let I = -V; + let NdotI = dot(N, I); + + // Calculate refracted direction using Snell's law + let k = 1.0 - eta * eta * (1.0 - NdotI * NdotI); + let T = eta * I - (eta * NdotI + sqrt(k)) * N; + + // Calculate the exit position of the refracted ray, by propagating refacted direction through thickness + let exit_position = world_position.xyz + T * thickness; + + // Transform exit_position into clip space + let clip_exit_position = view_bindings::view.view_proj * vec4(exit_position, 1.0); + + // Scale / offset position so that coordinate is in right space for sampling transmissive background texture + let offset_position = (clip_exit_position.xy / clip_exit_position.w) * vec2(0.5, -0.5) + 0.5; + + // Fetch background color + var background_color: vec4; + if perceptual_roughness == 0.0 { + // If the material has zero roughness, we can use a faster approach without the blur + background_color = fetch_transmissive_background_non_rough(offset_position, frag_coord); + } else { + background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness); + } + + // Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted) + let MinusNdotT = dot(-N, T); + + // Calculate 1.0 - fresnel factor (how much light is _NOT_ reflected, i.e. how much is transmitted) + let F = vec3(1.0) - lighting::fresnel(F0, MinusNdotT); + + // Calculate final color by applying fresnel multiplied specular transmissive color to a mix of background color and transmitted specular environment light + return F * specular_transmissive_color * mix(transmitted_environment_light_specular, background_color.rgb, background_color.a); +} + +fn fetch_transmissive_background_non_rough(offset_position: vec2, frag_coord: vec3) -> vec4 { + var background_color = textureSample( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + offset_position, + ); + +#ifdef DEPTH_PREPASS + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + background_color.a = 0.0; + } +#endif + +#ifdef TONEMAP_IN_SHADER + background_color = approximate_inverse_tone_mapping(background_color, view_bindings::view.color_grading); +#endif + + return background_color; +} + +fn fetch_transmissive_background(offset_position: vec2, frag_coord: vec3, view_z: f32, perceptual_roughness: f32) -> vec4 { + // Calculate view aspect ratio, used to scale offset so that it's proportionate + let aspect = view_bindings::view.viewport.z / view_bindings::view.viewport.w; + + // Calculate how “blurry” the transmission should be. + // Blur is more or less eyeballed to look approximately “right”, since the “correct” + // approach would involve projecting many scattered rays and figuring out their individual + // exit positions. IRL, light rays can be scattered when entering/exiting a material (due to + // roughness) or inside the material (due to subsurface scattering). Here, we only consider + // the first scenario. + // + // Blur intensity is: + // - proportional to the square of `perceptual_roughness` + // - proportional to the inverse of view z + let blur_intensity = (perceptual_roughness * perceptual_roughness) / view_z; + +#ifdef SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS + let num_taps = #{SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS}; // Controlled by the `Camera3d::screen_space_specular_transmission_quality` property +#else + let num_taps = 8; // Fallback to 8 taps, if not specified +#endif + let num_spirals = i32(ceil(f32(num_taps) / 8.0)); +#ifdef TEMPORAL_JITTER + let random_angle = interleaved_gradient_noise(frag_coord.xy, view_bindings::globals.frame_count); +#else + let random_angle = interleaved_gradient_noise(frag_coord.xy, 0u); +#endif + // Pixel checkerboard pattern (helps make the interleaved gradient noise pattern less visible) + let pixel_checkboard = ( +#ifdef TEMPORAL_JITTER + // 0 or 1 on even/odd pixels, alternates every frame + (i32(frag_coord.x) + i32(frag_coord.y) + i32(view_bindings::globals.frame_count)) % 2 +#else + // 0 or 1 on even/odd pixels + (i32(frag_coord.x) + i32(frag_coord.y)) % 2 +#endif + ); + + var result = vec4(0.0); + for (var i: i32 = 0; i < num_taps; i = i + 1) { + let current_spiral = (i >> 3u); + let angle = (random_angle + f32(current_spiral) / f32(num_spirals)) * 2.0 * PI; + let m = vec2(sin(angle), cos(angle)); + let rotation_matrix = mat2x2( + m.y, -m.x, + m.x, m.y + ); + + // Get spiral offset + var spiral_offset: vec2; + switch i & 7 { + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) + // TODO: Figure out a more reasonable way of doing this, as WGSL + // seems to only allow constant indexes into constant arrays at the moment. + // The downstream shader compiler should be able to optimize this into a single + // constant when unrolling the for loop, but it's still not ideal. + case 0: { spiral_offset = utils::SPIRAL_OFFSET_0_; } // Note: We go even first and then odd, so that the lowest + case 1: { spiral_offset = utils::SPIRAL_OFFSET_2_; } // quality possible (which does 4 taps) still does a full spiral + case 2: { spiral_offset = utils::SPIRAL_OFFSET_4_; } // instead of just the first half of it + case 3: { spiral_offset = utils::SPIRAL_OFFSET_6_; } + case 4: { spiral_offset = utils::SPIRAL_OFFSET_1_; } + case 5: { spiral_offset = utils::SPIRAL_OFFSET_3_; } + case 6: { spiral_offset = utils::SPIRAL_OFFSET_5_; } + case 7: { spiral_offset = utils::SPIRAL_OFFSET_7_; } + default: {} + } + + // Make each consecutive spiral slightly smaller than the previous one + spiral_offset *= 1.0 - (0.5 * f32(current_spiral + 1) / f32(num_spirals)); + + // Rotate and correct for aspect ratio + let rotated_spiral_offset = (rotation_matrix * spiral_offset) * vec2(1.0, aspect); + + // Calculate final offset position, with blur and spiral offset + let modified_offset_position = offset_position + rotated_spiral_offset * blur_intensity * (1.0 - f32(pixel_checkboard) * 0.1); + + // Sample the view transmission texture at the offset position + noise offset, to get the background color + var sample = textureSample( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + modified_offset_position, + ); + +#ifdef DEPTH_PREPASS + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(modified_offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + sample = vec4(0.0); + } +#endif + + // As blur intensity grows higher, gradually limit *very bright* color RGB values towards a + // maximum length of 1.0 to prevent stray “firefly” pixel artifacts. This can potentially make + // very strong emissive meshes appear much dimmer, but the artifacts are noticeable enough to + // warrant this treatment. + let normalized_rgb = normalize(sample.rgb); + result += vec4(min(sample.rgb, normalized_rgb / saturate(blur_intensity / 2.0)), sample.a); + } + + result /= f32(num_taps); + +#ifdef TONEMAP_IN_SHADER + result = approximate_inverse_tone_mapping(result, view_bindings::view.color_grading); +#endif + + return result; +} diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index c9041fe017a9b..ff2fe8801369e 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -6,6 +6,12 @@ struct StandardMaterial { perceptual_roughness: f32, metallic: f32, reflectance: f32, + diffuse_transmission: f32, + specular_transmission: f32, + thickness: f32, + ior: f32, + attenuation_distance: f32, + attenuation_color: vec4, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, alpha_cutoff: f32, @@ -30,6 +36,10 @@ const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u; const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u; const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 256u; const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 512u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1024u; +const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 2048u; +const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 4096u; +const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 8192u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) @@ -51,6 +61,12 @@ fn standard_material_new() -> StandardMaterial { material.perceptual_roughness = 0.5; material.metallic = 0.00; material.reflectance = 0.5; + material.diffuse_transmission = 0.0; + material.specular_transmission = 0.0; + material.thickness = 0.0; + material.ior = 1.5; + material.attenuation_distance = 1.0; + material.attenuation_color = vec4(1.0, 1.0, 1.0, 1.0); material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; material.alpha_cutoff = 0.5; material.parallax_depth_scale = 0.1; diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index 0a93d5468b06b..c1f0ec449e6ae 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -2,7 +2,8 @@ #import bevy_pbr::{ mesh_view_bindings as view_bindings, - utils::PI, + utils::{PI, interleaved_gradient_noise}, + utils, } // Do the lookup, using HW 2x2 PCF and comparison @@ -70,13 +71,6 @@ fn sample_shadow_map_castano_thirteen(light_local: vec2, depth: f32, array_ return sum * (1.0 / 144.0); } -// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence -fn interleaved_gradient_noise(pixel_coordinates: vec2) -> f32 { - let frame = f32(view_bindings::globals.frame_count % 64u); - let xy = pixel_coordinates + 5.588238 * frame; - return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y)); -} - fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { return min2 + (value - min1) * (max2 - min2) / (max1 - min1); } @@ -84,7 +78,7 @@ fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); - let random_angle = 2.0 * PI * interleaved_gradient_noise(light_local * shadow_map_size); + let random_angle = 2.0 * PI * interleaved_gradient_noise(light_local * shadow_map_size, view_bindings::globals.frame_count); let m = vec2(sin(random_angle), cos(random_angle)); let rotation_matrix = mat2x2( m.y, -m.x, @@ -96,14 +90,14 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_ let uv_offset_scale = f / (texel_size * shadow_map_size); // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) - let sample_offset1 = (rotation_matrix * vec2(-0.7071, 0.7071)) * uv_offset_scale; - let sample_offset2 = (rotation_matrix * vec2(-0.0000, -0.8750)) * uv_offset_scale; - let sample_offset3 = (rotation_matrix * vec2( 0.5303, 0.5303)) * uv_offset_scale; - let sample_offset4 = (rotation_matrix * vec2(-0.6250, -0.0000)) * uv_offset_scale; - let sample_offset5 = (rotation_matrix * vec2( 0.3536, -0.3536)) * uv_offset_scale; - let sample_offset6 = (rotation_matrix * vec2(-0.0000, 0.3750)) * uv_offset_scale; - let sample_offset7 = (rotation_matrix * vec2(-0.1768, -0.1768)) * uv_offset_scale; - let sample_offset8 = (rotation_matrix * vec2( 0.1250, 0.0000)) * uv_offset_scale; + let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; + let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale; + let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale; + let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale; + let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale; + let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale; + let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale; + let sample_offset8 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale; var sum = 0.0; sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index); diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 89592f89cbf5e..fb3ef2d1f1b32 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -48,3 +48,22 @@ fn octahedral_decode(v: vec2) -> vec3 { n = vec3(n.xy + w, n.z); return normalize(n); } + +// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence +fn interleaved_gradient_noise(pixel_coordinates: vec2, frame: u32) -> f32 { + let xy = pixel_coordinates + 5.588238 * f32(frame % 64u); + return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y)); +} + +// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) +// TODO: Use an array here instead of a bunch of constants, once arrays work properly under DX12. +// NOTE: The names have a final underscore to avoid the following error: +// `Composable module identifiers must not require substitution according to naga writeback rules` +const SPIRAL_OFFSET_0_ = vec2(-0.7071, 0.7071); +const SPIRAL_OFFSET_1_ = vec2(-0.0000, -0.8750); +const SPIRAL_OFFSET_2_ = vec2( 0.5303, 0.5303); +const SPIRAL_OFFSET_3_ = vec2(-0.6250, -0.0000); +const SPIRAL_OFFSET_4_ = vec2( 0.3536, -0.3536); +const SPIRAL_OFFSET_5_ = vec2(-0.0000, 0.3750); +const SPIRAL_OFFSET_6_ = vec2(-0.1768, -0.1768); +const SPIRAL_OFFSET_7_ = vec2( 0.1250, 0.0000); diff --git a/crates/bevy_pbr/src/ssao/gtao.wgsl b/crates/bevy_pbr/src/ssao/gtao.wgsl index 075612fd508f5..a735fef2bc349 100644 --- a/crates/bevy_pbr/src/ssao/gtao.wgsl +++ b/crates/bevy_pbr/src/ssao/gtao.wgsl @@ -26,7 +26,7 @@ fn load_noise(pixel_coordinates: vec2) -> vec2 { var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; -#ifdef TEMPORAL_NOISE +#ifdef TEMPORAL_JITTER index += 288u * (globals.frame_count % 64u); #endif diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 6ad1d0116743a..62a7d4e5a4a6a 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -560,7 +560,7 @@ impl FromWorld for SsaoPipelines { #[derive(PartialEq, Eq, Hash, Clone)] struct SsaoPipelineKey { ssao_settings: ScreenSpaceAmbientOcclusionSettings, - temporal_noise: bool, + temporal_jitter: bool, } impl SpecializedComputePipeline for SsaoPipelines { @@ -577,8 +577,8 @@ impl SpecializedComputePipeline for SsaoPipelines { ), ]; - if key.temporal_noise { - shader_defs.push("TEMPORAL_NOISE".into()); + if key.temporal_jitter { + shader_defs.push("TEMPORAL_JITTER".into()); } ComputePipelineDescriptor { @@ -731,7 +731,7 @@ fn prepare_ssao_pipelines( &pipeline, SsaoPipelineKey { ssao_settings: ssao_settings.clone(), - temporal_noise: temporal_jitter.is_some(), + temporal_jitter: temporal_jitter.is_some(), }, ); diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs index 16633b77b8a4f..bb4fe79670b18 100644 --- a/crates/bevy_render/src/batching/mod.rs +++ b/crates/bevy_render/src/batching/mod.rs @@ -97,7 +97,11 @@ pub fn batch_and_prepare_render_phase>, + mut materials: ResMut>, + asset_server: Res, +) { + let icosphere_mesh = meshes.add( + Mesh::try_from(shape::Icosphere { + radius: 0.9, + subdivisions: 7, + }) + .unwrap(), + ); + + let cube_mesh = meshes.add(Mesh::from(shape::Cube { size: 0.7 })); + + let plane_mesh = meshes.add(shape::Plane::from_size(2.0).into()); + + let cylinder_mesh = meshes.add( + Mesh::try_from(shape::Cylinder { + radius: 0.5, + height: 2.0, + resolution: 50, + segments: 1, + }) + .unwrap(), + ); + + // Cube #1 + commands.spawn(( + PbrBundle { + mesh: cube_mesh.clone(), + material: materials.add(StandardMaterial { ..default() }), + transform: Transform::from_xyz(0.25, 0.5, -2.0).with_rotation(Quat::from_euler( + EulerRot::XYZ, + 1.4, + 3.7, + 21.3, + )), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: false, + diffuse_transmission: false, + }, + )); + + // Cube #2 + commands.spawn(( + PbrBundle { + mesh: cube_mesh, + material: materials.add(StandardMaterial { ..default() }), + transform: Transform::from_xyz(-0.75, 0.7, -2.0).with_rotation(Quat::from_euler( + EulerRot::XYZ, + 0.4, + 2.3, + 4.7, + )), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: false, + diffuse_transmission: false, + }, + )); + + // Candle + commands.spawn(( + PbrBundle { + mesh: cylinder_mesh, + material: materials.add(StandardMaterial { + base_color: Color::rgba(0.9, 0.2, 0.3, 1.0), + diffuse_transmission: 0.7, + perceptual_roughness: 0.32, + thickness: 0.2, + ..default() + }), + transform: Transform::from_xyz(-1.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: false, + diffuse_transmission: true, + }, + )); + + // Candle Flame + commands.spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + emissive: Color::ANTIQUE_WHITE * 20.0 + Color::ORANGE_RED * 4.0, + diffuse_transmission: 1.0, + ..default() + }), + transform: Transform::from_xyz(-1.0, 1.15, 0.0).with_scale(Vec3::new(0.1, 0.2, 0.1)), + ..default() + }, + Flicker, + NotShadowCaster, + )); + + // Glass Sphere + commands.spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + specular_transmission: 0.9, + diffuse_transmission: 1.0, + thickness: 1.8, + ior: 1.5, + perceptual_roughness: 0.12, + ..default() + }), + transform: Transform::from_xyz(1.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: true, + diffuse_transmission: false, + }, + )); + + // R Sphere + commands.spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color: Color::RED, + specular_transmission: 0.9, + diffuse_transmission: 1.0, + thickness: 1.8, + ior: 1.5, + perceptual_roughness: 0.12, + ..default() + }), + transform: Transform::from_xyz(1.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: true, + diffuse_transmission: false, + }, + )); + + // G Sphere + commands.spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color: Color::GREEN, + specular_transmission: 0.9, + diffuse_transmission: 1.0, + thickness: 1.8, + ior: 1.5, + perceptual_roughness: 0.12, + ..default() + }), + transform: Transform::from_xyz(0.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: true, + diffuse_transmission: false, + }, + )); + + // B Sphere + commands.spawn(( + PbrBundle { + mesh: icosphere_mesh, + material: materials.add(StandardMaterial { + base_color: Color::BLUE, + specular_transmission: 0.9, + diffuse_transmission: 1.0, + thickness: 1.8, + ior: 1.5, + perceptual_roughness: 0.12, + ..default() + }), + transform: Transform::from_xyz(-1.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: true, + diffuse_transmission: false, + }, + )); + + // Chessboard Plane + let black_material = materials.add(StandardMaterial { + base_color: Color::BLACK, + reflectance: 0.3, + perceptual_roughness: 0.8, + ..default() + }); + + let white_material = materials.add(StandardMaterial { + base_color: Color::WHITE, + reflectance: 0.3, + perceptual_roughness: 0.8, + ..default() + }); + + for x in -3..4 { + for z in -3..4 { + commands.spawn(( + PbrBundle { + mesh: plane_mesh.clone(), + material: if (x + z) % 2 == 0 { + black_material.clone() + } else { + white_material.clone() + }, + transform: Transform::from_xyz(x as f32 * 2.0, -1.0, z as f32 * 2.0), + ..default() + }, + ExampleControls { + color: true, + specular_transmission: false, + diffuse_transmission: false, + }, + )); + } + } + + // Paper + commands.spawn(( + PbrBundle { + mesh: plane_mesh, + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + diffuse_transmission: 0.6, + perceptual_roughness: 0.8, + reflectance: 1.0, + double_sided: true, + cull_mode: None, + ..default() + }), + transform: Transform::from_xyz(0.0, 0.5, -3.0) + .with_scale(Vec3::new(2.0, 1.0, 1.0)) + .with_rotation(Quat::from_euler(EulerRot::XYZ, PI / 2.0, 0.0, 0.0)), + ..default() + }, + TransmittedShadowReceiver, + ExampleControls { + specular_transmission: false, + color: false, + diffuse_transmission: true, + }, + )); + + // Candle Light + commands.spawn(( + PointLightBundle { + transform: Transform::from_xyz(-1.0, 1.7, 0.0), + point_light: PointLight { + color: Color::ANTIQUE_WHITE * 0.8 + Color::ORANGE_RED * 0.2, + intensity: 1600.0, + radius: 0.2, + range: 5.0, + shadows_enabled: true, + ..default() + }, + ..default() + }, + Flicker, + )); + + // Camera + commands.spawn(( + Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + transform: Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y), + color_grading: ColorGrading { + exposure: -2.0, + post_saturation: 1.2, + ..default() + }, + tonemapping: Tonemapping::TonyMcMapface, + ..default() + }, + #[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))] + TemporalAntiAliasBundle::default(), + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + }, + BloomSettings::default(), + )); + + // Controls Text + let text_style = TextStyle { + font_size: 18.0, + color: Color::WHITE, + ..Default::default() + }; + + commands.spawn(( + TextBundle::from_section("", text_style).with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ExampleDisplay, + )); +} + +#[derive(Component)] +struct Flicker; + +#[derive(Component)] +struct ExampleControls { + diffuse_transmission: bool, + specular_transmission: bool, + color: bool, +} + +struct ExampleState { + diffuse_transmission: f32, + specular_transmission: f32, + thickness: f32, + ior: f32, + perceptual_roughness: f32, + reflectance: f32, + auto_camera: bool, +} + +#[derive(Component)] +struct ExampleDisplay; + +impl Default for ExampleState { + fn default() -> Self { + ExampleState { + diffuse_transmission: 0.5, + specular_transmission: 0.9, + thickness: 1.8, + ior: 1.5, + perceptual_roughness: 0.12, + reflectance: 0.5, + auto_camera: true, + } + } +} + +#[allow(clippy::too_many_arguments)] +fn example_control_system( + mut commands: Commands, + mut materials: ResMut>, + controllable: Query<(&Handle, &ExampleControls)>, + mut camera: Query< + ( + Entity, + &mut Camera, + &mut Camera3d, + &mut Transform, + Option<&DepthPrepass>, + Option<&TemporalJitter>, + ), + With, + >, + mut display: Query<&mut Text, With>, + mut state: Local, + time: Res