Add Order Independent Transparency (#14876)

# Objective

- Alpha blending can easily fail in many situations and requires sorting
on the cpu

## Solution

- Implement order independent transparency (OIT) as an alternative to
alpha blending
- The implementation uses 2 passes
- The first pass records all the fragments colors and position to a
buffer that is the size of N layers * the render target resolution.
- The second pass sorts the fragments, blends them and draws them to the
screen. It also currently does manual depth testing because early-z
fails in too many cases in the first pass.

## Testing

- We've been using this implementation at foresight in production for
many months now and we haven't had any issues related to OIT.

---

## Showcase


![image](https://github.com/user-attachments/assets/157f3e32-adaf-4782-b25b-c10313b9bc43)

![image](https://github.com/user-attachments/assets/bef23258-0c22-4b67-a0b8-48a9f571c44f)

## Future work

- Add an example showing how to use OIT for a custom material
- Next step would be to implement a per-pixel linked list to reduce
memory use
- I'd also like to investigate using a BinnedRenderPhase instead of a
SortedRenderPhase. If it works, it would make the transparent pass
significantly faster.

---------

Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
Co-authored-by: Charlotte McElwain <charlotte.c.mcelwain@gmail.com>
This commit is contained in:
IceSentry 2024-10-07 19:50:28 -04:00 committed by GitHub
parent e7b83acadc
commit 4bf647ff3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1090 additions and 26 deletions

View file

@ -973,6 +973,17 @@ description = "Demonstrates per-pixel motion blur"
category = "3D Rendering"
wasm = false
[[example]]
name = "order_independent_transparency"
path = "examples/3d/order_independent_transparency.rs"
doc-scrape-examples = true
[package.metadata.example.order_independent_transparency]
name = "Order Independent Transparency"
description = "Demonstrates how to use OIT"
category = "3D Rendering"
wasm = false
[[example]]
name = "tonemapping"
path = "examples/3d/tonemapping.rs"

View file

@ -34,6 +34,7 @@ bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
serde = { version = "1", features = ["derive"] }
bitflags = "2.3"

View file

@ -19,6 +19,7 @@ pub mod fullscreen_vertex_shader;
pub mod fxaa;
pub mod motion_blur;
pub mod msaa_writeback;
pub mod oit;
pub mod post_process;
pub mod prepass;
mod skybox;
@ -75,6 +76,8 @@ use crate::{
use bevy_app::{App, Plugin};
use bevy_asset::load_internal_asset;
use bevy_render::prelude::Shader;
#[cfg(not(feature = "webgl"))]
use oit::OrderIndependentTransparencyPlugin;
#[derive(Default)]
pub struct CorePipelinePlugin;
@ -107,6 +110,9 @@ impl Plugin for CorePipelinePlugin {
DepthOfFieldPlugin,
SmaaPlugin,
PostProcessingPlugin,
// DownlevelFlags::FRAGMENT_WRITABLE_STORAGE is required for OIT
#[cfg(not(feature = "webgl"))]
OrderIndependentTransparencyPlugin,
));
}
}

View file

@ -0,0 +1,283 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, Handle};
use bevy_ecs::prelude::*;
use bevy_math::UVec2;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, Shader, TextureUsages},
renderer::{RenderDevice, RenderQueue},
view::Msaa,
Render, RenderApp, RenderSet,
};
use bevy_utils::{tracing::trace, HashSet, Instant};
use bevy_window::PrimaryWindow;
use resolve::{
node::{OitResolveNode, OitResolvePass},
OitResolvePlugin,
};
use crate::core_3d::{
graph::{Core3d, Node3d},
Camera3d,
};
/// Module that defines the necesasry systems to resolve the OIT buffer and render it to the screen.
pub mod resolve;
/// Shader handle for the shader that draws the transparent meshes to the OIT layers buffer.
pub const OIT_DRAW_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4042527984320512);
/// Used to identify which camera will use OIT to render transparent meshes
/// and to configure OIT.
// TODO consider supporting multiple OIT techniques like WBOIT, Moment Based OIT,
// depth peeling, stochastic transparency, ray tracing etc.
// This should probably be done by adding an enum to this component.
#[derive(Component, Clone, Copy, ExtractComponent)]
pub struct OrderIndependentTransparencySettings {
/// Controls how many layers will be used to compute the blending.
/// The more layers you use the more memory it will use but it will also give better results.
/// 8 is generally recommended, going above 16 is probably not worth it in the vast majority of cases
pub layer_count: u8,
}
impl Default for OrderIndependentTransparencySettings {
fn default() -> Self {
Self { layer_count: 8 }
}
}
/// A plugin that adds support for Order Independent Transparency (OIT).
/// This can correctly render some scenes that would otherwise have artifacts due to alpha blending, but uses more memory.
///
/// To enable OIT for a camera you need to add the [`OrderIndependentTransparencySettings`] component to it.
///
/// If you want to use OIT for your custom material you need to call `oit_draw(position, color)` in your fragment shader.
/// You also need to make sure that your fragment shader doesn't output any colors.
///
/// # Implementation details
/// This implementation uses 2 passes.
///
/// The first pass writes the depth and color of all the fragments to a big buffer.
/// The buffer contains N layers for each pixel, where N can be set with [`OrderIndependentTransparencySettings::layer_count`].
/// This pass is essentially a forward pass.
///
/// The second pass is a single fullscreen triangle pass that sorts all the fragments then blends them together
/// and outputs the result to the screen.
pub struct OrderIndependentTransparencyPlugin;
impl Plugin for OrderIndependentTransparencyPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
OIT_DRAW_SHADER_HANDLE,
"oit_draw.wgsl",
Shader::from_wgsl
);
app.add_plugins((
ExtractComponentPlugin::<OrderIndependentTransparencySettings>::default(),
OitResolvePlugin,
))
.add_systems(Update, check_msaa)
.add_systems(Last, configure_depth_texture_usages);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
prepare_oit_buffers.in_set(RenderSet::PrepareResources),
);
render_app
.add_render_graph_node::<ViewNodeRunner<OitResolveNode>>(Core3d, OitResolvePass)
.add_render_graph_edges(
Core3d,
(
Node3d::MainTransparentPass,
OitResolvePass,
Node3d::EndMainPass,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<OitBuffers>();
}
}
// WARN This should only happen for cameras with the [`OrderIndependentTransparencySettings`] component
// but when multiple cameras are present on the same window
// bevy reuses the same depth texture so we need to set this on all cameras with the same render target.
fn configure_depth_texture_usages(
p: Query<Entity, With<PrimaryWindow>>,
cameras: Query<(&Camera, Has<OrderIndependentTransparencySettings>)>,
mut new_cameras: Query<(&mut Camera3d, &Camera), Added<Camera3d>>,
) {
if new_cameras.is_empty() {
return;
}
// Find all the render target that potentially uses OIT
let primary_window = p.get_single().ok();
let mut render_target_has_oit = HashSet::new();
for (camera, has_oit) in &cameras {
if has_oit {
render_target_has_oit.insert(camera.target.normalize(primary_window));
}
}
// Update the depth texture usage for cameras with a render target that has OIT
for (mut camera_3d, camera) in &mut new_cameras {
if render_target_has_oit.contains(&camera.target.normalize(primary_window)) {
let mut usages = TextureUsages::from(camera_3d.depth_texture_usages);
usages |= TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
camera_3d.depth_texture_usages = usages.into();
}
}
}
fn check_msaa(cameras: Query<&Msaa, With<OrderIndependentTransparencySettings>>) {
for msaa in &cameras {
if msaa.samples() > 1 {
panic!("MSAA is not supported when using OrderIndependentTransparency");
}
}
}
/// Holds the buffers that contain the data of all OIT layers.
/// We use one big buffer for the entire app. Each camaera will reuse it so it will
/// always be the size of the biggest OIT enabled camera.
#[derive(Resource)]
pub struct OitBuffers {
/// The OIT layers containing depth and color for each fragments.
/// This is essentially used as a 3d array where xy is the screen coordinate and z is
/// the list of fragments rendered with OIT.
pub layers: BufferVec<UVec2>,
/// Buffer containing the index of the last layer that was written for each fragment.
pub layer_ids: BufferVec<i32>,
pub layers_count_uniforms: DynamicUniformBuffer<i32>,
}
impl FromWorld for OitBuffers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
// initialize buffers with something so there's a valid binding
let mut layers = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layers.set_label(Some("oit_layers"));
layers.reserve(1, render_device);
layers.write_buffer(render_device, render_queue);
let mut layer_ids = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layer_ids.set_label(Some("oit_layer_ids"));
layer_ids.reserve(1, render_device);
layer_ids.write_buffer(render_device, render_queue);
let mut layers_count_uniforms = DynamicUniformBuffer::default();
layers_count_uniforms.set_label(Some("oit_layers_count"));
Self {
layers,
layer_ids,
layers_count_uniforms,
}
}
}
#[derive(Component)]
pub struct OitLayersCountOffset {
pub offset: u32,
}
/// This creates or resizes the oit buffers for each camera.
/// It will always create one big buffer that's as big as the biggest buffer needed.
/// Cameras with smaller viewports or less layers will simply use the big buffer and ignore the rest.
#[allow(clippy::type_complexity)]
pub fn prepare_oit_buffers(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
cameras: Query<
(&ExtractedCamera, &OrderIndependentTransparencySettings),
(
Changed<ExtractedCamera>,
Changed<OrderIndependentTransparencySettings>,
),
>,
camera_oit_uniforms: Query<(Entity, &OrderIndependentTransparencySettings)>,
mut buffers: ResMut<OitBuffers>,
) {
// Get the max buffer size for any OIT enabled camera
let mut max_layer_ids_size = usize::MIN;
let mut max_layers_size = usize::MIN;
for (camera, settings) in &cameras {
let Some(size) = camera.physical_target_size else {
continue;
};
let layer_count = settings.layer_count as usize;
let size = (size.x * size.y) as usize;
max_layer_ids_size = max_layer_ids_size.max(size);
max_layers_size = max_layers_size.max(size * layer_count);
}
// Create or update the layers buffer based on the max size
if buffers.layers.capacity() < max_layers_size {
let start = Instant::now();
buffers.layers.reserve(max_layers_size, &render_device);
let remaining = max_layers_size - buffers.layers.capacity();
for _ in 0..remaining {
buffers.layers.push(UVec2::ZERO);
}
buffers.layers.write_buffer(&render_device, &render_queue);
trace!(
"OIT layers buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layers.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
// Create or update the layer_ids buffer based on the max size
if buffers.layer_ids.capacity() < max_layer_ids_size {
let start = Instant::now();
buffers
.layer_ids
.reserve(max_layer_ids_size, &render_device);
let remaining = max_layer_ids_size - buffers.layer_ids.capacity();
for _ in 0..remaining {
buffers.layer_ids.push(0);
}
buffers
.layer_ids
.write_buffer(&render_device, &render_queue);
trace!(
"OIT layer ids buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layer_ids.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
if let Some(mut writer) = buffers.layers_count_uniforms.get_writer(
camera_oit_uniforms.iter().len(),
&render_device,
&render_queue,
) {
for (entity, settings) in &camera_oit_uniforms {
let offset = writer.write(&(settings.layer_count as i32));
commands
.entity(entity)
.insert(OitLayersCountOffset { offset });
}
}
}

View file

@ -0,0 +1,44 @@
#define_import_path bevy_core_pipeline::oit
#import bevy_pbr::mesh_view_bindings::{view, oit_layers, oit_layer_ids, oit_layers_count}
#ifdef OIT_ENABLED
// Add the fragment to the oit buffer
fn oit_draw(position: vec4f, color: vec4f) -> vec4f {
// get the index of the current fragment relative to the screen size
let screen_index = i32(floor(position.x) + floor(position.y) * view.viewport.z);
// get the size of the buffer.
// It's always the size of the screen
let buffer_size = i32(view.viewport.z * view.viewport.w);
// gets the layer index of the current fragment
var layer_id = atomicAdd(&oit_layer_ids[screen_index], 1);
// exit early if we've reached the maximum amount of fragments per layer
if layer_id >= oit_layers_count {
// force to store the oit_layers_count to make sure we don't
// accidentally increase the index above the maximum value
atomicStore(&oit_layer_ids[screen_index], oit_layers_count);
// TODO for tail blending we should return the color here
discard;
}
// get the layer_index from the screen
let layer_index = screen_index + layer_id * buffer_size;
let rgb9e5_color = bevy_pbr::rgb9e5::vec3_to_rgb9e5_(color.rgb);
let depth_alpha = pack_24bit_depth_8bit_alpha(position.z, color.a);
oit_layers[layer_index] = vec2(rgb9e5_color, depth_alpha);
discard;
}
#endif // OIT_ENABLED
fn pack_24bit_depth_8bit_alpha(depth: f32, alpha: f32) -> u32 {
let depth_bits = u32(saturate(depth) * f32(0xFFFFFFu) + 0.5);
let alpha_bits = u32(saturate(alpha) * f32(0xFFu) + 0.5);
return (depth_bits & 0xFFFFFFu) | ((alpha_bits & 0xFFu) << 24u);
}
fn unpack_24bit_depth_8bit_alpha(packed: u32) -> vec2<f32> {
let depth_bits = packed & 0xFFFFFFu;
let alpha_bits = (packed >> 24u) & 0xFFu;
return vec2(f32(depth_bits) / f32(0xFFFFFFu), f32(alpha_bits) / f32(0xFFu));
}

View file

@ -0,0 +1,212 @@
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, Handle};
use bevy_derive::Deref;
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_render::{
render_resource::{
binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent,
BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState,
MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, Shader,
ShaderStages, TextureFormat,
},
renderer::RenderDevice,
texture::BevyDefault,
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSet,
};
use crate::{
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
oit::OrderIndependentTransparencySettings,
};
use super::OitBuffers;
/// Shader handle for the shader that sorts the OIT layers, blends the colors based on depth and renders them to the screen.
pub const OIT_RESOLVE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7698420424769536);
/// Contains the render node used to run the resolve pass.
pub mod node;
/// Plugin needed to resolve the Order Independent Transparency (OIT) buffer to the screen.
pub struct OitResolvePlugin;
impl Plugin for OitResolvePlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(
app,
OIT_RESOLVE_SHADER_HANDLE,
"oit_resolve.wgsl",
Shader::from_wgsl
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
(
queue_oit_resolve_pipeline.in_set(RenderSet::Queue),
prepare_oit_resolve_bind_group.in_set(RenderSet::PrepareBindGroups),
),
);
}
fn finish(&self, app: &mut bevy_app::App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<OitResolvePipeline>();
}
}
/// Bind group for the OIT resolve pass.
#[derive(Resource, Deref)]
pub struct OitResolveBindGroup(pub BindGroup);
/// Bind group layouts used for the OIT resolve pass.
#[derive(Resource)]
pub struct OitResolvePipeline {
/// View bind group layout.
pub view_bind_group_layout: BindGroupLayout,
/// Depth bind group layout.
pub oit_depth_bind_group_layout: BindGroupLayout,
}
impl FromWorld for OitResolvePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_bind_group_layout = render_device.create_bind_group_layout(
"oit_resolve_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
// layers
storage_buffer_sized(false, None),
// layer ids
storage_buffer_sized(false, None),
),
),
);
let oit_depth_bind_group_layout = render_device.create_bind_group_layout(
"oit_depth_bind_group_layout",
&BindGroupLayoutEntries::single(ShaderStages::FRAGMENT, texture_depth_2d()),
);
OitResolvePipeline {
view_bind_group_layout,
oit_depth_bind_group_layout,
}
}
}
#[derive(Component, Deref, Clone, Copy)]
pub struct OitResolvePipelineId(pub CachedRenderPipelineId);
/// This key is used to cache the pipeline id and to specialize the render pipeline descriptor.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct OitResolvePipelineKey {
hdr: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn queue_oit_resolve_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
resolve_pipeline: Res<OitResolvePipeline>,
views: Query<(Entity, &ExtractedView), With<OrderIndependentTransparencySettings>>,
// Store the key with the id to make the clean up logic easier.
// This also means it will always replace the entry if the key changes so nothing to clean up.
mut cached_pipeline_id: Local<EntityHashMap<(OitResolvePipelineKey, CachedRenderPipelineId)>>,
) {
let mut current_view_entities = EntityHashSet::default();
for (e, view) in &views {
current_view_entities.insert(e);
let key = OitResolvePipelineKey { hdr: view.hdr };
if let Some((cached_key, id)) = cached_pipeline_id.get(&e) {
if *cached_key == key {
commands.entity(e).insert(OitResolvePipelineId(*id));
continue;
}
}
let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline);
let pipeline_id = pipeline_cache.queue_render_pipeline(desc);
commands.entity(e).insert(OitResolvePipelineId(pipeline_id));
cached_pipeline_id.insert(e, (key, pipeline_id));
}
// Clear cache for views that don't exist anymore.
for e in cached_pipeline_id.keys().copied().collect::<Vec<_>>() {
if !current_view_entities.contains(&e) {
cached_pipeline_id.remove(&e);
}
}
}
fn specialize_oit_resolve_pipeline(
key: OitResolvePipelineKey,
resolve_pipeline: &OitResolvePipeline,
) -> RenderPipelineDescriptor {
let format = if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
RenderPipelineDescriptor {
label: Some("oit_resolve_pipeline".into()),
layout: vec![
resolve_pipeline.view_bind_group_layout.clone(),
resolve_pipeline.oit_depth_bind_group_layout.clone(),
],
fragment: Some(FragmentState {
entry_point: "fragment".into(),
shader: OIT_RESOLVE_SHADER_HANDLE,
shader_defs: vec![],
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState {
color: BlendComponent::OVER,
alpha: BlendComponent::OVER,
}),
write_mask: ColorWrites::ALL,
})],
}),
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
}
}
pub fn prepare_oit_resolve_bind_group(
mut commands: Commands,
resolve_pipeline: Res<OitResolvePipeline>,
render_device: Res<RenderDevice>,
view_uniforms: Res<ViewUniforms>,
buffers: Res<OitBuffers>,
) {
if let (Some(binding), Some(layers_binding), Some(layer_ids_binding)) = (
view_uniforms.uniforms.binding(),
buffers.layers.binding(),
buffers.layer_ids.binding(),
) {
let bind_group = render_device.create_bind_group(
"oit_resolve_bind_group",
&resolve_pipeline.view_bind_group_layout,
&BindGroupEntries::sequential((binding.clone(), layers_binding, layer_ids_binding)),
);
commands.insert_resource(OitResolveBindGroup(bind_group));
}
}

View file

@ -0,0 +1,78 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
view::{ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
use super::{OitResolveBindGroup, OitResolvePipeline, OitResolvePipelineId};
/// Render label for the OIT resolve pass.
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
pub struct OitResolvePass;
/// The node that executes the OIT resolve pass.
#[derive(Default)]
pub struct OitResolveNode;
impl ViewNode for OitResolveNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static ViewUniformOffset,
&'static OitResolvePipelineId,
&'static ViewDepthTexture,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let Some(resolve_pipeline) = world.get_resource::<OitResolvePipeline>() else {
return Ok(());
};
// resolve oit
// sorts the layers and renders the final blended color to the screen
{
let pipeline_cache = world.resource::<PipelineCache>();
let bind_group = world.resource::<OitResolveBindGroup>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(oit_resolve_pipeline_id.0)
else {
return Ok(());
};
let depth_bind_group = render_context.render_device().create_bind_group(
"oit_resolve_depth_bind_group",
&resolve_pipeline.oit_depth_bind_group_layout,
&BindGroupEntries::single(depth.view()),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("oit_resolve_pass"),
color_attachments: &[Some(view_target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[view_uniform.offset]);
render_pass.set_bind_group(1, &depth_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
Ok(())
}
}

View file

@ -0,0 +1,107 @@
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<storage, read_write> layers: array<vec2<u32>>;
@group(0) @binding(2) var<storage, read_write> layer_ids: array<atomic<i32>>;
@group(1) @binding(0) var depth: texture_depth_2d;
struct OitFragment {
color: vec3<f32>,
alpha: f32,
depth: f32,
}
// Contains all the colors and depth for this specific fragment
// TODO don't hardcode size
var<private> fragment_list: array<OitFragment, 32>;
struct FullscreenVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let buffer_size = i32(view.viewport.z * view.viewport.w);
let screen_index = i32(floor(in.position.x) + floor(in.position.y) * view.viewport.z);
let counter = atomicLoad(&layer_ids[screen_index]);
if counter == 0 {
reset_indices(screen_index);
discard;
} else {
let result = sort(screen_index, buffer_size);
reset_indices(screen_index);
// Manually do depth testing.
// This is necessary because early z doesn't seem to trigger in the transparent pass.
// Once we have a per pixel linked list it should be done much earlier
let d = textureLoad(depth, vec2<i32>(in.position.xy), 0);
if d > result.depth {
discard;
}
return result.color;
}
}
// Resets all indices to 0.
// This means we don't have to clear the entire layers buffer
fn reset_indices(screen_index: i32) {
atomicStore(&layer_ids[screen_index], 0);
layers[screen_index] = vec2(0u);
}
struct SortResult {
color: vec4f,
depth: f32,
}
fn sort(screen_index: i32, buffer_size: i32) -> SortResult {
var counter = atomicLoad(&layer_ids[screen_index]);
// fill list
for (var i = 0; i < counter; i += 1) {
let fragment = layers[screen_index + buffer_size * i];
// unpack color/alpha/depth
let color = bevy_pbr::rgb9e5::rgb9e5_to_vec3_(fragment.x);
let depth_alpha = bevy_core_pipeline::oit::unpack_24bit_depth_8bit_alpha(fragment.y);
fragment_list[i].color = color;
fragment_list[i].alpha = depth_alpha.y;
fragment_list[i].depth = depth_alpha.x;
}
// bubble sort the list based on the depth
for (var i = counter; i >= 0; i -= 1) {
for (var j = 0; j < i; j += 1) {
if fragment_list[j].depth < fragment_list[j + 1].depth {
// swap
let temp = fragment_list[j + 1];
fragment_list[j + 1] = fragment_list[j];
fragment_list[j] = temp;
}
}
}
// resolve blend
var final_color = vec4(0.0);
for (var i = 0; i <= counter; i += 1) {
let color = fragment_list[i].color;
let alpha = fragment_list[i].alpha;
var base_color = vec4(color.rgb * alpha, alpha);
final_color = blend(final_color, base_color);
}
var result: SortResult;
result.color = final_color;
result.depth = fragment_list[0].depth;
return result;
}
// OVER operator using premultiplied alpha
// see: https://en.wikipedia.org/wiki/Alpha_compositing
fn blend(color_a: vec4<f32>, color_b: vec4<f32>) -> vec4<f32> {
let final_color = color_a.rgb + (1.0 - color_a.a) * color_b.rgb;
let alpha = color_a.a + (1.0 - color_a.a) * color_b.a;
return vec4(final_color.rgb, alpha);
}

View file

@ -10,6 +10,7 @@ use bevy_core_pipeline::{
AlphaMask3d, Camera3d, Opaque3d, Opaque3dBinKey, ScreenSpaceTransmissionQuality,
Transmissive3d, Transparent3d,
},
oit::OrderIndependentTransparencySettings,
prepass::{
DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, OpaqueNoLightmap3dBinKey,
},
@ -620,6 +621,7 @@ pub fn queue_material_meshes<M: Material>(
Has<RenderViewLightProbes<EnvironmentMapLight>>,
Has<RenderViewLightProbes<IrradianceVolume>>,
),
Has<OrderIndependentTransparencySettings>,
)>,
) where
M::Data: PartialEq + Eq + Hash + Clone,
@ -638,6 +640,7 @@ pub fn queue_material_meshes<M: Material>(
temporal_jitter,
projection,
(has_environment_maps, has_irradiance_volumes),
has_oit,
) in &views
{
let (
@ -691,6 +694,10 @@ pub fn queue_material_meshes<M: Material>(
view_key |= MeshPipelineKey::IRRADIANCE_VOLUME;
}
if has_oit {
view_key |= MeshPipelineKey::OIT_ENABLED;
}
if let Some(projection) = projection {
view_key |= match projection {
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,

View file

@ -5,6 +5,7 @@ use bevy_asset::{load_internal_asset, AssetId};
use bevy_core_pipeline::{
core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
deferred::{AlphaMask3dDeferred, Opaque3dDeferred},
oit::{prepare_oit_buffers, OitLayersCountOffset},
prepass::MotionVectorPrepass,
};
use bevy_derive::{Deref, DerefMut};
@ -47,6 +48,7 @@ use bevy_utils::{
use bytemuck::{Pod, Zeroable};
use nonmax::{NonMaxU16, NonMaxU32};
use smallvec::{smallvec, SmallVec};
use static_assertions::const_assert_eq;
use crate::{
@ -167,7 +169,9 @@ impl Plugin for MeshRenderPlugin {
prepare_skins.in_set(RenderSet::PrepareResources),
prepare_morphs.in_set(RenderSet::PrepareResources),
prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups),
prepare_mesh_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
prepare_mesh_view_bind_groups
.in_set(RenderSet::PrepareBindGroups)
.after(prepare_oit_buffers),
no_gpu_preprocessing::clear_batched_cpu_instance_buffers::<MeshPipeline>
.in_set(RenderSet::Cleanup)
.after(RenderSet::Render),
@ -1490,6 +1494,7 @@ bitflags::bitflags! {
const SCREEN_SPACE_REFLECTIONS = 1 << 16;
const HAS_PREVIOUS_SKIN = 1 << 17;
const HAS_PREVIOUS_MORPH = 1 << 18;
const OIT_ENABLED = 1 << 18;
const LAST_FLAG = Self::HAS_PREVIOUS_MORPH.bits();
// Bitfields
@ -1751,7 +1756,15 @@ impl SpecializedMeshPipeline for MeshPipeline {
let (label, blend, depth_write_enabled);
let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS);
let (mut is_opaque, mut alpha_to_coverage_enabled) = (false, false);
if pass == MeshPipelineKey::BLEND_ALPHA {
if key.contains(MeshPipelineKey::OIT_ENABLED) && pass == MeshPipelineKey::BLEND_ALPHA {
label = "oit_mesh_pipeline".into();
// TODO tail blending would need alpha blending
blend = None;
shader_defs.push("OIT_ENABLED".into());
// TODO it should be possible to use this to combine MSAA and OIT
// alpha_to_coverage_enabled = true;
depth_write_enabled = false;
} else if pass == MeshPipelineKey::BLEND_ALPHA {
label = "alpha_blend_mesh_pipeline".into();
blend = Some(BlendState::ALPHA_BLENDING);
// For the transparent pass, fragments that are closer will be alpha blended
@ -2179,6 +2192,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
Read<ViewScreenSpaceReflectionsUniformOffset>,
Read<ViewEnvironmentMapUniformOffset>,
Read<MeshViewBindGroup>,
Option<Read<OitLayersCountOffset>>,
);
type ItemQuery = ();
@ -2193,23 +2207,24 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
view_ssr,
view_environment_map,
mesh_view_bind_group,
maybe_oit_layers_count_offset,
): ROQueryItem<'w, Self::ViewQuery>,
_entity: Option<()>,
_: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(
I,
&mesh_view_bind_group.value,
&[
let mut offsets: SmallVec<[u32; 8]> = smallvec![
view_uniform.offset,
view_lights.offset,
view_fog.offset,
**view_light_probes,
**view_ssr,
**view_environment_map,
],
);
];
if let Some(layers_count_offset) = maybe_oit_layers_count_offset {
offsets.push(layers_count_offset.offset);
}
pass.set_bind_group(I, &mesh_view_bind_group.value, &offsets);
RenderCommandResult::Success
}

View file

@ -3,6 +3,7 @@ use core::{array, num::NonZero};
use bevy_core_pipeline::{
core_3d::ViewTransmissionTexture,
oit::{OitBuffers, OrderIndependentTransparencySettings},
prepass::ViewPrepassTextures,
tonemapping::{
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
@ -12,6 +13,7 @@ use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::Has,
system::{Commands, Query, Res, Resource},
world::{FromWorld, World},
};
@ -71,6 +73,7 @@ bitflags::bitflags! {
const NORMAL_PREPASS = 1 << 2;
const MOTION_VECTOR_PREPASS = 1 << 3;
const DEFERRED_PREPASS = 1 << 4;
const OIT_ENABLED = 1 << 5;
}
}
@ -83,7 +86,7 @@ impl MeshPipelineViewLayoutKey {
use MeshPipelineViewLayoutKey as Key;
format!(
"mesh_view_layout{}{}{}{}{}",
"mesh_view_layout{}{}{}{}{}{}",
self.contains(Key::MULTISAMPLED)
.then_some("_multisampled")
.unwrap_or_default(),
@ -99,6 +102,9 @@ impl MeshPipelineViewLayoutKey {
self.contains(Key::DEFERRED_PREPASS)
.then_some("_deferred")
.unwrap_or_default(),
self.contains(Key::OIT_ENABLED)
.then_some("_oit")
.unwrap_or_default(),
)
}
}
@ -122,6 +128,9 @@ impl From<MeshPipelineKey> for MeshPipelineViewLayoutKey {
if value.contains(MeshPipelineKey::DEFERRED_PREPASS) {
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
}
if value.contains(MeshPipelineKey::OIT_ENABLED) {
result |= MeshPipelineViewLayoutKey::OIT_ENABLED;
}
result
}
@ -348,6 +357,18 @@ fn layout_entries(
(30, sampler(SamplerBindingType::Filtering)),
));
// OIT
if cfg!(not(feature = "webgl")) && layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) {
entries = entries.extend_with_indices((
// oit_layers
(31, storage_buffer_sized(false, None)),
// oit_layer_ids,
(32, storage_buffer_sized(false, None)),
// oit_layer_count
(33, uniform_buffer::<i32>(true)),
));
}
entries.to_vec()
}
@ -453,8 +474,7 @@ pub fn prepare_mesh_view_bind_groups(
render_device: Res<RenderDevice>,
mesh_pipeline: Res<MeshPipeline>,
shadow_samplers: Res<ShadowSamplers>,
light_meta: Res<LightMeta>,
global_light_meta: Res<GlobalClusterableObjectMeta>,
(light_meta, global_light_meta): (Res<LightMeta>, Res<GlobalClusterableObjectMeta>),
fog_meta: Res<FogMeta>,
(view_uniforms, environment_map_uniform): (Res<ViewUniforms>, Res<EnvironmentMapUniformBuffer>),
views: Query<(
@ -468,6 +488,7 @@ pub fn prepare_mesh_view_bind_groups(
&Tonemapping,
Option<&RenderViewLightProbes<EnvironmentMapLight>>,
Option<&RenderViewLightProbes<IrradianceVolume>>,
Has<OrderIndependentTransparencySettings>,
)>,
(images, mut fallback_images, fallback_image, fallback_image_zero): (
Res<RenderAssets<GpuImage>>,
@ -480,6 +501,7 @@ pub fn prepare_mesh_view_bind_groups(
light_probes_buffer: Res<LightProbesBuffer>,
visibility_ranges: Res<RenderVisibilityRanges>,
ssr_buffer: Res<ScreenSpaceReflectionsBuffer>,
oit_buffers: Res<OitBuffers>,
) {
if let (
Some(view_binding),
@ -513,6 +535,7 @@ pub fn prepare_mesh_view_bind_groups(
tonemapping,
render_view_environment_maps,
render_view_irradiance_volumes,
has_oit,
) in &views
{
let fallback_ssao = fallback_images
@ -523,10 +546,13 @@ pub fn prepare_mesh_view_bind_groups(
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
.unwrap_or(&fallback_ssao);
let layout = &mesh_pipeline.get_view_layout(
MeshPipelineViewLayoutKey::from(*msaa)
| MeshPipelineViewLayoutKey::from(prepass_textures),
);
let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa)
| MeshPipelineViewLayoutKey::from(prepass_textures);
if has_oit {
layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED;
}
let layout = &mesh_pipeline.get_view_layout(layout_key);
let mut entries = DynamicBindGroupEntries::new_with_indices((
(0, view_binding.clone()),
@ -645,6 +671,24 @@ pub fn prepare_mesh_view_bind_groups(
entries =
entries.extend_with_indices(((29, transmission_view), (30, transmission_sampler)));
if has_oit {
if let (
Some(oit_layers_binding),
Some(oit_layer_ids_binding),
Some(oit_layers_count_uniforms_binding),
) = (
oit_buffers.layers.binding(),
oit_buffers.layer_ids.binding(),
oit_buffers.layers_count_uniforms.binding(),
) {
entries = entries.extend_with_indices((
(31, oit_layers_binding.clone()),
(32, oit_layer_ids_binding.clone()),
(33, oit_layers_count_uniforms_binding.clone()),
));
}
}
commands.entity(entity).insert(MeshViewBindGroup {
value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries),
});

View file

@ -101,3 +101,9 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
@group(0) @binding(29) var view_transmission_texture: texture_2d<f32>;
@group(0) @binding(30) var view_transmission_sampler: sampler;
#ifdef OIT_ENABLED
@group(0) @binding(31) var<storage, read_write> oit_layers: array<vec2<u32>>;
@group(0) @binding(32) var<storage, read_write> oit_layer_ids: array<atomic<i32>>;
@group(0) @binding(33) var<uniform> oit_layers_count: i32;
#endif OIT_ENABLED

View file

@ -1,4 +1,5 @@
#import bevy_pbr::{
pbr_types,
pbr_functions::alpha_discard,
pbr_fragment::pbr_input_from_standard_material,
}
@ -21,6 +22,10 @@
#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output
#endif
#ifdef OIT_ENABLED
#import bevy_core_pipeline::oit::oit_draw
#endif // OIT_ENABLED
@fragment
fn fragment(
#ifdef MESHLET_MESH_MATERIAL_PASS
@ -65,5 +70,13 @@ fn fragment(
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
#endif
#ifdef OIT_ENABLED
let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
// This will always return 0.0. The fragments will only be drawn during the oit resolve pass.
out.color = oit_draw(in.position, out.color);
}
#endif // OIT_ENABLED
return out;
}

View file

@ -0,0 +1,236 @@
//! A simple 3D scene showing how alpha blending can break and how order independent transparency (OIT) can fix it.
//!
//! See [`OrderIndependentTransparencyPlugin`] for the trade-offs of using OIT.
//!
//! [`OrderIndependentTransparencyPlugin`]: bevy::render::pipeline::OrderIndependentTransparencyPlugin
use bevy::{
color::palettes::css::{BLUE, GREEN, RED},
core_pipeline::oit::OrderIndependentTransparencySettings,
prelude::*,
render::view::RenderLayers,
};
fn main() {
std::env::set_var("RUST_BACKTRACE", "1");
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (toggle_oit, cycle_scenes))
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// camera
commands
.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
// Add this component to this camera to render transparent meshes using OIT
OrderIndependentTransparencySettings::default(),
RenderLayers::layer(1),
))
.insert(
// Msaa currently doesn't work with OIT
Msaa::Off,
);
// light
commands.spawn((
PointLight {
shadows_enabled: false,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
RenderLayers::layer(1),
));
// spawn help text
commands.spawn((
TextBundle::from_sections([
TextSection::new("Press T to toggle OIT\n", TextStyle::default()),
TextSection::new("OIT Enabled", TextStyle::default()),
TextSection::new("\nPress C to cycle test scenes", TextStyle::default()),
]),
RenderLayers::layer(1),
));
// spawn default scene
spawn_spheres(&mut commands, &mut meshes, &mut materials);
}
fn toggle_oit(
mut commands: Commands,
mut text: Query<&mut Text>,
keyboard_input: Res<ButtonInput<KeyCode>>,
q: Query<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
) {
if keyboard_input.just_pressed(KeyCode::KeyT) {
let (e, has_oit) = q.single();
text.single_mut().sections[1].value = if has_oit {
// Removing the component will completely disable OIT for this camera
commands
.entity(e)
.remove::<OrderIndependentTransparencySettings>();
"OIT disabled".to_string()
} else {
// Adding the component to the camera will render any transparent meshes
// with OIT instead of alpha blending
commands
.entity(e)
.insert(OrderIndependentTransparencySettings::default());
"OIT enabled".to_string()
};
}
}
fn cycle_scenes(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
q: Query<Entity, With<Mesh3d>>,
mut scene_id: Local<usize>,
) {
if keyboard_input.just_pressed(KeyCode::KeyC) {
// depsawn current scene
for e in &q {
commands.entity(e).despawn_recursive();
}
// increment scene_id
*scene_id = (*scene_id + 1) % 2;
// spawn next scene
match *scene_id {
0 => spawn_spheres(&mut commands, &mut meshes, &mut materials),
1 => spawn_occlusion_test(&mut commands, &mut meshes, &mut materials),
_ => unreachable!(),
}
}
}
/// Spawns 3 overlapping spheres
/// Technically, when using `alpha_to_coverage` with MSAA this particular example wouldn't break,
/// but it breaks when disabling MSAA and is enough to show the difference between OIT enabled vs disabled.
fn spawn_spheres(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
let pos_a = Vec3::new(-1.0, 0.75, 0.0);
let pos_b = Vec3::new(0.0, -0.75, 0.0);
let pos_c = Vec3::new(1.0, 0.75, 0.0);
let offset = Vec3::new(0.0, 0.0, 0.0);
let sphere_handle = meshes.add(Sphere::new(2.0).mesh());
let alpha = 0.25;
let render_layers = RenderLayers::layer(1);
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.with_alpha(alpha).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_translation(pos_a + offset),
render_layers.clone(),
));
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: GREEN.with_alpha(alpha).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_translation(pos_b + offset),
render_layers.clone(),
));
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: BLUE.with_alpha(alpha).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_translation(pos_c + offset),
render_layers.clone(),
));
}
/// Spawn a combination of opaque cubes and transparent spheres.
/// This is useful to make sure transparent meshes drawn with OIT
/// are properly occluded by opaque meshes.
fn spawn_occlusion_test(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
let sphere_handle = meshes.add(Sphere::new(1.0).mesh());
let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh());
let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6));
let render_layers = RenderLayers::layer(1);
// front
let x = -2.5;
commands.spawn((
Mesh3d(cube_handle.clone()),
MeshMaterial3d(cube_material.clone()),
Transform::from_xyz(x, 0.0, 2.0),
render_layers.clone(),
));
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.with_alpha(0.5).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_xyz(x, 0., 0.),
render_layers.clone(),
));
// intersection
commands.spawn((
Mesh3d(cube_handle.clone()),
MeshMaterial3d(cube_material.clone()),
Transform::from_xyz(x, 0.0, 1.0),
render_layers.clone(),
));
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.with_alpha(0.5).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_xyz(0., 0., 0.),
render_layers.clone(),
));
// back
let x = 2.5;
commands.spawn((
Mesh3d(cube_handle.clone()),
MeshMaterial3d(cube_material.clone()),
Transform::from_xyz(x, 0.0, -2.0),
render_layers.clone(),
));
commands.spawn((
Mesh3d(sphere_handle.clone()),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED.with_alpha(0.5).into(),
alpha_mode: AlphaMode::Blend,
..default()
})),
Transform::from_xyz(x, 0., 0.),
render_layers.clone(),
));
}

View file

@ -158,6 +158,7 @@ Example | Description
[Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras
[Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental)
[Motion Blur](../examples/3d/motion_blur.rs) | Demonstrates per-pixel motion blur
[Order Independent Transparency](../examples/3d/order_independent_transparency.rs) | Demonstrates how to use OIT
[Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications)
[Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping
[Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations