#![deny(
warnings,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
missing_docs,
unsafe_code,
unstable_features,
unused_import_braces,
clippy::suboptimal_flops,
clippy::imprecise_flops,
clippy::branches_sharing_code,
clippy::suspicious_operation_groupings,
clippy::useless_let_if_seq
)]
#![allow(clippy::too_many_arguments, clippy::type_complexity)]
use std::fmt::Write as _;
use bevy::{
asset::AsAssetId,
camera::visibility::VisibilityClass,
platform::collections::{HashMap, HashSet},
prelude::*,
render::{extract_component::ExtractComponent, sync_world::SyncToRenderWorld},
};
use rand::{RngExt as _, SeedableRng as _};
use serde::{Deserialize, Serialize};
use thiserror::Error;
mod asset;
pub mod attributes;
mod gradient;
pub mod graph;
pub mod modifier;
mod plugin;
pub mod properties;
mod render;
mod spawn;
mod time;
#[cfg(test)]
mod test_utils;
pub use asset::{
AlphaMode, DefaultMesh, EffectAsset, EffectAssetDeserializer, EffectAssetLoader,
EffectAssetSerializer, EffectParent, MotionIntegration, SimulationCondition,
};
pub use attributes::*;
pub use gradient::{Gradient, GradientKey};
pub use graph::*;
pub use modifier::*;
pub use plugin::{EffectSystems, HanabiPlugin};
pub use properties::*;
pub use render::{DebugSettings, LayoutFlags, ShaderCache};
pub use spawn::{tick_spawners, CpuValue, EffectSpawner, Random, SpawnerSettings};
pub use time::{EffectSimulation, EffectSimulationTime};
#[allow(missing_docs)]
pub mod prelude {
#[doc(hidden)]
pub use crate::*;
}
#[cfg(not(any(feature = "2d", feature = "3d")))]
compile_error!(
"You need to enable at least one of the '2d' or '3d' features for anything to happen."
);
pub trait ToWgslString {
fn to_wgsl_string(&self) -> String;
}
impl ToWgslString for f32 {
fn to_wgsl_string(&self) -> String {
let s = format!("{self:.6}");
s.trim_end_matches('0').to_string()
}
}
impl ToWgslString for f64 {
fn to_wgsl_string(&self) -> String {
let s = format!("{self:.15}");
s.trim_end_matches('0').to_string()
}
}
impl ToWgslString for Vec2 {
fn to_wgsl_string(&self) -> String {
format!(
"vec2<f32>({0},{1})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string()
)
}
}
impl ToWgslString for Vec3 {
fn to_wgsl_string(&self) -> String {
format!(
"vec3<f32>({0},{1},{2})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string()
)
}
}
impl ToWgslString for Vec4 {
fn to_wgsl_string(&self) -> String {
format!(
"vec4<f32>({0},{1},{2},{3})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string(),
self.w.to_wgsl_string()
)
}
}
impl ToWgslString for bool {
fn to_wgsl_string(&self) -> String {
if *self {
"true".to_string()
} else {
"false".to_string()
}
}
}
impl ToWgslString for BVec2 {
fn to_wgsl_string(&self) -> String {
format!(
"vec2<bool>({0},{1})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string()
)
}
}
impl ToWgslString for BVec3 {
fn to_wgsl_string(&self) -> String {
format!(
"vec3<bool>({0},{1},{2})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string()
)
}
}
impl ToWgslString for BVec4 {
fn to_wgsl_string(&self) -> String {
format!(
"vec4<bool>({0},{1},{2},{3})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string(),
self.w.to_wgsl_string()
)
}
}
impl ToWgslString for i32 {
fn to_wgsl_string(&self) -> String {
format!("{}", self)
}
}
impl ToWgslString for IVec2 {
fn to_wgsl_string(&self) -> String {
format!(
"vec2<i32>({0},{1})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string()
)
}
}
impl ToWgslString for IVec3 {
fn to_wgsl_string(&self) -> String {
format!(
"vec3<i32>({0},{1},{2})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string()
)
}
}
impl ToWgslString for IVec4 {
fn to_wgsl_string(&self) -> String {
format!(
"vec4<i32>({0},{1},{2},{3})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string(),
self.w.to_wgsl_string()
)
}
}
impl ToWgslString for u32 {
fn to_wgsl_string(&self) -> String {
format!("{}u", self)
}
}
impl ToWgslString for UVec2 {
fn to_wgsl_string(&self) -> String {
format!(
"vec2<u32>({0},{1})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string()
)
}
}
impl ToWgslString for UVec3 {
fn to_wgsl_string(&self) -> String {
format!(
"vec3<u32>({0},{1},{2})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string()
)
}
}
impl ToWgslString for UVec4 {
fn to_wgsl_string(&self) -> String {
format!(
"vec4<u32>({0},{1},{2},{3})",
self.x.to_wgsl_string(),
self.y.to_wgsl_string(),
self.z.to_wgsl_string(),
self.w.to_wgsl_string()
)
}
}
impl ToWgslString for CpuValue<f32> {
fn to_wgsl_string(&self) -> String {
match self {
Self::Single(x) => x.to_wgsl_string(),
Self::Uniform((a, b)) => format!(
"(frand() * ({1} - {0}) + {0})",
a.to_wgsl_string(),
b.to_wgsl_string(),
),
}
}
}
impl ToWgslString for CpuValue<Vec2> {
fn to_wgsl_string(&self) -> String {
match self {
Self::Single(v) => v.to_wgsl_string(),
Self::Uniform((a, b)) => format!(
"(frand2() * ({1} - {0}) + {0})",
a.to_wgsl_string(),
b.to_wgsl_string(),
),
}
}
}
impl ToWgslString for CpuValue<Vec3> {
fn to_wgsl_string(&self) -> String {
match self {
Self::Single(v) => v.to_wgsl_string(),
Self::Uniform((a, b)) => format!(
"(frand3() * ({1} - {0}) + {0})",
a.to_wgsl_string(),
b.to_wgsl_string(),
),
}
}
}
impl ToWgslString for CpuValue<Vec4> {
fn to_wgsl_string(&self) -> String {
match self {
Self::Single(v) => v.to_wgsl_string(),
Self::Uniform((a, b)) => format!(
"(frand4() * ({1} - {0}) + {0})",
a.to_wgsl_string(),
b.to_wgsl_string(),
),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflect, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SimulationSpace {
#[default]
Global,
Local,
}
impl SimulationSpace {
pub fn eval(&self, context: &dyn EvalContext) -> Result<String, ExprError> {
match context.modifier_context() {
ModifierContext::Init | ModifierContext::Update => match *self {
SimulationSpace::Global => {
if !context.particle_layout().contains(Attribute::POSITION) {
return Err(ExprError::GraphEvalError(format!("Global-space simulation requires that the particles have a {} attribute.", Attribute::POSITION.name())));
}
Ok(format!(
"particle.{} += transform[3].xyz;", Attribute::POSITION.name()
))
}
SimulationSpace::Local => Ok("".to_string()),
},
ModifierContext::Render => Ok(match *self {
SimulationSpace::Global => "vec4<f32>(local_position, 1.0)",
SimulationSpace::Local => "transform * vec4<f32>(local_position, 1.0)",
}
.to_string()),
_ => Err(ExprError::GraphEvalError(
"Invalid modifier context value.".to_string(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Reflect)]
pub struct PropertyValue {
name: String,
value: Value,
}
impl From<PropertyInstance> for PropertyValue {
fn from(prop: PropertyInstance) -> Self {
Self {
name: prop.def.name().to_string(),
value: prop.value,
}
}
}
impl From<&PropertyInstance> for PropertyValue {
fn from(prop: &PropertyInstance) -> Self {
Self {
name: prop.def.name().to_string(),
value: prop.value,
}
}
}
#[derive(Default, Clone, Copy, Component, ExtractComponent)]
pub struct EffectVisibilityClass;
#[derive(Debug, Default, Clone, Component, Reflect)]
#[reflect(Component)]
#[require(
CompiledParticleEffect,
Transform,
Visibility,
VisibilityClass,
SyncToRenderWorld
)]
#[component(on_add = bevy::camera::visibility::add_visibility_class::<EffectVisibilityClass>)]
pub struct ParticleEffect {
pub handle: Handle<EffectAsset>,
pub prng_seed: Option<u32>,
}
impl ParticleEffect {
pub fn new(handle: Handle<EffectAsset>) -> Self {
Self {
handle,
prng_seed: None,
}
}
}
impl AsAssetId for ParticleEffect {
type Asset = EffectAsset;
fn as_asset_id(&self) -> AssetId<Self::Asset> {
self.handle.id()
}
}
#[derive(Debug, Default, Clone, Component)]
pub struct EffectMaterial {
pub images: Vec<Handle<Image>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub struct TextureSlot {
pub name: String,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub struct TextureLayout {
pub layout: Vec<TextureSlot>,
}
impl TextureLayout {
pub fn get_slot_by_name(&self, name: &str) -> Option<usize> {
self.layout.iter().position(|slot| slot.name == name)
}
}
#[derive(Debug, Default, Clone, PartialEq, Hash, Component)]
pub struct EffectMesh(pub Handle<Mesh>);
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EffectShaders {
pub init: Handle<Shader>,
pub update: Handle<Shader>,
pub render: Handle<Shader>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EffectShaderSources {
pub init_shader_source: String,
pub update_shader_source: String,
pub render_shader_source: String,
pub layout_flags: LayoutFlags,
}
#[derive(Debug, Error)]
pub enum ShaderGenerateError {
#[error("Expression error: {0}")]
Expr(ExprError),
#[error("Validation error: {0}")]
Validate(String),
}
impl EffectShaderSources {
pub fn generate(
asset: &EffectAsset,
parent_layout: Option<&ParticleLayout>,
num_event_bindings: u32,
) -> Result<EffectShaderSources, ShaderGenerateError> {
trace!(
"Generating shader sources for asset '{}' with {} event bindings",
asset.name,
num_event_bindings
);
let particle_layout = asset.particle_layout();
if particle_layout.size() == 0 {
return Err(ShaderGenerateError::Validate(format!(
"Asset {} has invalid empty particle layout.",
asset.name
)));
}
if let Some(parent_layout) = parent_layout {
if parent_layout.size() == 0 {
return Err(ShaderGenerateError::Validate(format!(
"Effect using asset {} has invalid empty parent particle layout.",
asset.name
)));
}
}
if !particle_layout.contains(Attribute::POSITION) {
return Err(ShaderGenerateError::Validate(format!(
"The particle layout of asset '{}' is missing the '{}' attribute. Add a modifier using that attribute, for example the SetAttributeModifier.",
asset.name, Attribute::POSITION.name().to_ascii_uppercase()
)));
}
if particle_layout.contains(Attribute::RIBBON_ID)
&& !particle_layout.contains(Attribute::AGE)
{
return Err(ShaderGenerateError::Validate(format!(
"The particle layout of asset '{}' uses ribbons (has the '{}' attribute), but is missing the '{}' attribute, which is mandatory for ribbons. Add a modifier using that attribute, for example the SetAttributeModifier.",
asset.name, Attribute::RIBBON_ID.name().to_ascii_uppercase(), Attribute::AGE.name().to_ascii_uppercase()
)));
}
let attributes_code = particle_layout.generate_code();
let parent_attributes_code = parent_layout
.map(|layout| layout.generate_code())
.unwrap_or_default();
let mut inputs_code = String::new();
let required_attributes =
HashSet::from_iter([Attribute::AXIS_X, Attribute::AXIS_Y, Attribute::AXIS_Z]);
let mut present_attributes = HashSet::new();
let mut has_size = false;
let mut has_color = false;
for attr_layout in particle_layout.attributes() {
let attr = attr_layout.attribute;
if attr == Attribute::SIZE {
if !has_size {
inputs_code += &format!(
"var size = vec3<f32>(particle.{0}, particle.{0}, particle.{0});\n",
Attribute::SIZE.name()
);
has_size = true;
present_attributes.insert(attr);
} else {
warn!("Attribute SIZE conflicts with another size attribute; ignored.");
}
} else if attr == Attribute::SIZE2 {
if !has_size {
inputs_code += &format!(
"var size = vec3<f32>(particle.{0}, 1.0);\n",
Attribute::SIZE2.name()
);
has_size = true;
present_attributes.insert(attr);
} else {
warn!("Attribute SIZE2 conflicts with another size attribute; ignored.");
}
} else if attr == Attribute::SIZE3 {
if !has_size {
inputs_code += &format!("var size = particle.{0};\n", Attribute::SIZE3.name());
has_size = true;
present_attributes.insert(attr);
} else {
warn!("Attribute SIZE3 conflicts with another size attribute; ignored.");
}
} else if attr == Attribute::HDR_COLOR {
if !has_color {
inputs_code +=
&format!("var color = particle.{};\n", Attribute::HDR_COLOR.name());
has_color = true;
present_attributes.insert(attr);
} else {
warn!("Attribute HDR_COLOR conflicts with another color attribute; ignored.");
}
} else if attr == Attribute::COLOR {
if !has_color {
inputs_code += &format!(
"var color = unpack4x8unorm(particle.{0});\n",
Attribute::COLOR.name()
);
has_color = true;
present_attributes.insert(attr);
} else {
warn!("Attribute COLOR conflicts with another color attribute; ignored.");
}
} else {
inputs_code += &format!("var {0} = particle.{0};\n", attr.name());
present_attributes.insert(attr);
}
}
if !has_size {
inputs_code += &format!(
"var size = {0};\n",
Attribute::SIZE3.default_value().to_wgsl_string() );
}
if !has_color {
inputs_code += &format!(
"var color = {};\n",
Attribute::HDR_COLOR.default_value().to_wgsl_string() );
}
for &attr in required_attributes.difference(&present_attributes) {
inputs_code += &format!(
"var {} = {};\n",
attr.name(),
attr.default_value().to_wgsl_string()
);
}
let property_layout = asset.property_layout();
let properties_code = property_layout
.generate_property_struct_code()
.unwrap_or_default();
let properties_binding_code = if property_layout.is_empty() {
"// (no properties)".to_string()
} else {
"@group(2) @binding(3) var<storage, read> properties : array<Properties>;".to_string()
};
let mut emit_event_buffer_bindings_code = String::with_capacity(256);
emit_event_buffer_bindings_code.push_str(
"@group(3) @binding(1) var<storage, read_write> child_info_buffer : ChildInfoBuffer;\n",
);
let mut emit_event_buffer_append_funcs_code = String::with_capacity(1024);
let base_binding_index = 2;
for i in 0..num_event_bindings {
let binding_index = base_binding_index + i;
emit_event_buffer_bindings_code.push_str(&format!(
"@group(3) @binding({binding_index}) var<storage, read_write> event_buffer_{i} : EventBuffer;\n"));
emit_event_buffer_append_funcs_code.push_str(&format!(
r##"/// Append one or more spawn events to the event buffer.
fn append_spawn_events_{0}(base_child_index: u32, particle_index: u32, count: u32) {{
// Optimize this case.
if (count == 0u) {{
return;
}}
let capacity = arrayLength(&event_buffer_{0}.spawn_events);
let base = min(u32(atomicAdd(&child_info_buffer.rows[base_child_index + {0}].event_count, i32(count))), capacity);
let capped_count = min(count, capacity - base);
for (var i = 0u; i < capped_count; i += 1u) {{
event_buffer_{0}.spawn_events[base + i].particle_index = particle_index;
}}
}}
"##,
i,
));
}
emit_event_buffer_bindings_code.pop();
if emit_event_buffer_bindings_code.is_empty() {
emit_event_buffer_bindings_code = "// (not emitting GPU events)".into();
}
emit_event_buffer_append_funcs_code.pop();
if emit_event_buffer_append_funcs_code.is_empty() {
emit_event_buffer_append_funcs_code = "// (not emitting GPU events)".into();
}
let mut module = asset.module().clone();
let mut layout_flags = LayoutFlags::NONE;
if asset.simulation_space == SimulationSpace::Local {
layout_flags |= LayoutFlags::LOCAL_SPACE_SIMULATION;
}
match &asset.alpha_mode {
AlphaMode::Mask(_) => layout_flags.insert(LayoutFlags::USE_ALPHA_MASK),
AlphaMode::Opaque => layout_flags.insert(LayoutFlags::OPAQUE),
_ => layout_flags.remove(LayoutFlags::USE_ALPHA_MASK | LayoutFlags::OPAQUE),
}
if particle_layout.contains(Attribute::RIBBON_ID) {
layout_flags |= LayoutFlags::RIBBONS;
}
if parent_layout.is_some() {
layout_flags |= LayoutFlags::READ_PARENT_PARTICLE;
}
let (init_code, init_extra, init_sim_space_transform_code, consume_gpu_spawn_events) = {
let mut init_context =
ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
for m in asset.init_modifiers() {
if let Err(err) = m.apply(&mut module, &mut init_context) {
error!(
"Failed to compile effect '{}', error in init context: {}",
asset.name, err
);
return Err(ShaderGenerateError::Expr(err));
}
}
let sim_space_transform_code =
asset.simulation_space.eval(&init_context).map_err(|err| {
error!("Failed to compile effect's simulation space: {}", err);
ShaderGenerateError::Expr(err)
})?;
let consume_gpu_spawn_events =
init_context.emits_gpu_spawn_events().unwrap_or(false) || parent_layout.is_some();
(
init_context.main_code,
init_context.extra_code,
sim_space_transform_code,
consume_gpu_spawn_events,
)
};
let init_shader_source = PARTICLES_INIT_SHADER_TEMPLATE
.replace("{{ATTRIBUTES}}", &attributes_code)
.replace("{{PARENT_ATTRIBUTES}}", &parent_attributes_code)
.replace("{{INIT_CODE}}", &init_code)
.replace("{{INIT_EXTRA}}", &init_extra)
.replace("{{PROPERTIES}}", &properties_code)
.replace("{{PROPERTIES_BINDING}}", &properties_binding_code)
.replace(
"{{SIMULATION_SPACE_TRANSFORM_PARTICLE}}",
&init_sim_space_transform_code,
);
trace!(
"Configured init shader for '{}':\n{}",
asset.name,
init_shader_source
);
let (mut update_code, update_extra, emit_gpu_spawn_events) = {
let mut update_context =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
for m in asset.update_modifiers() {
if let Err(err) = m.apply(&mut module, &mut update_context) {
error!(
"Failed to compile effect '{}', error in update context: {}",
asset.name, err
);
return Err(ShaderGenerateError::Expr(err));
}
}
let emit_gpu_spawn_events = update_context.emits_gpu_spawn_events().unwrap_or(false);
(
update_context.main_code,
update_context.extra_code,
emit_gpu_spawn_events,
)
};
if consume_gpu_spawn_events {
layout_flags |= LayoutFlags::CONSUME_GPU_SPAWN_EVENTS;
}
if emit_gpu_spawn_events {
layout_flags |= LayoutFlags::EMIT_GPU_SPAWN_EVENTS;
}
let has_position = present_attributes.contains(&Attribute::POSITION);
let has_velocity = present_attributes.contains(&Attribute::VELOCITY);
if asset.motion_integration != MotionIntegration::None {
if has_position && has_velocity {
let code = format!(
"\nparticle.{0} += particle.{1} * sim_params.delta_time;\n",
Attribute::POSITION.name(),
Attribute::VELOCITY.name()
);
if asset.motion_integration == MotionIntegration::PreUpdate {
update_code.insert_str(0, &code);
} else {
update_code += &code;
}
} else {
warn!(
"Asset '{}' specifies motion integration but is missing {}. Particles won't move unless the POSITION attribute is explicitly assigned. Set MotionIntegration::None to remove this warning.",
asset.name,
if has_position {
"Attribute::VELOCITY"
} else {
"Attribute::POSITION"
}
)
}
}
let (
vertex_code,
fragment_code,
render_extra,
alpha_cutoff_code,
flipbook_scale_code,
flipbook_row_count_code,
material_bindings_code,
) = {
let texture_layout = module.texture_layout();
let mut render_context =
RenderContext::new(&property_layout, &particle_layout, &texture_layout);
for m in asset.render_modifiers() {
m.apply_render(&mut module, &mut render_context)
.map_err(ShaderGenerateError::Expr)?;
}
if render_context.needs_uv {
layout_flags |= LayoutFlags::NEEDS_UV;
}
if render_context.needs_normal {
layout_flags |= LayoutFlags::NEEDS_NORMAL;
}
if render_context.needs_particle_fragment {
layout_flags |= LayoutFlags::NEEDS_PARTICLE_FRAGMENT;
}
let alpha_cutoff_code = if let AlphaMode::Mask(cutoff) = &asset.alpha_mode {
render_context.eval(&module, *cutoff).unwrap_or_else(|err| {
error!(
"Failed to evaluate the expression for AlphaMode::Mask, error: {}",
err
);
#[cfg(debug_assertions)]
return 1_f32.to_wgsl_string();
#[cfg(not(debug_assertions))]
return 0_f32.to_wgsl_string();
})
} else {
String::new()
};
let (flipbook_scale_code, flipbook_row_count_code) = if let Some(grid_size) =
render_context.sprite_grid_size
{
layout_flags |= LayoutFlags::FLIPBOOK;
let flipbook_row_count_code = (grid_size.x as i32).to_wgsl_string();
let flipbook_scale_code =
Vec2::new(1.0 / grid_size.x as f32, 1.0 / grid_size.y as f32).to_wgsl_string();
(flipbook_scale_code, flipbook_row_count_code)
} else {
(String::new(), String::new())
};
trace!(
"Generating material bindings code for layout: {:?}",
texture_layout
);
let mut material_bindings_code = String::new();
let mut bind_index = 0;
for (slot, _) in texture_layout.layout.iter().enumerate() {
let tex_index = bind_index;
let sampler_index = bind_index + 1;
material_bindings_code.push_str(&format!(
"@group(3) @binding({tex_index}) var material_texture_{slot}: texture_2d<f32>;
@group(3) @binding({sampler_index}) var material_sampler_{slot}: sampler;
"
));
bind_index += 2;
}
(
render_context.vertex_code,
render_context.fragment_code,
render_context.render_extra,
alpha_cutoff_code,
flipbook_scale_code,
flipbook_row_count_code,
material_bindings_code,
)
};
let has_age = present_attributes.contains(&Attribute::AGE);
let has_lifetime = present_attributes.contains(&Attribute::LIFETIME);
let mut age_code = String::new();
if has_age {
if has_lifetime {
age_code += &format!(
"\n let was_alive = particle.{0} < particle.{1};",
Attribute::AGE.name(),
Attribute::LIFETIME.name()
);
}
age_code += &format!(
"\n particle.{0} = particle.{0} + sim_params.delta_time;",
Attribute::AGE.name()
);
if has_lifetime {
age_code += &format!(
"\n var is_alive = particle.{0} < particle.{1};",
Attribute::AGE.name(),
Attribute::LIFETIME.name()
);
}
} else {
age_code = "\n let was_alive = true;\n var is_alive = true;".to_string();
}
let reap_code = if has_age && has_lifetime {
format!(
"is_alive = is_alive && (particle.{0} < particle.{1});",
Attribute::AGE.name(),
Attribute::LIFETIME.name()
)
} else {
"".to_string()
};
let mut writeback_code = "".to_owned();
for attribute in present_attributes
.iter()
.filter(|attribute| **attribute != Attribute::PREV && **attribute != Attribute::NEXT)
{
writeln!(
&mut writeback_code,
" particle_buffer.particles[base_particle + particle_index].{0} = particle.{0};",
attribute.name()
)
.unwrap();
}
let update_shader_source = PARTICLES_UPDATE_SHADER_TEMPLATE
.replace("{{ATTRIBUTES}}", &attributes_code)
.replace("{{PARENT_ATTRIBUTES}}", &parent_attributes_code)
.replace("{{AGE_CODE}}", &age_code)
.replace("{{REAP_CODE}}", &reap_code)
.replace("{{UPDATE_CODE}}", &update_code)
.replace("{{WRITEBACK_CODE}}", &writeback_code)
.replace("{{UPDATE_EXTRA}}", &update_extra)
.replace("{{PROPERTIES}}", &properties_code)
.replace("{{PROPERTIES_BINDING}}", &properties_binding_code)
.replace(
"{{EMIT_EVENT_BUFFER_BINDINGS}}",
&emit_event_buffer_bindings_code,
)
.replace(
"{{EMIT_EVENT_BUFFER_APPEND_FUNCS}}",
&emit_event_buffer_append_funcs_code,
);
trace!(
"Configured update shader for '{}':\n{}",
asset.name,
update_shader_source
);
let render_shader_source = PARTICLES_RENDER_SHADER_TEMPLATE
.replace("{{ATTRIBUTES}}", &attributes_code)
.replace("{{INPUTS}}", &inputs_code)
.replace("{{PROPERTIES}}", &properties_code)
.replace("{{PROPERTIES_BINDING}}", &properties_binding_code)
.replace("{{MATERIAL_BINDINGS}}", &material_bindings_code)
.replace("{{VERTEX_MODIFIERS}}", &vertex_code)
.replace("{{FRAGMENT_MODIFIERS}}", &fragment_code)
.replace("{{RENDER_EXTRA}}", &render_extra)
.replace("{{ALPHA_CUTOFF}}", &alpha_cutoff_code)
.replace("{{FLIPBOOK_SCALE}}", &flipbook_scale_code)
.replace("{{FLIPBOOK_ROW_COUNT}}", &flipbook_row_count_code);
trace!(
"Configured render shader for '{}':\n{}",
asset.name,
render_shader_source
);
Ok(EffectShaderSources {
init_shader_source,
update_shader_source,
render_shader_source,
layout_flags,
})
}
}
#[derive(Debug, Clone, Component)]
pub struct CompiledParticleEffect {
asset: Handle<EffectAsset>,
parent: Option<Entity>,
children: Vec<Entity>,
simulation_condition: SimulationCondition,
mesh: Option<Handle<Mesh>>,
effect_shader: Option<EffectShaders>,
textures: Vec<Handle<Image>>,
layout_flags: LayoutFlags,
alpha_mode: AlphaMode,
parent_particle_layout: Option<ParticleLayout>,
prng_seed: u32,
is_ready: bool,
}
impl Default for CompiledParticleEffect {
fn default() -> Self {
Self {
asset: default(),
parent: None,
children: vec![],
simulation_condition: SimulationCondition::default(),
mesh: None,
effect_shader: None,
textures: vec![],
layout_flags: LayoutFlags::NONE,
alpha_mode: default(),
parent_particle_layout: None,
prng_seed: 0,
is_ready: false,
}
}
}
impl CompiledParticleEffect {
#[inline(always)]
pub fn is_ready(&self) -> bool {
self.is_ready
}
#[cfg(test)]
pub(crate) fn with_ready_for_tests(mut self) -> Self {
self.is_ready = true;
self
}
pub(crate) fn clear(&mut self) {
self.asset = Handle::default();
self.effect_shader = None;
self.textures.clear();
}
pub(crate) fn update(
&mut self,
rebuild: bool,
instance: &ParticleEffect,
material: Option<&EffectMaterial>,
mesh: Option<&EffectMesh>,
asset: &EffectAsset,
parent_entity: Option<Entity>,
child_entities: Vec<Entity>,
parent_layout: Option<ParticleLayout>,
asset_server: &AssetServer,
shaders: &mut ResMut<Assets<Shader>>,
shader_cache: &mut ResMut<ShaderCache>,
) {
trace!(
"Updating (rebuild:{}) compiled particle effect '{}' ({:?})",
rebuild,
asset.name,
instance.handle,
);
debug_assert!(instance.handle.is_strong());
self.asset = instance.handle.clone();
self.simulation_condition = asset.simulation_condition;
self.prng_seed = instance.prng_seed.unwrap_or(asset.prng_seed);
if rebuild {
self.effect_shader = None;
}
if self.effect_shader.is_some() {
return;
}
self.parent = parent_entity;
self.children = child_entities;
let num_event_bindings = self.children.len() as u32;
let shader_source = match EffectShaderSources::generate(
asset,
parent_layout.as_ref(),
num_event_bindings,
) {
Ok(shader_source) => shader_source,
Err(err) => {
error!(
"Failed to generate shaders for effect asset '{}': {}",
asset.name, err
);
return;
}
};
self.layout_flags = shader_source.layout_flags;
self.alpha_mode = asset.alpha_mode;
self.parent_particle_layout = parent_layout;
trace!(
"Compiled effect sources: layout_flags={:?} alpha_mode={:?}",
self.layout_flags,
self.alpha_mode
);
self.effect_shader = Some(EffectShaders {
init: shader_cache.get_or_insert(
&asset.name,
"init",
&shader_source.init_shader_source,
shaders,
),
update: shader_cache.get_or_insert(
&asset.name,
"update",
&shader_source.update_shader_source,
shaders,
),
render: shader_cache.get_or_insert(
&asset.name,
"render",
&shader_source.render_shader_source,
shaders,
),
});
trace!(
"CompiledParticleEffect::update(): shaders={:?} texture_count={} layout_flags={:?}",
self.effect_shader.as_ref().unwrap(),
material.map(|mat| mat.images.len()).unwrap_or(0),
self.layout_flags,
);
if let Some(mesh) = mesh {
self.mesh = Some(mesh.0.clone());
} else {
self.mesh = asset
.mesh
.as_ref()
.map(|path: &bevy::asset::AssetPath<'_>| asset_server.load::<Mesh>(path));
}
self.textures = material.map(|mat| &mat.images).cloned().unwrap_or_default();
}
pub fn get_configured_shaders(&self) -> Option<&EffectShaders> {
self.effect_shader.as_ref()
}
}
const PARTICLES_INIT_SHADER_TEMPLATE: &str = include_str!("render/vfx_init.wgsl");
const PARTICLES_UPDATE_SHADER_TEMPLATE: &str = include_str!("render/vfx_update.wgsl");
const PARTICLES_RENDER_SHADER_TEMPLATE: &str = include_str!("render/vfx_render.wgsl");
trait ShaderCode {
fn to_shader_code(&self, input: &str) -> String;
}
impl ShaderCode for Gradient<Vec2> {
fn to_shader_code(&self, input: &str) -> String {
if self.keys().is_empty() {
return String::new();
}
let mut s: String = self
.keys()
.iter()
.enumerate()
.map(|(index, key)| {
format!(
"let t{0} = {1};\nlet v{0} = {2};",
index,
key.ratio().to_wgsl_string(),
key.value.to_wgsl_string()
)
})
.fold("// Gradient\n".into(), |s, key| s + &key + "\n");
if self.keys().len() == 1 {
s + "return v0;\n"
} else {
s += &format!("if ({input} <= t0) {{ return v0; }}\n");
let mut s = self
.keys()
.iter()
.skip(1)
.enumerate()
.map(|(index, _key)| {
format!(
"else if ({input} <= t{1}) {{ return mix(v{0}, v{1}, ({input} - t{0}) / (t{1} - t{0})); }}\n",
index,
index + 1
)
})
.fold(s, |s, key| s + &key);
let _ = writeln!(s, "else {{ return v{}; }}", self.keys().len() - 1);
s
}
}
}
impl ShaderCode for Gradient<Vec3> {
fn to_shader_code(&self, input: &str) -> String {
if self.keys().is_empty() {
return String::new();
}
let mut s: String = self
.keys()
.iter()
.enumerate()
.map(|(index, key)| {
format!(
"let t{0} = {1};\nlet v{0} = {2};",
index,
key.ratio().to_wgsl_string(),
key.value.to_wgsl_string()
)
})
.fold("// Gradient\n".into(), |s, key| s + &key + "\n");
if self.keys().len() == 1 {
s + "return v0;\n"
} else {
s += &format!("if ({input} <= t0) {{ return v0; }}\n");
let mut s = self
.keys()
.iter()
.skip(1)
.enumerate()
.map(|(index, _key)| {
format!(
"else if ({input} <= t{1}) {{ return mix(v{0}, v{1}, ({input} - t{0}) / (t{1} - t{0})); }}\n",
index,
index + 1
)
})
.fold(s, |s, key| s + &key);
let _ = writeln!(s, "else {{ return v{}; }}", self.keys().len() - 1);
s
}
}
}
impl ShaderCode for Gradient<Vec4> {
fn to_shader_code(&self, input: &str) -> String {
if self.keys().is_empty() {
return String::new();
}
let mut s: String = self
.keys()
.iter()
.enumerate()
.map(|(index, key)| {
format!(
"let t{0} = {1};\nlet c{0} = {2};",
index,
key.ratio().to_wgsl_string(),
key.value.to_wgsl_string()
)
})
.fold("// Gradient\n".into(), |s, key| s + &key + "\n");
if self.keys().len() == 1 {
s + "return c0;\n"
} else {
s += &format!("if ({input} <= t0) {{ return c0; }}\n");
let mut s = self
.keys()
.iter()
.skip(1)
.enumerate()
.map(|(index, _key)| {
format!(
"else if ({input} <= t{1}) {{ return mix(c{0}, c{1}, ({input} - t{0}) / (t{1} - t{0})); }}\n",
index,
index + 1
)
})
.fold(s, |s, key| s + &key);
let _ = writeln!(s, "else {{ return c{}; }}", self.keys().len() - 1);
s
}
}
}
fn compile_effects(
asset_server: Res<AssetServer>,
effects: Res<Assets<EffectAsset>>,
mut shaders: ResMut<Assets<Shader>>,
mut shader_cache: ResMut<ShaderCache>,
mut q_effects: Query<(
Entity,
Ref<ParticleEffect>,
Option<Ref<EffectMaterial>>,
Option<Ref<EffectMesh>>,
Option<Ref<EffectParent>>,
&mut CompiledParticleEffect,
)>,
) {
trace!("compile_effects: {} effect(s)", q_effects.iter().len());
let particle_layouts_and_parents: HashMap<Entity, (ParticleLayout, Option<Entity>)> = q_effects
.iter()
.filter_map(|(entity, effect, _, _, parent, _)| {
effects
.get(&effect.handle)
.map(|asset| (entity, (asset.particle_layout(), parent.map(|p| p.entity))))
})
.collect();
let mut children: HashMap<Entity, Vec<Entity>> =
HashMap::with_capacity_and_hasher(particle_layouts_and_parents.len(), Default::default());
for (child, (_, parent)) in particle_layouts_and_parents.iter() {
if let Some(parent) = parent.as_ref() {
children.entry(*parent).or_default().push(*child);
}
}
for (
asset,
entity,
effect,
material,
mesh,
parent_entity,
parent_layout,
mut compiled_effect,
) in q_effects.iter_mut().filter_map(
|(entity, effect, material, mesh, parent, compiled_effect)| {
let asset = effects.get(&effect.handle)?;
let (parent_entity, parent_layout) = if let Some(parent) = &parent {
let Some((parent_layout, _)) = particle_layouts_and_parents.get(&parent.entity)
else {
return None;
};
(Some(parent.entity), Some(parent_layout.clone()))
} else {
(None, None)
};
Some((
asset,
entity,
effect,
material,
mesh,
parent_entity,
parent_layout,
compiled_effect,
))
},
) {
let child_entities = children
.get_mut(&entity)
.map(std::mem::take)
.unwrap_or_default();
let material_changed = material.as_ref().is_some_and(|r| r.is_changed());
let need_rebuild =
effect.is_changed() || material_changed || compiled_effect.children != child_entities;
if need_rebuild || (compiled_effect.asset != effect.handle) {
if need_rebuild {
debug!("Invalidating the compiled cache for effect on entity {:?} due to changes in the ParticleEffect component. If you see this message too much, then performance might be affected. Find why the change detection of the ParticleEffect is triggered.", entity);
}
compiled_effect.update(
need_rebuild,
&effect,
material.map(|r| r.into_inner()),
mesh.map(|r| r.into_inner()),
asset,
parent_entity,
child_entities,
parent_layout,
&asset_server,
&mut shaders,
&mut shader_cache,
);
} else {
let mut rng = rand::rngs::StdRng::seed_from_u64(compiled_effect.prng_seed as u64);
compiled_effect.prng_seed = rng.random();
}
}
for (_, effect, _, _, parent, mut compiled_effect) in q_effects.iter_mut() {
if effects.get(&effect.handle).is_none() {
compiled_effect.clear();
}
if let Some(parent) = parent {
if particle_layouts_and_parents.get(&parent.entity).is_none() {
compiled_effect.clear();
}
}
}
}
fn update_properties_from_asset(
assets: Res<Assets<EffectAsset>>,
mut q_effects: Query<(Ref<ParticleEffect>, &mut EffectProperties), Changed<ParticleEffect>>,
) {
#[cfg(feature = "trace")]
let _span = bevy::log::info_span!("update_properties_from_asset").entered();
trace!("update_properties_from_asset()");
for (effect, properties) in q_effects.iter_mut() {
let Some(asset) = assets.get(&effect.handle) else {
continue;
};
EffectProperties::update(properties, asset.properties(), effect.is_added());
}
}
#[cfg(test)]
mod tests {
use std::ops::DerefMut;
use bevy::{
asset::{
io::{
memory::{Dir, MemoryAssetReader},
AssetSourceBuilder, AssetSourceBuilders, AssetSourceId,
},
AssetServerMode, LoadState, UnapprovedPathMode,
},
camera::visibility::{VisibilityPlugin, VisibilitySystems},
shader::ShaderLoader,
tasks::{IoTaskPool, TaskPoolBuilder},
};
use naga_oil::compose::{Composer, NagaModuleDescriptor, ShaderDefValue};
use super::*;
use crate::spawn::new_rng;
const INTS: &[usize] = &[1, 2, 4, 8, 9, 15, 16, 17, 23, 24, 31, 32, 33];
const INTS_POW2: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
const INTS16: &[usize] = &[16, 16, 16, 16, 16, 16, 16, 32, 32, 32, 32, 32, 48];
#[test]
fn next_multiple() {
for &size in INTS {
assert_eq!(size, size.next_multiple_of(1));
}
for &align in INTS_POW2 {
assert_eq!(0, 0usize.next_multiple_of(align));
}
for &size in INTS {
assert_eq!(256, size.next_multiple_of(256));
}
for (&size, &aligned_size) in INTS.iter().zip(INTS16) {
assert_eq!(aligned_size, size.next_multiple_of(16));
}
}
#[test]
fn to_wgsl_f32() {
let s = 1.0_f32.to_wgsl_string();
assert_eq!(s, "1.");
let s = (-1.0_f32).to_wgsl_string();
assert_eq!(s, "-1.");
let s = 1.5_f32.to_wgsl_string();
assert_eq!(s, "1.5");
let s = 0.5_f32.to_wgsl_string();
assert_eq!(s, "0.5");
let s = 0.123_456_78_f32.to_wgsl_string();
assert_eq!(s, "0.123457"); }
#[test]
fn to_wgsl_f64() {
let s = 1.0_f64.to_wgsl_string();
assert_eq!(s, "1.");
let s = (-1.0_f64).to_wgsl_string();
assert_eq!(s, "-1.");
let s = 1.5_f64.to_wgsl_string();
assert_eq!(s, "1.5");
let s = 0.5_f64.to_wgsl_string();
assert_eq!(s, "0.5");
let s = 0.123_456_789_012_345_67_f64.to_wgsl_string();
assert_eq!(s, "0.123456789012346"); }
#[test]
fn to_wgsl_vec() {
let s = Vec2::new(1., 2.).to_wgsl_string();
assert_eq!(s, "vec2<f32>(1.,2.)");
let s = Vec3::new(1., 2., -1.).to_wgsl_string();
assert_eq!(s, "vec3<f32>(1.,2.,-1.)");
let s = Vec4::new(1., 2., -1., 2.).to_wgsl_string();
assert_eq!(s, "vec4<f32>(1.,2.,-1.,2.)");
}
#[test]
fn to_wgsl_ivec() {
let s = IVec2::new(1, 2).to_wgsl_string();
assert_eq!(s, "vec2<i32>(1,2)");
let s = IVec3::new(1, 2, -1).to_wgsl_string();
assert_eq!(s, "vec3<i32>(1,2,-1)");
let s = IVec4::new(1, 2, -1, 2).to_wgsl_string();
assert_eq!(s, "vec4<i32>(1,2,-1,2)");
}
#[test]
fn to_wgsl_uvec() {
let s = UVec2::new(1, 2).to_wgsl_string();
assert_eq!(s, "vec2<u32>(1u,2u)");
let s = UVec3::new(1, 2, 42).to_wgsl_string();
assert_eq!(s, "vec3<u32>(1u,2u,42u)");
let s = UVec4::new(1, 2, 42, 5).to_wgsl_string();
assert_eq!(s, "vec4<u32>(1u,2u,42u,5u)");
}
#[test]
fn to_wgsl_bvec() {
let s = BVec2::new(false, true).to_wgsl_string();
assert_eq!(s, "vec2<bool>(false,true)");
let s = BVec3::new(false, true, true).to_wgsl_string();
assert_eq!(s, "vec3<bool>(false,true,true)");
let s = BVec4::new(false, true, true, false).to_wgsl_string();
assert_eq!(s, "vec4<bool>(false,true,true,false)");
}
#[test]
fn to_wgsl_value_f32() {
let s = CpuValue::Single(1.0_f32).to_wgsl_string();
assert_eq!(s, "1.");
let s = CpuValue::Uniform((1.0_f32, 2.0_f32)).to_wgsl_string();
assert_eq!(s, "(frand() * (2. - 1.) + 1.)");
}
#[test]
fn to_wgsl_value_vec2() {
let s = CpuValue::Single(Vec2::ONE).to_wgsl_string();
assert_eq!(s, "vec2<f32>(1.,1.)");
let s = CpuValue::Uniform((Vec2::ZERO, Vec2::ONE)).to_wgsl_string();
assert_eq!(
s,
"(frand2() * (vec2<f32>(1.,1.) - vec2<f32>(0.,0.)) + vec2<f32>(0.,0.))"
);
}
#[test]
fn to_wgsl_value_vec3() {
let s = CpuValue::Single(Vec3::ONE).to_wgsl_string();
assert_eq!(s, "vec3<f32>(1.,1.,1.)");
let s = CpuValue::Uniform((Vec3::ZERO, Vec3::ONE)).to_wgsl_string();
assert_eq!(
s,
"(frand3() * (vec3<f32>(1.,1.,1.) - vec3<f32>(0.,0.,0.)) + vec3<f32>(0.,0.,0.))"
);
}
#[test]
fn to_wgsl_value_vec4() {
let s = CpuValue::Single(Vec4::ONE).to_wgsl_string();
assert_eq!(s, "vec4<f32>(1.,1.,1.,1.)");
let s = CpuValue::Uniform((Vec4::ZERO, Vec4::ONE)).to_wgsl_string();
assert_eq!(s, "(frand4() * (vec4<f32>(1.,1.,1.,1.) - vec4<f32>(0.,0.,0.,0.)) + vec4<f32>(0.,0.,0.,0.))");
}
#[test]
fn to_shader_code() {
let mut grad = Gradient::new();
assert_eq!("", grad.to_shader_code("key"));
grad.add_key(0.0, Vec4::splat(0.0));
assert_eq!(
"// Gradient\nlet t0 = 0.;\nlet c0 = vec4<f32>(0.,0.,0.,0.);\nreturn c0;\n",
grad.to_shader_code("key")
);
grad.add_key(1.0, Vec4::new(1.0, 0.0, 0.0, 1.0));
assert_eq!(
r#"// Gradient
let t0 = 0.;
let c0 = vec4<f32>(0.,0.,0.,0.);
let t1 = 1.;
let c1 = vec4<f32>(1.,0.,0.,1.);
if (key <= t0) { return c0; }
else if (key <= t1) { return mix(c0, c1, (key - t0) / (t1 - t0)); }
else { return c1; }
"#,
grad.to_shader_code("key")
);
}
#[test]
fn test_simulation_space_eval() {
let particle_layout = ParticleLayout::empty();
let property_layout = PropertyLayout::default();
{
let ctx =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
assert!(SimulationSpace::Local.eval(&ctx).is_ok());
assert!(SimulationSpace::Global.eval(&ctx).is_err());
let particle_layout = ParticleLayout::new().append(Attribute::POSITION).build();
let ctx =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
assert!(SimulationSpace::Local.eval(&ctx).is_ok());
assert!(SimulationSpace::Global.eval(&ctx).is_ok());
}
{
let ctx =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
assert!(SimulationSpace::Local.eval(&ctx).is_ok());
assert!(SimulationSpace::Global.eval(&ctx).is_err());
let particle_layout = ParticleLayout::new().append(Attribute::POSITION).build();
let ctx =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
assert!(SimulationSpace::Local.eval(&ctx).is_ok());
assert!(SimulationSpace::Global.eval(&ctx).is_ok());
}
{
let texture_layout = TextureLayout::default();
let ctx = RenderContext::new(&property_layout, &particle_layout, &texture_layout);
assert!(SimulationSpace::Local.eval(&ctx).is_ok());
assert!(SimulationSpace::Global.eval(&ctx).is_ok());
}
}
fn make_test_app() -> App {
IoTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
.num_threads(1)
.thread_name("Hanabi test IO Task Pool".to_string())
.build()
});
let mut app = App::new();
let watch_for_changes = false;
let mut builders = app
.world_mut()
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
let dir = Dir::default();
let dummy_builder =
AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() }));
builders.insert(AssetSourceId::Default, dummy_builder);
let sources = builders.build_sources(watch_for_changes, false);
let asset_server = AssetServer::new(
sources.into(),
AssetServerMode::Unprocessed,
watch_for_changes,
UnapprovedPathMode::Forbid,
);
app.insert_resource(asset_server);
app.init_asset::<Mesh>();
app.init_asset::<bevy::mesh::skinning::SkinnedMeshInverseBindposes>();
app.init_asset::<Shader>();
app.add_plugins(VisibilityPlugin);
app.init_resource::<ShaderCache>();
app.insert_resource(Random(new_rng()));
app.init_asset::<EffectAsset>();
app.add_systems(
PostUpdate,
compile_effects.after(VisibilitySystems::CheckVisibility),
);
app
}
struct TestCase {
visibility: Option<Visibility>,
}
impl TestCase {
fn new(visibility: Option<Visibility>) -> Self {
Self { visibility }
}
}
#[test]
fn test_effect_shader_source() {
let module = Module::default();
let asset = EffectAsset::new(256, SpawnerSettings::rate(32.0.into()), module)
.with_simulation_space(SimulationSpace::Local);
assert_eq!(asset.simulation_space, SimulationSpace::Local);
let res = EffectShaderSources::generate(&asset, None, 0);
assert!(res.is_err());
let err = res.err().unwrap();
assert!(matches!(err, ShaderGenerateError::Validate(_)));
let mut module = Module::default();
let zero = module.lit(Vec3::ZERO);
let asset = EffectAsset::new(256, SpawnerSettings::rate(32.0.into()), module)
.init(SetAttributeModifier::new(Attribute::VELOCITY, zero));
assert!(asset.particle_layout().size() > 0);
let res = EffectShaderSources::generate(&asset, None, 0);
assert!(res.is_err());
let err = res.err().unwrap();
assert!(matches!(err, ShaderGenerateError::Validate(_)));
let mut module = Module::default();
let zero = module.lit(Vec3::ZERO);
let asset = EffectAsset::new(256, SpawnerSettings::rate(32.0.into()), module)
.with_simulation_space(SimulationSpace::Local)
.init(SetAttributeModifier::new(Attribute::POSITION, zero));
assert_eq!(asset.simulation_space, SimulationSpace::Local);
let res = EffectShaderSources::generate(&asset, None, 0);
assert!(res.is_ok());
let shader_source = res.unwrap();
for (name, code) in [
("Init", shader_source.init_shader_source),
("Update", shader_source.update_shader_source),
("Render", shader_source.render_shader_source),
] {
println!("{} shader:\n\n{}", name, code);
let mut shader_defs = std::collections::HashMap::<String, ShaderDefValue>::new();
shader_defs.insert("LOCAL_SPACE_SIMULATION".into(), ShaderDefValue::Bool(true));
shader_defs.insert("NEEDS_UV".into(), ShaderDefValue::Bool(true));
shader_defs.insert("NEEDS_NORMAL".into(), ShaderDefValue::Bool(false));
shader_defs.insert(
"NEEDS_PARTICLE_FRAGMENT".into(),
ShaderDefValue::Bool(false),
);
shader_defs.insert(
"PARTICLE_SCREEN_SPACE_SIZE".into(),
ShaderDefValue::Bool(true),
);
if name == "Update" {
shader_defs.insert("EM_MAX_SPAWN_ATOMIC".into(), ShaderDefValue::Bool(true));
}
let mut composer = Composer::default();
{
IoTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
.num_threads(1)
.thread_name("Hanabi test IO Task Pool".to_string())
.build()
});
let mut dummy_app = App::new();
dummy_app.add_plugins(bevy::asset::AssetPlugin::default());
dummy_app
.init_asset::<Shader>()
.init_asset_loader::<ShaderLoader>();
dummy_app.add_plugins(bevy::render::view::ViewPlugin);
let asset_server = dummy_app.world().resource::<AssetServer>();
let view_shader_handle =
asset_server.load::<Shader>("embedded://bevy_render/view/view.wgsl");
let mut max_frames = 10000; while max_frames > 0 {
dummy_app.update();
let asset_server = dummy_app.world().resource::<AssetServer>();
let load_state = asset_server.get_load_state(&view_shader_handle).unwrap();
if let LoadState::Failed(err) = load_state {
panic!("Load failed: {:?}", err);
}
if matches!(load_state, LoadState::Loaded) {
break;
}
max_frames -= 1;
}
assert!(max_frames > 0);
let shaders = dummy_app.world().get_resource::<Assets<Shader>>().unwrap();
for (id, shader) in shaders.iter() {
println!("[{id:?}] {shader:?}");
}
let view_shader = shaders.get(&view_shader_handle).unwrap();
let res = composer.add_composable_module(view_shader.into());
assert!(res.is_ok());
}
{
let min_storage_buffer_offset_alignment = 256;
let common_shader =
HanabiPlugin::make_common_shader(min_storage_buffer_offset_alignment);
let res = composer.add_composable_module((&common_shader).into());
assert!(res.is_ok());
}
match composer.make_naga_module(NagaModuleDescriptor {
source: &code[..],
file_path: &format!("{}.wgsl", name),
shader_defs,
..Default::default()
}) {
Ok(module) => {
let info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::default(),
)
.validate(&module)
.unwrap();
let wgsl = naga::back::wgsl::write_string(
&module,
&info,
naga::back::wgsl::WriterFlags::EXPLICIT_TYPES,
)
.unwrap();
println!("Final wgsl from naga:\n\n{}", wgsl);
}
Err(e) => {
panic!("{}", e.emit_to_string(&composer));
}
}
}
}
#[test]
fn test_compile_effect_invalid_handle() {
let mut app = make_test_app();
let effect_entity = {
let world = app.world_mut();
let entity = world.spawn(ParticleEffect::default()).id();
world.spawn(Camera3d::default());
entity
};
app.update();
{
let world = app.world_mut();
let (entity, particle_effect, compiled_particle_effect) = world
.query::<(Entity, &ParticleEffect, &CompiledParticleEffect)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(particle_effect.handle, Handle::<EffectAsset>::default());
assert_eq!(
compiled_particle_effect.asset,
Handle::<EffectAsset>::default()
);
assert!(compiled_particle_effect.effect_shader.is_none());
}
}
#[test]
fn test_compile_effect_changed() {
let spawner = SpawnerSettings::once(32.0.into());
let mut app = make_test_app();
let (effect_entity, handle) = {
let world = app.world_mut();
let mut assets = world.resource_mut::<Assets<EffectAsset>>();
let mut module = Module::default();
let init_pos = module.lit(Vec3::ZERO);
let mut asset = EffectAsset::new(64, spawner, module)
.init(SetAttributeModifier::new(Attribute::POSITION, init_pos));
asset.simulation_condition = SimulationCondition::Always;
let handle = assets.add(asset);
let entity = world
.spawn((
ParticleEffect::new(handle.clone()),
CompiledParticleEffect::default(),
))
.id();
world.spawn(Camera3d::default());
(entity, handle)
};
app.update();
{
let world = app.world_mut();
let (entity, particle_effect, compiled_particle_effect) = world
.query::<(Entity, &ParticleEffect, &CompiledParticleEffect)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(particle_effect.handle, handle);
assert_eq!(compiled_particle_effect.asset, handle);
assert!(compiled_particle_effect.asset.is_strong());
assert!(compiled_particle_effect.effect_shader.is_some());
}
{
let world = app.world_mut();
let mut particle_effect = world
.query::<&mut ParticleEffect>()
.iter_mut(world)
.next()
.unwrap();
particle_effect.deref_mut();
}
app.update();
{
let world = app.world_mut();
let (entity, particle_effect, compiled_particle_effect) = world
.query::<(Entity, &ParticleEffect, &CompiledParticleEffect)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(particle_effect.handle, handle);
assert_eq!(compiled_particle_effect.asset, handle);
assert!(compiled_particle_effect.asset.is_strong());
assert!(compiled_particle_effect.effect_shader.is_some());
}
}
#[test]
fn test_compile_effect_visibility() {
let spawner = SpawnerSettings::once(32.0.into());
for test_case in &[
TestCase::new(None),
TestCase::new(Some(Visibility::Hidden)),
TestCase::new(Some(Visibility::Visible)),
] {
let mut app = make_test_app();
let (effect_entity, handle) = {
let world = app.world_mut();
let mut assets = world.resource_mut::<Assets<EffectAsset>>();
let mut module = Module::default();
let init_pos = module.lit(Vec3::ZERO);
let mut asset = EffectAsset::new(64, spawner, module)
.init(SetAttributeModifier::new(Attribute::POSITION, init_pos));
asset.simulation_condition = if test_case.visibility.is_some() {
SimulationCondition::WhenVisible
} else {
SimulationCondition::Always
};
asset.simulation_space = SimulationSpace::Local;
let handle = assets.add(asset);
let entity = if let Some(visibility) = test_case.visibility {
world
.spawn((
visibility,
InheritedVisibility::default(),
ParticleEffect::new(handle.clone()),
CompiledParticleEffect::default(),
))
.id()
} else {
world
.spawn((
ParticleEffect::new(handle.clone()),
CompiledParticleEffect::default(),
))
.id()
};
world.spawn(Camera3d::default());
(entity, handle)
};
app.update();
let world = app.world_mut();
if let Some(test_visibility) = test_case.visibility {
let (
entity,
visibility,
inherited_visibility,
particle_effect,
compiled_particle_effect,
) = world
.query::<(
Entity,
&Visibility,
&InheritedVisibility,
&ParticleEffect,
&CompiledParticleEffect,
)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(visibility, test_visibility);
assert_eq!(
inherited_visibility.get(),
test_visibility == Visibility::Visible
);
assert_eq!(particle_effect.handle, handle);
assert_eq!(compiled_particle_effect.asset, handle);
assert!(compiled_particle_effect.asset.is_strong());
assert!(compiled_particle_effect.effect_shader.is_some());
let (mut visibility, _) = world
.query::<(&mut Visibility, &ParticleEffect)>()
.iter_mut(world)
.next()
.unwrap();
if *visibility == Visibility::Visible {
*visibility = Visibility::Hidden;
} else {
*visibility = Visibility::Visible;
}
app.update();
} else {
let (entity, particle_effect, compiled_particle_effect) = world
.query::<(Entity, &ParticleEffect, &CompiledParticleEffect)>()
.iter(world)
.next()
.unwrap();
assert_eq!(entity, effect_entity);
assert_eq!(particle_effect.handle, handle);
assert_eq!(compiled_particle_effect.asset, handle);
assert!(compiled_particle_effect.asset.is_strong());
assert!(compiled_particle_effect.effect_shader.is_some());
}
}
}
}