Skip to content

Commit

Permalink
Fix Issue djeedai#320 Fix transparent object ordering in 3d (djeedai#323
Browse files Browse the repository at this point in the history
)

Order particle effects based on the distance of the emitter to the camera. This ensures they're sorted during the `Transparent3d` render phase relative to other transparent objects.

---------

Co-authored-by: Jerome Humbert <[email protected]>
  • Loading branch information
Katsutoshii and djeedai authored May 18, 2024
1 parent 50915fa commit 7b8f0dd
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .github/example-run/3d/ordering.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
exit_after: Some(5)
)
3 changes: 3 additions & 0 deletions .github/example-run/3d/ribbon.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
exit_after: Some(5)
)
3 changes: 3 additions & 0 deletions .github/example-run/3d/visibility.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
exit_after: Some(5)
)
3 changes: 3 additions & 0 deletions .github/example-run/3dpng/worms.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
exit_after: Some(5)
)
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "bevy/png", "3d" ]
name = "ribbon"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]

[[example]]
name = "ordering"
required-features = [ "bevy/bevy_winit", "bevy/bevy_pbr", "3d" ]

[workspace]
resolver = "2"
members = ["."]
204 changes: 204 additions & 0 deletions examples/ordering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//! Ordering
//!
//! This example demonstrates occluding particle effects behind opaque and
//! partially transparent objects. The occlusion is based on the built-in Bevy
//! ordering of transparent objects, which are sorted by their distance to the
//! camera, and therefore only works for non-overlapping objects. For Hanabi
//! effects, the origin of the emitter (the Entity with the ParticleEffect
//! component) is used as the origin of the object, and therefore the point from
//! which the distance to the camera is calculated. In this example, we
//! therefore ensure that the rectangles in front and behind the particle effect
//! do not overlap the bounding box of the effect itself.
use bevy::{
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
log::LogPlugin,
prelude::*,
};

use bevy_hanabi::prelude::*;
#[cfg(feature = "examples_world_inspector")]
use bevy_inspector_egui::quick::WorldInspectorPlugin;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::default();
app.add_plugins(
DefaultPlugins
.set(LogPlugin {
level: bevy::log::Level::WARN,
filter: "bevy_hanabi=warn,firework=trace".to_string(),
update_subscriber: None,
})
.set(WindowPlugin {
primary_window: Some(Window {
title: "🎆 Hanabi — ordering".to_string(),
..default()
}),
..default()
}),
)
.add_systems(Update, bevy::window::close_on_esc)
.add_plugins(HanabiPlugin);

#[cfg(feature = "examples_world_inspector")]
app.add_plugins(WorldInspectorPlugin::default());

app.add_systems(Startup, setup).run();

Ok(())
}

/// Create the firework particle effect which will be rendered in-between other
/// PBR objects.
fn make_firework() -> EffectAsset {
// Set the particles bright white (HDR; value=4.) so we can see the effect of
// any colored object covering them.
let mut color_gradient1 = Gradient::new();
color_gradient1.add_key(0.0, Vec4::new(4.0, 4.0, 4.0, 1.0));
color_gradient1.add_key(1.0, Vec4::new(4.0, 4.0, 4.0, 0.0));

// Keep the size large so we can more visibly see the particles for longer, and
// see the effect of alpha blending.
let mut size_gradient1 = Gradient::new();
size_gradient1.add_key(0.0, Vec2::ONE);
size_gradient1.add_key(0.1, Vec2::ONE);
size_gradient1.add_key(1.0, Vec2::ZERO);

let writer = ExprWriter::new();

// Give a bit of variation by randomizing the age per particle. This will
// control the starting color and starting size of particles.
let age = writer.lit(0.).uniform(writer.lit(0.2)).expr();
let init_age = SetAttributeModifier::new(Attribute::AGE, age);

// Give a bit of variation by randomizing the lifetime per particle
let lifetime = writer.lit(2.).uniform(writer.lit(3.)).expr();
let init_lifetime = SetAttributeModifier::new(Attribute::LIFETIME, lifetime);

// Add constant downward acceleration to simulate gravity
let accel = writer.lit(Vec3::Y * -8.).expr();
let update_accel = AccelModifier::new(accel);

// Add drag to make particles slow down a bit after the initial explosion
let drag = writer.lit(5.).expr();
let update_drag = LinearDragModifier::new(drag);

let init_pos = SetPositionSphereModifier {
center: writer.lit(Vec3::ZERO).expr(),
radius: writer.lit(2.).expr(),
dimension: ShapeDimension::Volume,
};

// Give a bit of variation by randomizing the initial speed
let init_vel = SetVelocitySphereModifier {
center: writer.lit(Vec3::ZERO).expr(),
speed: (writer.rand(ScalarType::Float) * writer.lit(20.) + writer.lit(60.)).expr(),
};

EffectAsset::new(vec![2048], Spawner::rate(128.0.into()), writer.finish())
.with_name("firework")
.init(init_pos)
.init(init_vel)
.init(init_age)
.init(init_lifetime)
.update(update_drag)
.update(update_accel)
// Note: we (ab)use the ColorOverLifetimeModifier to set a fixed color hard-coded in the
// render shader, without having to store a per-particle color. This is an optimization.
.render(ColorOverLifetimeModifier {
gradient: color_gradient1,
})
.render(SizeOverLifetimeModifier {
gradient: size_gradient1,
screen_space_size: false,
})
}

fn setup(
mut commands: Commands,
mut effects: ResMut<Assets<EffectAsset>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3dBundle {
transform: Transform::from_translation(Vec3::new(0., 0., 50.)),
camera: Camera {
hdr: true,
clear_color: Color::BLACK.into(),
..default()
},
tonemapping: Tonemapping::None,
..default()
},
BloomSettings::default(),
));

let effect1 = effects.add(make_firework());

commands.spawn((
Name::new("firework"),
ParticleEffectBundle {
effect: ParticleEffect::new(effect1),
transform: Transform {
translation: Vec3::Z,
..default()
},
..default()
},
));

// Red background at origin, with alpha blending
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(Rectangle {
half_size: Vec2 { x: 0.5, y: 0.5 },
})),
material: materials.add(StandardMaterial {
base_color: Color::rgba(1., 0., 0., 0.5),
alpha_mode: bevy::pbr::AlphaMode::Blend,
..default()
}),
transform: Transform {
scale: Vec3::splat(50.),
..default()
},
..default()
});

// Blue rectangle in front of particles, with alpha blending
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(Rectangle {
half_size: Vec2 { x: 0.5, y: 0.5 },
})),
material: materials.add(StandardMaterial {
// Keep the alpha quite high, because the particles are very bright (HDR, value=4.)
// so otherwise we can't see the attenuation of the blue box over the white particles.
base_color: Color::rgba(0., 0., 1., 0.95),
alpha_mode: bevy::pbr::AlphaMode::Blend,
..default()
}),
transform: Transform {
translation: Vec3::Y * 6. + Vec3::Z * 40.,
scale: Vec3::splat(10.),
..default()
},
..default()
});

// Green square in front of particles, without alpha blending
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(Rectangle {
half_size: Vec2 { x: 0.5, y: 0.5 },
})),
material: materials.add(StandardMaterial {
base_color: Color::GREEN,
alpha_mode: bevy::pbr::AlphaMode::Opaque,
..default()
}),
transform: Transform {
translation: Vec3::Y * -6. + Vec3::Z * 40.,
scale: Vec3::splat(10.),
..default()
},
..default()
});
}
1 change: 1 addition & 0 deletions run_examples.bat
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ cargo r --example force_field --no-default-features --features="bevy/bevy_winit
cargo r --example init --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example lifetime --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example instancing --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example ordering --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example ribbon --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
REM 3D + PNG
cargo r --example gradient --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr bevy/png 3d examples_world_inspector"
Expand Down
1 change: 1 addition & 0 deletions run_examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ cargo r --example force_field --no-default-features --features="bevy/bevy_winit
cargo r --example init --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example lifetime --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example instancing --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example ordering --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
cargo r --example ribbon --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr 3d examples_world_inspector"
# 3D + PNG
cargo r --example gradient --no-default-features --features="bevy/bevy_winit bevy/bevy_pbr bevy/png 3d examples_world_inspector"
Expand Down
4 changes: 4 additions & 0 deletions src/render/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ pub(crate) struct EffectDrawBatch {
/// rendering.
#[cfg(feature = "2d")]
pub z_sort_key_2d: FloatOrd,
/// For 3d rendering, the position of the emitter so we can compute distance
/// to camera. Ignored for 2D rendering.
#[cfg(feature = "3d")]
pub translation_3d: Vec3,
}

/// Batch data specific to a single particle group.
Expand Down
35 changes: 28 additions & 7 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ impl From<&Mat4> for GpuCompressedTransform {
}
}

impl GpuCompressedTransform {
/// Returns the translation as represented by this transform.
#[allow(dead_code)]
pub fn translation(&self) -> Vec3 {
Vec3 {
x: self.x_row.w,
y: self.y_row.w,
z: self.z_row.w,
}
}
}

/// GPU representation of spawner parameters.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
Expand Down Expand Up @@ -2353,6 +2365,9 @@ pub(crate) fn prepare_effects(
#[cfg(feature = "2d")]
let z_sort_key_2d = input.z_sort_key_2d;

#[cfg(feature = "3d")]
let translation_3d = input.transform.translation();

// Spawn one shared EffectBatches for all groups of this effect. This contains
// most of the data needed to drive rendering, except the per-group data.
// However this doesn't drive rendering; this is just storage.
Expand All @@ -2376,6 +2391,8 @@ pub(crate) fn prepare_effects(
group_index,
#[cfg(feature = "2d")]
z_sort_key_2d,
#[cfg(feature = "3d")]
translation_3d,
});
}
}
Expand Down Expand Up @@ -2508,7 +2525,7 @@ fn emit_draw<T, F>(
use_alpha_mask: bool,
) where
T: PhaseItem,
F: Fn(CachedRenderPipelineId, Entity, &EffectDrawBatch, u32) -> T,
F: Fn(CachedRenderPipelineId, Entity, &EffectDrawBatch, u32, &ExtractedView) -> T,
{
for (mut render_phase, visible_entities, view) in views.iter_mut() {
trace!("Process new view (use_alpha_mask={})", use_alpha_mask);
Expand Down Expand Up @@ -2633,12 +2650,12 @@ fn emit_draw<T, F>(
batches.spawner_base,
batches.handle
);

render_phase.add(make_phase_item(
render_pipeline_id,
draw_entity,
draw_batch,
draw_batch.group_index,
view,
));
}
}
Expand Down Expand Up @@ -2723,7 +2740,7 @@ pub(crate) fn queue_effects(
specialized_render_pipelines.reborrow(),
&pipeline_cache,
msaa.samples(),
|id, entity, draw_batch, _group| Transparent2d {
|id, entity, draw_batch, _group, _view| Transparent2d {
draw_function: draw_effects_function_2d,
pipeline: id,
entity,
Expand Down Expand Up @@ -2763,11 +2780,13 @@ pub(crate) fn queue_effects(
specialized_render_pipelines.reborrow(),
&pipeline_cache,
msaa.samples(),
|id, entity, _batch, _group| Transparent3d {
|id, entity, batch, _group, view| Transparent3d {
draw_function: draw_effects_function_3d,
pipeline: id,
entity,
distance: 0.0, // TODO
distance: view
.rangefinder3d()
.distance_translation(&batch.translation_3d),
batch_range: 0..1,
dynamic_offset: None,
},
Expand Down Expand Up @@ -2799,11 +2818,13 @@ pub(crate) fn queue_effects(
specialized_render_pipelines.reborrow(),
&pipeline_cache,
msaa.samples(),
|id, entity, _batch, _group| AlphaMask3d {
|id, entity, batch, _group, view| AlphaMask3d {
draw_function: draw_effects_function_alpha_mask,
pipeline: id,
entity,
distance: 0.0, // TODO
distance: view
.rangefinder3d()
.distance_translation(&batch.translation_3d),
batch_range: 0..1,
dynamic_offset: None,
},
Expand Down

0 comments on commit 7b8f0dd

Please sign in to comment.