StandardMaterial Light Transmission (#8015)

# Objective

<img width="1920" alt="Screenshot 2023-04-26 at 01 07 34"
src="https://user-images.githubusercontent.com/418473/234467578-0f34187b-5863-4ea1-88e9-7a6bb8ce8da3.png">

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`
This commit is contained in:
Marco Buono 2023-10-31 17:59:02 -03:00 committed by GitHub
parent d67fbd5e90
commit 44928e0df4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1977 additions and 60 deletions

View file

@ -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"

View file

@ -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<Camera3dDepthLoadOp> for LoadOp<f32> {
}
}
/// 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,

View file

@ -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<Transmissive3d>,
&'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<usize>, max_num_splits: usize) -> impl Iterator<Item = Range<usize>> {
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
})
}

View file

@ -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::<DrawFunctions<Opaque3d>>()
.init_resource::<DrawFunctions<AlphaMask3d>>()
.init_resource::<DrawFunctions<Transmissive3d>>()
.init_resource::<DrawFunctions<Transparent3d>>()
.init_resource::<DrawFunctions<Opaque3dPrepass>>()
.init_resource::<DrawFunctions<AlphaMask3dPrepass>>()
@ -103,12 +107,14 @@ impl Plugin for Core3dPlugin {
(
sort_phase_system::<Opaque3d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<AlphaMask3d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Transmissive3d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Transparent3d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Opaque3dPrepass>.in_set(RenderSet::PhaseSort),
sort_phase_system::<AlphaMask3dPrepass>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Opaque3dDeferred>.in_set(RenderSet::PhaseSort),
sort_phase_system::<AlphaMask3dDeferred>.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::<ViewNodeRunner<MainTransmissivePass3dNode>>(
CORE_3D,
MAIN_TRANSMISSIVE_PASS,
)
.add_render_graph_node::<ViewNodeRunner<MainTransparentPass3dNode>>(
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<u32>,
pub dynamic_offset: Option<NonMaxU32>,
}
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<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn dynamic_offset(&self) -> Option<NonMaxU32> {
self.dynamic_offset
}
#[inline]
fn dynamic_offset_mut(&mut self) -> &mut Option<NonMaxU32> {
&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::<Opaque3d>::default(),
RenderPhase::<AlphaMask3d>::default(),
RenderPhase::<Transmissive3d>::default(),
RenderPhase::<Transparent3d>::default(),
));
}
@ -424,6 +508,7 @@ pub fn prepare_core_3d_depth_textures(
(
With<RenderPhase<Opaque3d>>,
With<RenderPhase<AlphaMask3d>>,
With<RenderPhase<Transmissive3d>>,
With<RenderPhase<Transparent3d>>,
),
>,
@ -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<TextureCache>,
render_device: Res<RenderDevice>,
views_3d: Query<
(
Entity,
&ExtractedCamera,
&Camera3d,
&ExtractedView,
&RenderPhase<Transmissive3d>,
),
(
With<RenderPhase<Opaque3d>>,
With<RenderPhase<AlphaMask3d>>,
With<RenderPhase<Transparent3d>>,
),
>,
) {
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<Msaa>,

View file

@ -316,3 +316,12 @@ fn tone_mapping(in: vec4<f32>, color_grading: ColorGrading) -> vec4<f32> {
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<f32>, color_grading: ColorGrading) -> vec4<f32> {
let out = tone_mapping(in, color_grading);
let approximate_ratio = length(in.rgb) / length(out.rgb);
return vec4(in.rgb * approximate_ratio, in.a);
}

View file

@ -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",

View file

@ -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<Handle<Image>> = 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<Handle<Image>> =
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()

View file

@ -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"]

View file

@ -10,6 +10,7 @@ keywords = ["bevy"]
[features]
webgl = []
pbr_transmission_textures = []
[dependencies]
# bevy

View file

@ -199,6 +199,10 @@ impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
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)
}

View file

@ -73,6 +73,7 @@ pub const PBR_BINDINGS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(56
pub const UTILS_HANDLE: Handle<Shader> = Handle::weak_from_u128(1900548483293416725);
pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> = Handle::weak_from_u128(166852093121196815);
pub const PBR_LIGHTING_HANDLE: Handle<Shader> = Handle::weak_from_u128(14170772752254856967);
pub const PBR_TRANSMISSION_HANDLE: Handle<Shader> = Handle::weak_from_u128(77319684653223658032);
pub const SHADOWS_HANDLE: Handle<Shader> = Handle::weak_from_u128(11350275143789590502);
pub const SHADOW_SAMPLING_HANDLE: Handle<Shader> = Handle::weak_from_u128(3145627513789590502);
pub const PBR_FRAGMENT_HANDLE: Handle<Shader> = 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,

View file

@ -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.

View file

@ -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::<DrawFunctions<Shadow>>()
.add_render_command::<Shadow, DrawPrepass<M>>()
.add_render_command::<Transmissive3d, DrawMaterial<M>>()
.add_render_command::<Transparent3d, DrawMaterial<M>>()
.add_render_command::<Opaque3d, DrawMaterial<M>>()
.add_render_command::<AlphaMask3d, DrawMaterial<M>>()
@ -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<M: Material>(
opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
alpha_mask_draw_functions: Res<DrawFunctions<AlphaMask3d>>,
transmissive_draw_functions: Res<DrawFunctions<Transmissive3d>>,
transparent_draw_functions: Res<DrawFunctions<Transparent3d>>,
material_pipeline: Res<MaterialPipeline<M>>,
mut pipelines: ResMut<SpecializedMeshPipelines<MaterialPipeline<M>>>,
@ -446,10 +479,12 @@ pub fn queue_material_meshes<M: Material>(
Has<MotionVectorPrepass>,
Has<DeferredPrepass>,
),
Option<&TemporalAntiAliasSettings>,
Option<&Camera3d>,
Option<&TemporalJitter>,
Option<&Projection>,
&mut RenderPhase<Opaque3d>,
&mut RenderPhase<AlphaMask3d>,
&mut RenderPhase<Transmissive3d>,
&mut RenderPhase<Transparent3d>,
)>,
) where
@ -464,15 +499,18 @@ pub fn queue_material_meshes<M: Material>(
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::<DrawMaterial<M>>();
let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::<DrawMaterial<M>>();
let draw_transmissive_pbr = transmissive_draw_functions.read().id::<DrawMaterial<M>>();
let draw_transparent_pbr = transparent_draw_functions.read().id::<DrawMaterial<M>>();
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
@ -494,9 +532,10 @@ pub fn queue_material_meshes<M: Material>(
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<M: Material>(
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<M: Material>(
+ 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<M: Material>(
}
}
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<M: 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,
},
})

View file

@ -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<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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<Handle<Image>>,
/// 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
// <https://google.github.io/filament/Material%20Properties.pdf>
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<StandardMaterialUniform> 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<StandardMaterialUniform> 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,
}
}
}

View file

@ -818,6 +818,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
| 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,

View file

@ -5,11 +5,10 @@
#ifdef DEPTH_PREPASS
fn prepass_depth(frag_coord: vec4<f32>, sample_index: u32) -> f32 {
#ifdef MULTISAMPLED
let depth_sample = textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
#else
let depth_sample = textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#endif
return depth_sample;
return textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
#else // MULTISAMPLED
return textureLoad(view_bindings::depth_prepass_texture, vec2<i32>(frag_coord.xy), 0);
#endif // MULTISAMPLED
}
#endif // DEPTH_PREPASS

View file

@ -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::<Opaque3d, MeshPipeline>,
batch_and_prepare_render_phase::<Transmissive3d, MeshPipeline>,
batch_and_prepare_render_phase::<Transparent3d, MeshPipeline>,
batch_and_prepare_render_phase::<AlphaMask3d, MeshPipeline>,
batch_and_prepare_render_phase::<Shadow, MeshPipeline>,
@ -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<Mesh>,
Has<NotShadowReceiver>,
Has<TransmittedShadowReceiver>,
Has<NotShadowCaster>,
Has<NoAutomaticBatching>,
)>,
@ -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 {

View file

@ -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;

View file

@ -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<RenderAssets<Image>>,
FallbackImageMsaa,
Res<FallbackImageCubemap>,
Res<FallbackImageZero>,
),
msaa: Res<Msaa>,
globals_buffer: Res<GlobalsBuffer>,
@ -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),

View file

@ -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<u32>;
#endif // DEFERRED_PREPASS
@group(0) @binding(21) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(22) var view_transmission_sampler: sampler;

View file

@ -15,3 +15,11 @@
@group(1) @binding(10) var normal_map_sampler: sampler;
@group(1) @binding(11) var depth_map_texture: texture_2d<f32>;
@group(1) @binding(12) var depth_map_sampler: sampler;
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
@group(1) @binding(13) var specular_transmission_texture: texture_2d<f32>;
@group(1) @binding(14) var specular_transmission_sampler: sampler;
@group(1) @binding(15) var thickness_texture: texture_2d<f32>;
@group(1) @binding(16) var thickness_sampler: sampler;
@group(1) @binding(17) var diffuse_transmission_texture: texture_2d<f32>;
@group(1) @binding(18) var diffuse_transmission_sampler: sampler;
#endif

View file

@ -100,8 +100,10 @@ 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
@ -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<f32> = vec3(1.0);

View file

@ -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<f32>) -> vec4<f32> {
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<f32>(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<f32> = vec3<f32>(0.0);
// Transmitted Light (Specular and Diffuse)
var transmitted_light: vec3<f32> = vec3<f32>(0.0);
let view_z = dot(vec4<f32>(
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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(0.0) // doesn't really matter
// f_ab = vec2<f32>(0.1)
// F0 = vec3<f32>(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<f32>(0.0), vec3<f32>(0.0), vec2<f32>(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<f32>(0.0)
// occlusion = vec3<f32>(1.0)
transmitted_light += ambient::ambient_light(diffuse_transmissive_lobe_world_position, -in.N, -in.V, 1.0, diffuse_transmissive_color, vec3<f32>(0.0), 1.0, vec3<f32>(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<f32>(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<f32>(1.0) // later we use `diffuse_transmissive_color` and `specular_transmissive_color`
// NdotV = 1.0;
// R = T // see definition below
// F0 = vec3<f32>(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<f32>(1.0), 1.0, f_ab, -in.N, T, vec3<f32>(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<f32>(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<f32>(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<f32>(transmitted_light, 1.0), thickness,
vec3<f32>(0.0) // TODO: Pass in (pre-attenuated) scattered light contribution here
).rgb;
}
// Total light
output_color = vec4<f32>(
direct_light + indirect_light + emissive_light,
transmitted_light + direct_light + indirect_light + emissive_light,
output_color.a
);

View file

@ -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<f32>, frag_coord: vec3<f32>, view_z: f32, N: vec3<f32>, V: vec3<f32>, F0: vec3<f32>, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3<f32>, transmitted_environment_light_specular: vec3<f32>) -> vec3<f32> {
// 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<f32>(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<f32>(0.5, -0.5) + 0.5;
// Fetch background color
var background_color: vec4<f32>;
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<f32>, frag_coord: vec3<f32>) -> vec4<f32> {
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<f32>(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<f32>, frag_coord: vec3<f32>, view_z: f32, perceptual_roughness: f32) -> vec4<f32> {
// 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<f32>(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<f32>;
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<f32>(modified_offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z {
sample = vec4<f32>(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;
}

View file

@ -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<f32>,
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32,
alpha_cutoff: f32,
@ -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<f32>(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;

View file

@ -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<f32>, 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>) -> 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<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
let shadow_map_size = vec2<f32>(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<f32>, 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);

View file

@ -48,3 +48,22 @@ fn octahedral_decode(v: vec2<f32>) -> vec3<f32> {
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<f32>, 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<f32>(-0.7071, 0.7071);
const SPIRAL_OFFSET_1_ = vec2<f32>(-0.0000, -0.8750);
const SPIRAL_OFFSET_2_ = vec2<f32>( 0.5303, 0.5303);
const SPIRAL_OFFSET_3_ = vec2<f32>(-0.6250, -0.0000);
const SPIRAL_OFFSET_4_ = vec2<f32>( 0.3536, -0.3536);
const SPIRAL_OFFSET_5_ = vec2<f32>(-0.0000, 0.3750);
const SPIRAL_OFFSET_6_ = vec2<f32>(-0.1768, -0.1768);
const SPIRAL_OFFSET_7_ = vec2<f32>( 0.1250, 0.0000);

View file

@ -26,7 +26,7 @@
fn load_noise(pixel_coordinates: vec2<i32>) -> vec2<f32> {
var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r;
#ifdef TEMPORAL_NOISE
#ifdef TEMPORAL_JITTER
index += 288u * (globals.frame_count % 64u);
#endif

View file

@ -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(),
},
);

View file

@ -97,7 +97,11 @@ pub fn batch_and_prepare_render_phase<I: CachedRenderPipelinePhaseItem, F: GetBa
*item.batch_range_mut() = index..index + 1;
*item.dynamic_offset_mut() = buffer_index.dynamic_offset;
compare_data.map(|compare_data| BatchMeta::new(item, compare_data))
if I::AUTOMATIC_BATCHING {
compare_data.map(|compare_data| BatchMeta::new(item, compare_data))
} else {
None
}
};
for mut phase in &mut views {

View file

@ -139,6 +139,9 @@ pub trait PhaseItem: Sized + Send + Sync + 'static {
/// based on the view-space `Z` value of the corresponding view matrix.
type SortKey: Ord;
/// Whether or not this `PhaseItem` should be subjected to automatic batching. (Default: `true`)
const AUTOMATIC_BATCHING: bool = true;
/// The corresponding entity that will be drawn.
///
/// This is used to fetch the render data of the entity, required by the draw function,

View file

@ -61,6 +61,7 @@ The default feature set enables most of the expected features of a game engine,
|jpeg|JPEG image format support|
|minimp3|MP3 audio format support (through minimp3)|
|mp3|MP3 audio format support|
|pbr_transmission_textures|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|
|pnm|PNM image format support, includes pam, pbm, pgm and ppm|
|serialize|Enable serialization support through serde|
|shader_format_glsl|Enable support for shaders in GLSL|

664
examples/3d/transmission.rs Normal file
View file

@ -0,0 +1,664 @@
//! This example showcases light transmission
//!
//! ## Controls
//!
//! | Key Binding | Action |
//! |:-------------------|:-----------------------------------------------------|
//! | `J`/`K`/`L`/`;` | Change Screen Space Transmission Quality |
//! | `O` / `P` | Decrease / Increase Screen Space Transmission Steps |
//! | `1` / `2` | Decrease / Increase Diffuse Transmission |
//! | `Q` / `W` | Decrease / Increase Specular Transmission |
//! | `A` / `S` | Decrease / Increase Thickness |
//! | `Z` / `X` | Decrease / Increase IOR |
//! | `E` / `R` | Decrease / Increase Perceptual Roughness |
//! | `U` / `I` | Decrease / Increase Reflectance |
//! | Arrow Keys | Control Camera |
//! | `C` | Randomize Colors |
//! | `H` | Toggle HDR + Bloom |
//! | `D` | Toggle Depth Prepass |
//! | `T` | Toggle TAA |
// This lint usually gives bad advice in the context of Bevy -- hiding complex queries behind
// type aliases tends to obfuscate code while offering no improvement in code cleanliness.
#![allow(clippy::type_complexity)]
use std::f32::consts::PI;
use bevy::{
core_pipeline::{
bloom::BloomSettings, core_3d::ScreenSpaceTransmissionQuality, prepass::DepthPrepass,
tonemapping::Tonemapping,
},
pbr::{NotShadowCaster, PointLightShadowMap, TransmittedShadowReceiver},
prelude::*,
render::camera::TemporalJitter,
render::view::ColorGrading,
};
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
use bevy::core_pipeline::experimental::taa::{
TemporalAntiAliasBundle, TemporalAntiAliasPlugin, TemporalAntiAliasSettings,
};
use rand::random;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.insert_resource(ClearColor(Color::BLACK))
.insert_resource(PointLightShadowMap { size: 2048 })
.insert_resource(AmbientLight {
brightness: 0.0,
..default()
})
.add_systems(Startup, setup)
.add_systems(Update, (example_control_system, flicker_system));
// *Note:* TAA is not _required_ for specular transmission, but
// it _greatly enhances_ the look of the resulting blur effects.
// Sadly, it's not available under WebGL.
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
app.insert_resource(Msaa::Off)
.add_plugins(TemporalAntiAliasPlugin);
app.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
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<Assets<StandardMaterial>>,
controllable: Query<(&Handle<StandardMaterial>, &ExampleControls)>,
mut camera: Query<
(
Entity,
&mut Camera,
&mut Camera3d,
&mut Transform,
Option<&DepthPrepass>,
Option<&TemporalJitter>,
),
With<Camera3d>,
>,
mut display: Query<&mut Text, With<ExampleDisplay>>,
mut state: Local<ExampleState>,
time: Res<Time>,
input: Res<Input<KeyCode>>,
) {
if input.pressed(KeyCode::Key2) {
state.diffuse_transmission = (state.diffuse_transmission + time.delta_seconds()).min(1.0);
} else if input.pressed(KeyCode::Key1) {
state.diffuse_transmission = (state.diffuse_transmission - time.delta_seconds()).max(0.0);
}
if input.pressed(KeyCode::W) {
state.specular_transmission = (state.specular_transmission + time.delta_seconds()).min(1.0);
} else if input.pressed(KeyCode::Q) {
state.specular_transmission = (state.specular_transmission - time.delta_seconds()).max(0.0);
}
if input.pressed(KeyCode::S) {
state.thickness = (state.thickness + time.delta_seconds()).min(5.0);
} else if input.pressed(KeyCode::A) {
state.thickness = (state.thickness - time.delta_seconds()).max(0.0);
}
if input.pressed(KeyCode::X) {
state.ior = (state.ior + time.delta_seconds()).min(3.0);
} else if input.pressed(KeyCode::Z) {
state.ior = (state.ior - time.delta_seconds()).max(1.0);
}
if input.pressed(KeyCode::I) {
state.reflectance = (state.reflectance + time.delta_seconds()).min(1.0);
} else if input.pressed(KeyCode::U) {
state.reflectance = (state.reflectance - time.delta_seconds()).max(0.0);
}
if input.pressed(KeyCode::R) {
state.perceptual_roughness = (state.perceptual_roughness + time.delta_seconds()).min(1.0);
} else if input.pressed(KeyCode::E) {
state.perceptual_roughness = (state.perceptual_roughness - time.delta_seconds()).max(0.0);
}
let randomize_colors = input.just_pressed(KeyCode::C);
for (material_handle, controls) in &controllable {
let material = materials.get_mut(material_handle).unwrap();
if controls.specular_transmission {
material.specular_transmission = state.specular_transmission;
material.thickness = state.thickness;
material.ior = state.ior;
material.perceptual_roughness = state.perceptual_roughness;
material.reflectance = state.reflectance;
}
if controls.diffuse_transmission {
material.diffuse_transmission = state.diffuse_transmission;
}
if controls.color && randomize_colors {
material.base_color.set_r(random());
material.base_color.set_g(random());
material.base_color.set_b(random());
}
}
let (
camera_entity,
mut camera,
mut camera_3d,
mut camera_transform,
depth_prepass,
temporal_jitter,
) = camera.single_mut();
if input.just_pressed(KeyCode::H) {
camera.hdr = !camera.hdr;
}
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
if input.just_pressed(KeyCode::D) {
if depth_prepass.is_none() {
commands.entity(camera_entity).insert(DepthPrepass);
} else {
commands.entity(camera_entity).remove::<DepthPrepass>();
}
}
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
if input.just_pressed(KeyCode::T) {
if temporal_jitter.is_none() {
commands.entity(camera_entity).insert((
TemporalJitter::default(),
TemporalAntiAliasSettings::default(),
));
} else {
commands
.entity(camera_entity)
.remove::<(TemporalJitter, TemporalAntiAliasSettings)>();
}
}
if input.just_pressed(KeyCode::O) && camera_3d.screen_space_specular_transmission_steps > 0 {
camera_3d.screen_space_specular_transmission_steps -= 1;
}
if input.just_pressed(KeyCode::P) && camera_3d.screen_space_specular_transmission_steps < 4 {
camera_3d.screen_space_specular_transmission_steps += 1;
}
if input.just_pressed(KeyCode::J) {
camera_3d.screen_space_specular_transmission_quality = ScreenSpaceTransmissionQuality::Low;
}
if input.just_pressed(KeyCode::K) {
camera_3d.screen_space_specular_transmission_quality =
ScreenSpaceTransmissionQuality::Medium;
}
if input.just_pressed(KeyCode::L) {
camera_3d.screen_space_specular_transmission_quality = ScreenSpaceTransmissionQuality::High;
}
if input.just_pressed(KeyCode::Semicolon) {
camera_3d.screen_space_specular_transmission_quality =
ScreenSpaceTransmissionQuality::Ultra;
}
let rotation = if input.pressed(KeyCode::Right) {
state.auto_camera = false;
time.delta_seconds()
} else if input.pressed(KeyCode::Left) {
state.auto_camera = false;
-time.delta_seconds()
} else if state.auto_camera {
time.delta_seconds() * 0.25
} else {
0.0
};
let distance_change =
if input.pressed(KeyCode::Down) && camera_transform.translation.length() < 25.0 {
time.delta_seconds()
} else if input.pressed(KeyCode::Up) && camera_transform.translation.length() > 2.0 {
-time.delta_seconds()
} else {
0.0
};
camera_transform.translation *= distance_change.exp();
camera_transform.rotate_around(
Vec3::ZERO,
Quat::from_euler(EulerRot::XYZ, 0.0, rotation, 0.0),
);
let mut display = display.single_mut();
display.sections[0].value = format!(
concat!(
" J / K / L / ; Screen Space Specular Transmissive Quality: {:?}\n",
" O / P Screen Space Specular Transmissive Steps: {}\n",
" 1 / 2 Diffuse Transmission: {:.2}\n",
" Q / W Specular Transmission: {:.2}\n",
" A / S Thickness: {:.2}\n",
" Z / X IOR: {:.2}\n",
" E / R Perceptual Roughness: {:.2}\n",
" U / I Reflectance: {:.2}\n",
" Arrow Keys Control Camera\n",
" C Randomize Colors\n",
" H HDR + Bloom: {}\n",
" D Depth Prepass: {}\n",
" T TAA: {}\n",
),
camera_3d.screen_space_specular_transmission_quality,
camera_3d.screen_space_specular_transmission_steps,
state.diffuse_transmission,
state.specular_transmission,
state.thickness,
state.ior,
state.perceptual_roughness,
state.reflectance,
if camera.hdr { "ON " } else { "OFF" },
if cfg!(any(not(feature = "webgl2"), not(target_arch = "wasm32"))) {
if depth_prepass.is_some() {
"ON "
} else {
"OFF"
}
} else {
"N/A (WebGL)"
},
if cfg!(any(not(feature = "webgl2"), not(target_arch = "wasm32"))) {
if temporal_jitter.is_some() {
if depth_prepass.is_some() {
"ON "
} else {
"N/A (Needs Depth Prepass)"
}
} else {
"OFF"
}
} else {
"N/A (WebGL)"
},
);
}
fn flicker_system(
mut flame: Query<&mut Transform, (With<Flicker>, With<Handle<Mesh>>)>,
mut light: Query<(&mut PointLight, &mut Transform), (With<Flicker>, Without<Handle<Mesh>>)>,
time: Res<Time>,
) {
let s = time.elapsed_seconds();
let a = (s * 6.0).cos() * 0.0125 + (s * 4.0).cos() * 0.025;
let b = (s * 5.0).cos() * 0.0125 + (s * 3.0).cos() * 0.025;
let c = (s * 7.0).cos() * 0.0125 + (s * 2.0).cos() * 0.025;
let (mut light, mut light_transform) = light.single_mut();
let mut flame_transform = flame.single_mut();
light.intensity = 1600.0 + 3000.0 * (a + b + c);
flame_transform.translation = Vec3::new(-1.0, 1.23, 0.0);
flame_transform.look_at(Vec3::new(-1.0 - c, 1.7 - b, 0.0 - a), Vec3::X);
flame_transform.rotate(Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, PI / 2.0));
light_transform.translation = Vec3::new(-1.0 - c, 1.7, 0.0 - a);
flame_transform.translation = Vec3::new(-1.0 - c, 1.23, 0.0 - a);
}

View file

@ -142,6 +142,7 @@ Example | Description
[Spotlight](../examples/3d/spotlight.rs) | Illustrates spot lights
[Texture](../examples/3d/texture.rs) | Shows configuration of texture materials
[Tonemapping](../examples/3d/tonemapping.rs) | Compares tonemapping options
[Transmission](../examples/3d/transmission.rs) | Showcases light transmission in the PBR material
[Transparency in 3D](../examples/3d/transparency_3d.rs) | Demonstrates transparency in 3d
[Two Passes](../examples/3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives
[Update glTF Scene](../examples/3d/update_gltf_scene.rs) | Update a scene from a glTF file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene