Skip to content

Commit 9db8cc5

Browse files
committed
Split render orientation and size
Split the calculation of the particle orientation (generally, from `OrientModifier`) and its size (_e.g._ from `SizeOverLifetimeModifier`) in such a way they become independent, and we can mix and match orientations with screen space size or simulation space size. Fix screen space size to avoid a perspective divide, which was effectively breaking the depth-independent property of the size. Fixes #269
1 parent ae4ddec commit 9db8cc5

File tree

5 files changed

+75
-54
lines changed

5 files changed

+75
-54
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- Moved most shared WGSL code into an import module `vfx_common.wgsl`. This requires using `naga_oil` for import resolution, which in turns means `naga` and `naga_oil` are now dependencies of `bevy_hanabi` itself.
1111

12+
### Fixed
13+
14+
- Fixed a bug where particle attributes used in the context of a function would emit invalid code. (#275)
15+
- Fixed a bug where the screen-space size of particles was not correctly computed, leading to small variations in size. The new code correctly sizes particles based on a screen pixel size. This may increase the actual size of particles, if a larger size had been previously used to compensate the error introduced by this bug. (#269)
16+
- Fixed a bug where screen-space size ignored the particle's local orientation. (#269)
17+
1218
## [0.9.0] 2023-12-26
1319

1420
### Added

src/lib.rs

+1-16
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,6 @@ impl EffectShaderSource {
922922
vertex_code,
923923
fragment_code,
924924
render_extra,
925-
render_sim_space_transform_code,
926925
alpha_cutoff_code,
927926
particle_texture,
928927
layout_flags,
@@ -954,15 +953,6 @@ impl EffectShaderSource {
954953
String::new()
955954
};
956955

957-
let render_sim_space_transform_code = match asset.simulation_space.eval(&render_context)
958-
{
959-
Ok(s) => s,
960-
Err(err) => {
961-
error!("Failed to compile effect's simulation space: {:?}", err);
962-
return Err(ShaderGenerateError::Expr(err));
963-
}
964-
};
965-
966956
let mut layout_flags = LayoutFlags::NONE;
967957
if asset.simulation_space == SimulationSpace::Local {
968958
layout_flags |= LayoutFlags::LOCAL_SPACE_SIMULATION;
@@ -990,7 +980,6 @@ impl EffectShaderSource {
990980
render_context.vertex_code,
991981
render_context.fragment_code,
992982
render_context.render_extra,
993-
render_sim_space_transform_code,
994983
alpha_cutoff_code,
995984
render_context.particle_texture,
996985
layout_flags,
@@ -1070,10 +1059,6 @@ impl EffectShaderSource {
10701059
.replace("{{VERTEX_MODIFIERS}}", &vertex_code)
10711060
.replace("{{FRAGMENT_MODIFIERS}}", &fragment_code)
10721061
.replace("{{RENDER_EXTRA}}", &render_extra)
1073-
.replace(
1074-
"{{SIMULATION_SPACE_TRANSFORM_PARTICLE}}",
1075-
&render_sim_space_transform_code,
1076-
)
10771062
.replace("{{ALPHA_CUTOFF}}", &alpha_cutoff_code)
10781063
.replace("{{FLIPBOOK_SCALE}}", &flipbook_scale_code)
10791064
.replace("{{FLIPBOOK_ROW_COUNT}}", &flipbook_row_count_code)
@@ -1208,7 +1193,7 @@ impl CompiledParticleEffect {
12081193
let render_shader = shader_cache.get_or_insert(&asset.name, &shader_source.render, shaders);
12091194

12101195
trace!(
1211-
"tick_spawners: init_shader={:?} update_shader={:?} render_shader={:?} has_image={} layout_flags={:?}",
1196+
"CompiledParticleEffect::update(): init_shader={:?} update_shader={:?} render_shader={:?} has_image={} layout_flags={:?}",
12121197
init_shader,
12131198
update_shader,
12141199
render_shader,

src/render/effect_cache.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ impl EffectBuffer {
307307
count: None,
308308
},
309309
];
310-
if layout_flags.contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
310+
if layout_flags.contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
311+
|| layout_flags.contains(LayoutFlags::SCREEN_SPACE_SIZE)
312+
{
311313
entries.push(BindGroupLayoutEntry {
312314
binding: 3,
313315
visibility: ShaderStages::VERTEX,
@@ -319,7 +321,11 @@ impl EffectBuffer {
319321
count: None,
320322
});
321323
}
322-
trace!("Creating render layout with {} entries", entries.len());
324+
trace!(
325+
"Creating render layout with {} entries (flags: {:?})",
326+
entries.len(),
327+
layout_flags
328+
);
323329
let particles_buffer_layout_with_dispatch =
324330
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
325331
entries: &entries,

src/render/mod.rs

+11-15
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use crate::{
4848
render::batch::{BatchInput, BatchState, Batcher, EffectBatch},
4949
spawn::EffectSpawner,
5050
CompiledParticleEffect, EffectProperties, EffectShader, HanabiPlugin, ParticleLayout,
51-
PropertyLayout, RemovedEffectsEvent, SimulationCondition, SimulationSpace,
51+
PropertyLayout, RemovedEffectsEvent, SimulationCondition,
5252
};
5353

5454
mod aligned_buffer_vec;
@@ -1041,7 +1041,7 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline {
10411041
count: None,
10421042
},
10431043
];
1044-
if key.local_space_simulation {
1044+
if key.local_space_simulation || key.screen_space_size {
10451045
entries.push(BindGroupLayoutEntry {
10461046
binding: 3,
10471047
visibility: ShaderStages::VERTEX,
@@ -1088,11 +1088,13 @@ impl SpecializedRenderPipeline for ParticlesRenderPipeline {
10881088
// Key: PARTICLE_SCREEN_SPACE_SIZE
10891089
if key.screen_space_size {
10901090
shader_defs.push("PARTICLE_SCREEN_SPACE_SIZE".into());
1091+
shader_defs.push("RENDER_NEEDS_SPAWNER".into());
10911092
}
10921093

10931094
// Key: LOCAL_SPACE_SIMULATION
10941095
if key.local_space_simulation {
10951096
shader_defs.push("LOCAL_SPACE_SIMULATION".into());
1097+
shader_defs.push("RENDER_NEEDS_SPAWNER".into());
10961098
}
10971099

10981100
// Key: USE_ALPHA_MASK
@@ -1362,22 +1364,13 @@ pub(crate) fn extract_effects(
13621364
);
13631365
let property_layout = asset.property_layout();
13641366

1365-
let mut layout_flags = LayoutFlags::NONE;
1366-
if asset.simulation_space == SimulationSpace::Local {
1367-
layout_flags |= LayoutFlags::LOCAL_SPACE_SIMULATION;
1368-
}
1369-
if let crate::AlphaMode::Mask(_) = &asset.alpha_mode {
1370-
layout_flags |= LayoutFlags::USE_ALPHA_MASK;
1371-
}
1372-
// TODO - should we init the other flags here? (they're currently not used)
1373-
1374-
trace!("Found new effect: entity {:?} | capacity {} | particle_layout {:?} | property_layout {:?}", entity, asset.capacity(), particle_layout, property_layout);
1367+
trace!("Found new effect: entity {:?} | capacity {} | particle_layout {:?} | property_layout {:?} | layout_flags {:?}", entity, asset.capacity(), particle_layout, property_layout, effect.layout_flags);
13751368
AddedEffect {
13761369
entity,
13771370
capacity: asset.capacity(),
13781371
particle_layout,
13791372
property_layout,
1380-
layout_flags,
1373+
layout_flags: effect.layout_flags,
13811374
handle,
13821375
}
13831376
})
@@ -2545,7 +2538,7 @@ pub(crate) fn queue_effects(
25452538
}),
25462539
},
25472540
];
2548-
if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) {
2541+
if buffer.layout_flags().contains(LayoutFlags::LOCAL_SPACE_SIMULATION) || buffer.layout_flags().contains(LayoutFlags::SCREEN_SPACE_SIZE) {
25492542
entries.push(BindGroupEntry {
25502543
binding: 3,
25512544
resource: BindingResource::Buffer(BufferBinding {
@@ -2555,7 +2548,7 @@ pub(crate) fn queue_effects(
25552548
}),
25562549
});
25572550
}
2558-
trace!("Creating render bind group with {} entries", entries.len());
2551+
trace!("Creating render bind group with {} entries (layour flags: {:?})", entries.len(), buffer.layout_flags());
25592552
let render = render_device.create_bind_group(
25602553
&format!("hanabi:bind_group_render_vfx{buffer_index}_particles")[..],
25612554
buffer.particle_layout_bind_group_with_dispatch(),
@@ -2786,6 +2779,9 @@ fn draw<'w>(
27862779
let dyn_uniform_indices = if effect_batch
27872780
.layout_flags
27882781
.contains(LayoutFlags::LOCAL_SPACE_SIMULATION)
2782+
|| effect_batch
2783+
.layout_flags
2784+
.contains(LayoutFlags::SCREEN_SPACE_SIZE)
27892785
{
27902786
&dyn_uniform_indices
27912787
} else {

src/render/vfx_render.wgsl

+49-21
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct VertexOutput {
2626
@group(1) @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
2727
@group(1) @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
2828
@group(1) @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
29-
#ifdef LOCAL_SPACE_SIMULATION
29+
#ifdef RENDER_NEEDS_SPAWNER
3030
@group(1) @binding(3) var<storage, read> spawner : Spawner; // NOTE - same group as update
3131
#endif
3232
#ifdef PARTICLE_TEXTURE
@@ -70,6 +70,41 @@ fn get_camera_rotation_effect_space() -> mat3x3<f32> {
7070
#endif
7171
}
7272

73+
/// Unpack a compressed transform stored in transposed row-major form.
74+
fn unpack_compressed_transform(compressed_transform: mat3x4<f32>) -> mat4x4<f32> {
75+
return transpose(
76+
mat4x4(
77+
compressed_transform[0],
78+
compressed_transform[1],
79+
compressed_transform[2],
80+
vec4<f32>(0.0, 0.0, 0.0, 1.0)
81+
)
82+
);
83+
}
84+
85+
/// Transform a simulation space position into a world space position.
86+
///
87+
/// The simulation space depends on the effect's SimulationSpace value, and is either
88+
/// the effect space (SimulationSpace::Local) or the world space (SimulationSpace::Global).
89+
fn transform_position_simulation_to_world(sim_position: vec3<f32>) -> vec4<f32> {
90+
#ifdef LOCAL_SPACE_SIMULATION
91+
let transform = unpack_compressed_transform(spawner.transform);
92+
return transform * vec4<f32>(sim_position, 1.0);
93+
#else
94+
return vec4<f32>(sim_position, 1.0);
95+
#endif
96+
}
97+
98+
/// Transform a simulation space position into a clip space position.
99+
///
100+
/// The simulation space depends on the effect's SimulationSpace value, and is either
101+
/// the effect space (SimulationSpace::Local) or the world space (SimulationSpace::Global).
102+
/// The clip space is the final [-1:1]^3 space output from the vertex shader, before
103+
/// perspective divide and viewport transform are applied.
104+
fn transform_position_simulation_to_clip(sim_position: vec3<f32>) -> vec4<f32> {
105+
return view.view_proj * transform_position_simulation_to_world(sim_position);
106+
}
107+
73108
{{RENDER_EXTRA}}
74109

75110
@vertex
@@ -100,31 +135,24 @@ fn vertex(
100135

101136
{{VERTEX_MODIFIERS}}
102137

103-
#ifdef LOCAL_SPACE_SIMULATION
104-
let transform = transpose(
105-
mat4x4(
106-
spawner.transform[0],
107-
spawner.transform[1],
108-
spawner.transform[2],
109-
vec4<f32>(0.0, 0.0, 0.0, 1.0)
110-
)
111-
);
138+
#ifdef PARTICLE_SCREEN_SPACE_SIZE
139+
// Get perspective divide factor from clip space position. This is the "average" factor for the entire
140+
// particle, taken at its position (mesh origin), and applied uniformly for all vertices.
141+
let w_cs = transform_position_simulation_to_clip(particle.position).w;
142+
// Scale size by w_cs to negate the perspective divide which will happen later after the vertex shader.
143+
// The 2.0 factor is because clip space is in [-1:1] so we need to divide by the half screen size only.
144+
let screen_size_pixels = view.viewport.zw;
145+
let projection_scale = vec2<f32>(view.projection[0][0], view.projection[1][1])
146+
size = (size * w_cs * 2.0) / min(screen_size_pixels.x * projection_scale.x, screen_size_pixels.y * projection_scale.y);
112147
#endif
113148

114-
#ifdef PARTICLE_SCREEN_SPACE_SIZE
115-
let half_screen = view.viewport.zw / 2.;
116-
let vpos = vertex_position * vec3<f32>(size.x / half_screen.x, size.y / half_screen.y, 1.0);
117-
let local_position = particle.position;
118-
let world_position = {{SIMULATION_SPACE_TRANSFORM_PARTICLE}};
119-
out.position = view.view_proj * world_position + vec4<f32>(vpos, 0.0);
120-
#else
149+
// Expand particle mesh vertex based on particle position ("origin"), and local
150+
// orientation and size of the particle mesh (currently: only quad).
121151
let vpos = vertex_position * vec3<f32>(size.x, size.y, 1.0);
122-
let local_position = particle.position
152+
let sim_position = particle.position
123153
+ axis_x * vpos.x
124154
+ axis_y * vpos.y;
125-
let world_position = {{SIMULATION_SPACE_TRANSFORM_PARTICLE}};
126-
out.position = view.view_proj * world_position;
127-
#endif
155+
out.position = transform_position_simulation_to_clip(sim_position);
128156

129157
out.color = color;
130158

0 commit comments

Comments
 (0)