Skip to content

Commit

Permalink
Switch screen space size to be a modifier (#282)
Browse files Browse the repository at this point in the history
Remove the hard-coded path for scree-space size, and the field in
`RenderContext`, and add instead a `ScreenSpaceSizeModifier`. Convert
the previously hard-coded render shader code to a regular render
modifier, now that the screen-space size calculation is decoupled from
the orientation.
  • Loading branch information
djeedai authored Feb 23, 2024
1 parent cf16097 commit f3c1d21
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 61 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Added a new `ScreenSpaceSizeModifier` which negates the effect of perspective projection, and makes the particle's size a pixel size in screen space, instead of a Bevy world unit size. This replaces the hard-coded behavior previously available on the `SetSizeModifier`.

### Removed

- Removed the `screen_space_size` field from the `SetSizeModifier`. Use the new `ScreenSpaceSizeModifier` to use a screen-space size.

## [0.10.0] 2024-02-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_hanabi"
version = "0.10.0"
version = "0.11.0-dev"
authors = ["Jerome Humbert <[email protected]>"]
edition = "2021"
description = "Hanabi GPU particle system for the Bevy game engine"
Expand Down
5 changes: 1 addition & 4 deletions examples/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ where
.render(SetColorModifier {
color: COLOR.into(),
})
.render(SetSizeModifier {
size: SIZE.into(),
screen_space_size: false,
})
.render(SetSizeModifier { size: SIZE.into() })
}

fn spawn_effect(
Expand Down
4 changes: 2 additions & 2 deletions examples/spawn_on_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ fn setup(
// Set a size of 3 (logical) pixels, constant in screen space, independent of projection
.render(SetSizeModifier {
size: Vec2::splat(3.).into(),
screen_space_size: true,
}),
})
.render(ScreenSpaceSizeModifier),
);

commands
Expand Down
3 changes: 0 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,9 +960,6 @@ impl EffectShaderSource {
if let AlphaMode::Mask(_) = &asset.alpha_mode {
layout_flags |= LayoutFlags::USE_ALPHA_MASK;
}
if render_context.screen_space_size {
layout_flags |= LayoutFlags::SCREEN_SPACE_SIZE;
}

let (flipbook_scale_code, flipbook_row_count_code) = if let Some(grid_size) =
render_context.sprite_grid_size
Expand Down
5 changes: 0 additions & 5 deletions src/modifier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,10 +505,6 @@ pub struct RenderContext<'a> {
pub gradients: HashMap<u64, Gradient<Vec4>>,
/// Size gradients.
pub size_gradients: HashMap<u64, Gradient<Vec2>>,
/// Are particles using a fixed screen-space size (in logical pixels)? If
/// `true` then the particle size is not affected by the camera projection,
/// and in particular by the distance to the camera.
pub screen_space_size: bool,
/// Counter for unique variable names.
var_counter: u32,
/// Cache of evaluated expressions.
Expand All @@ -531,7 +527,6 @@ impl<'a> RenderContext<'a> {
sprite_grid_size: None,
gradients: HashMap::new(),
size_gradients: HashMap::new(),
screen_space_size: false,
var_counter: 0,
expr_cache: Default::default(),
is_attribute_pointer: false,
Expand Down
61 changes: 53 additions & 8 deletions src/modifier/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,14 @@ impl RenderModifier for ColorOverLifetimeModifier {
///
/// # Attributes
///
/// This modifier does not require any specific particle attribute.
/// This modifier does not require any specific particle attribute. The size of
/// the particle is extracted from the [`Attribute::SIZE`] or
/// [`Attribute::SIZE2`] if any, but even if they're absent this modifier acts
/// on the default particle size.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub struct SetSizeModifier {
/// The 2D particle (quad) size.
pub size: CpuValue<Vec2>,
/// Is the particle size in screen-space logical pixel? If `true`, the size
/// is in screen-space logical pixels, and not affected by the camera
/// projection. If `false`, the particle size is in world units.
pub screen_space_size: bool,
}

impl_mod_render!(SetSizeModifier, &[]);
Expand All @@ -174,7 +173,6 @@ impl_mod_render!(SetSizeModifier, &[]);
impl RenderModifier for SetSizeModifier {
fn apply_render(&self, _module: &mut Module, context: &mut RenderContext) {
context.vertex_code += &format!("size = {0};\n", self.size.to_wgsl_string());
context.screen_space_size = self.screen_space_size;
}
}

Expand Down Expand Up @@ -221,8 +219,6 @@ impl RenderModifier for SizeOverLifetimeModifier {
Attribute::AGE.name(),
Attribute::LIFETIME.name()
);

context.screen_space_size = self.screen_space_size;
}
}

Expand Down Expand Up @@ -508,6 +504,55 @@ impl RenderModifier for FlipbookModifier {
}
}

/// A modifier to interpret the size of all particles in screen-space pixels.
///
/// This modifier assigns a pixel size to particles in screen space, ignoring
/// the distance to the camera and perspective. It effectively scales the
/// existing [`Attribute::SIZE`] of each particle to negate the perspective
/// correction usually applied to rendered objects based on their distance to
/// the camera.
///
/// Note that this modifier should generally be placed last in the stack, or at
/// least after any modifier which might modify the particle position or its
/// size. Otherwise the scaling will be incorrect.
///
/// # Attributes
///
/// This modifier requires the following particle attributes:
/// - [`Attribute::POSITION`]
///
/// If the [`Attribute::SIZE`] or [`Attribute::SIZE2`] are present, they're used
/// to initialize the particle's size. Otherwise the default size is used. So
/// this modifier doesn't require any size attribute.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub struct ScreenSpaceSizeModifier;

impl_mod_render!(
ScreenSpaceSizeModifier,
&[Attribute::POSITION, Attribute::SIZE]
);

#[typetag::serde]
impl RenderModifier for ScreenSpaceSizeModifier {
fn apply_render(&self, _module: &mut Module, context: &mut RenderContext) {
// Get perspective divide factor from clip space position. This is the "average"
// factor for the entire particle, taken at its position (mesh origin),
// and applied uniformly for all vertices. Scale size by w_cs to negate
// the perspective divide which will happen later after the vertex shader.
// The 2.0 factor is because clip space is in [-1:1] so we need to divide by the
// half screen size only.
// Note: here "size" is the built-in render size, which is always defined and
// called "size", and which may or may not be the Attribute::SIZE/2
// attribute(s).
context.vertex_code += &format!(
"let w_cs = transform_position_simulation_to_clip(particle.{0}).w;\n
let screen_size_pixels = view.viewport.zw;\n
let projection_scale = vec2<f32>(view.projection[0][0], view.projection[1][1]);\n
size = (size * w_cs * 2.0) / min(screen_size_pixels.x * projection_scale.x, screen_size_pixels.y * projection_scale.y);\n",
Attribute::POSITION.name());
}
}

#[cfg(test)]
mod tests {
use crate::*;
Expand Down
4 changes: 1 addition & 3 deletions src/render/effect_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,7 @@ impl EffectBuffer {
count: None,
},
];
if layout_flags.contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
|| layout_flags.contains(LayoutFlags::SCREEN_SPACE_SIZE)
{
if layout_flags.contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
entries.push(BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::VERTEX,
Expand Down
27 changes: 3 additions & 24 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -901,12 +901,6 @@ pub(crate) struct ParticleRenderPipelineKey {
/// This key requires the presence of UV coordinates on the particle
/// vertices.
has_image: bool,
/// Key: PARTICLE_SCREEN_SPACE_SIZE
/// The particle size is expressed in screen space. The particle has a
/// constant size on screen (in logical pixels) which is not influenced by
/// the camera projection (and so, not influenced by the distance to the
/// camera).
screen_space_size: bool,
/// Key: LOCAL_SPACE_SIMULATION
/// The effect is simulated in local space, and during rendering all
/// particles are transformed by the effect's [`GlobalTransform`].
Expand Down Expand Up @@ -935,7 +929,6 @@ impl Default for ParticleRenderPipelineKey {
shader: Handle::default(),
particle_layout: ParticleLayout::empty(),
has_image: false,
screen_space_size: false,
local_space_simulation: false,
use_alpha_mask: false,
flipbook: false,
Expand Down Expand Up @@ -1023,7 +1016,7 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline {
count: None,
},
];
if key.local_space_simulation || key.screen_space_size {
if key.local_space_simulation {
entries.push(BindGroupLayoutEntry {
binding: 3,
visibility: ShaderStages::VERTEX,
Expand Down Expand Up @@ -1064,12 +1057,6 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline {
// vertex_buffer_layout.array_stride += 8;
}

// Key: PARTICLE_SCREEN_SPACE_SIZE
if key.screen_space_size {
shader_defs.push("PARTICLE_SCREEN_SPACE_SIZE".into());
shader_defs.push("RENDER_NEEDS_SPAWNER".into());
}

// Key: LOCAL_SPACE_SIMULATION
if key.local_space_simulation {
shader_defs.push("LOCAL_SPACE_SIMULATION".into());
Expand Down Expand Up @@ -1764,8 +1751,6 @@ bitflags! {
const NONE = 0;
/// The effect uses an image texture.
const PARTICLE_TEXTURE = (1 << 0);
/// The effect's particles have a size specified in screen space.
const SCREEN_SPACE_SIZE = (1 << 1);
/// The effect is simulated in local space.
const LOCAL_SPACE_SIMULATION = (1 << 2);
/// The effect uses alpha masking instead of alpha blending. Only used for 3D.
Expand Down Expand Up @@ -2233,7 +2218,6 @@ fn emit_draw<T, F>(
}
}

let screen_space_size = batch.layout_flags.contains(LayoutFlags::SCREEN_SPACE_SIZE);
let local_space_simulation = batch
.layout_flags
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION);
Expand All @@ -2242,10 +2226,9 @@ fn emit_draw<T, F>(

// Specialize the render pipeline based on the effect batch
trace!(
"Specializing render pipeline: render_shader={:?} has_image={:?} screen_space_size={:?} use_alpha_mask={:?} flipbook={:?} hdr={}",
"Specializing render pipeline: render_shader={:?} has_image={:?} use_alpha_mask={:?} flipbook={:?} hdr={}",
batch.render_shader,
has_image,
screen_space_size,
use_alpha_mask,
flipbook,
view.hdr
Expand All @@ -2257,7 +2240,6 @@ fn emit_draw<T, F>(
shader: batch.render_shader.clone(),
particle_layout: batch.particle_layout.clone(),
has_image,
screen_space_size,
local_space_simulation,
use_alpha_mask,
flipbook,
Expand Down Expand Up @@ -2518,7 +2500,7 @@ pub(crate) fn queue_effects(
}),
},
];
if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) || buffer.layout_flags().contains(LayoutFlags::SCREEN_SPACE_SIZE) {
if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
entries.push(BindGroupEntry {
binding: 3,
resource: BindingResource::Buffer(BufferBinding {
Expand Down Expand Up @@ -2759,9 +2741,6 @@ fn draw<'w>(
let dyn_uniform_indices = if effect_batch
.layout_flags
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
|| effect_batch
.layout_flags
.contains(LayoutFlags::SCREEN_SPACE_SIZE)
{
&dyn_uniform_indices
} else {
Expand Down
11 changes: 0 additions & 11 deletions src/render/vfx_render.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,6 @@ fn vertex(

{{VERTEX_MODIFIERS}}

#ifdef PARTICLE_SCREEN_SPACE_SIZE
// Get perspective divide factor from clip space position. This is the "average" factor for the entire
// particle, taken at its position (mesh origin), and applied uniformly for all vertices.
let w_cs = transform_position_simulation_to_clip(particle.position).w;
// Scale size by w_cs to negate the perspective divide which will happen later after the vertex shader.
// The 2.0 factor is because clip space is in [-1:1] so we need to divide by the half screen size only.
let screen_size_pixels = view.viewport.zw;
let projection_scale = vec2<f32>(view.projection[0][0], view.projection[1][1]);
size = (size * w_cs * 2.0) / min(screen_size_pixels.x * projection_scale.x, screen_size_pixels.y * projection_scale.y);
#endif

// Expand particle mesh vertex based on particle position ("origin"), and local
// orientation and size of the particle mesh (currently: only quad).
let vpos = vertex_position * vec3<f32>(size.x, size.y, 1.0);
Expand Down

0 comments on commit f3c1d21

Please sign in to comment.