use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use bevy::{
asset::Handle,
ecs::reflect::AppTypeRegistry,
image::Image,
math::{UVec2, Vec3, Vec4},
platform::collections::HashMap,
reflect::Reflect,
};
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
pub mod accel;
pub mod attr;
pub mod force;
pub mod kill;
pub mod output;
pub mod position;
pub mod registry;
pub mod velocity;
pub use accel::*;
pub use attr::*;
pub use force::*;
pub use kill::*;
pub use output::*;
pub use position::*;
pub use registry::*;
pub use velocity::*;
use crate::{
Attribute, EvalContext, ExprError, ExprHandle, Gradient, Module, ParticleLayout,
PropertyLayout, TextureLayout,
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub enum ShapeDimension {
#[default]
Surface,
Volume,
}
pub(crate) fn calc_func_id<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModifierContext : u8 {
const Init = 0b001;
const Update = 0b010;
const Render = 0b100;
}
}
impl std::fmt::Display for ModifierContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = if self.contains(ModifierContext::Init) {
"Init".to_string()
} else {
String::new()
};
if self.contains(ModifierContext::Update) {
if s.is_empty() {
s = "Update".to_string();
} else {
s += " | Update";
}
}
if self.contains(ModifierContext::Render) {
if s.is_empty() {
s = "Render".to_string();
} else {
s += " | Render";
}
}
if s.is_empty() {
s = "None".to_string();
}
write!(f, "{}", s)
}
}
pub trait Modifier: Reflect + Send + Sync + 'static {
fn context(&self) -> ModifierContext;
fn as_render(&self) -> Option<&dyn RenderModifier> {
None
}
fn as_render_mut(&mut self) -> Option<&mut dyn RenderModifier> {
None
}
fn into_boxed_render(self: Box<Self>) -> Option<Box<dyn RenderModifier>> {
None
}
fn attributes(&self) -> &[Attribute];
fn boxed_clone(&self) -> BoxedModifier;
fn apply(&self, module: &mut Module, context: &mut ShaderWriter) -> Result<(), ExprError>;
}
pub type BoxedModifier = Box<dyn Modifier>;
impl Clone for BoxedModifier {
fn clone(&self) -> Self {
self.boxed_clone()
}
}
#[derive(Debug, PartialEq)]
pub struct ShaderWriter<'a> {
pub main_code: String,
pub extra_code: String,
pub property_layout: &'a PropertyLayout,
pub particle_layout: &'a ParticleLayout,
modifier_context: ModifierContext,
var_counter: u32,
expr_cache: HashMap<ExprHandle, String>,
is_attribute_pointer: bool,
emits_gpu_spawn_events: Option<bool>,
}
impl<'a> ShaderWriter<'a> {
pub fn new(
modifier_context: ModifierContext,
property_layout: &'a PropertyLayout,
particle_layout: &'a ParticleLayout,
) -> Self {
Self {
main_code: String::new(),
extra_code: String::new(),
property_layout,
particle_layout,
modifier_context,
var_counter: 0,
expr_cache: Default::default(),
is_attribute_pointer: false,
emits_gpu_spawn_events: None,
}
}
pub fn with_attribute_pointer(mut self) -> Self {
self.is_attribute_pointer = true;
self
}
pub fn set_emits_gpu_spawn_events(&mut self, use_events: bool) -> Result<(), ExprError> {
if let Some(was_using_events) = self.emits_gpu_spawn_events {
if was_using_events == use_events {
Ok(())
} else {
Err(ExprError::GraphEvalError(
"Conflicting use of GPU spawn events.".to_string(),
))
}
} else {
self.emits_gpu_spawn_events = Some(use_events);
Ok(())
}
}
pub fn emits_gpu_spawn_events(&self) -> Option<bool> {
self.emits_gpu_spawn_events
}
}
impl EvalContext for ShaderWriter<'_> {
fn modifier_context(&self) -> ModifierContext {
self.modifier_context
}
fn property_layout(&self) -> &PropertyLayout {
self.property_layout
}
fn particle_layout(&self) -> &ParticleLayout {
self.particle_layout
}
fn eval(&mut self, module: &Module, handle: ExprHandle) -> Result<String, ExprError> {
if let Some(s) = self.expr_cache.get(&handle) {
Ok(s.clone())
} else {
module.try_get(handle)?.eval(module, self).inspect(|s| {
self.expr_cache.insert(handle, s.clone());
})
}
}
fn make_local_var(&mut self) -> String {
let index = self.var_counter;
self.var_counter += 1;
format!("var{}", index)
}
fn push_stmt(&mut self, stmt: &str) {
self.main_code += stmt;
self.main_code += "\n";
}
fn make_fn(
&mut self,
func_name: &str,
args: &str,
module: &mut Module,
f: &mut dyn FnMut(&mut Module, &mut dyn EvalContext) -> Result<String, ExprError>,
) -> Result<(), ExprError> {
let mut ctx = ShaderWriter::new(
self.modifier_context,
self.property_layout,
self.particle_layout,
)
.with_attribute_pointer();
let body = f(module, &mut ctx)?;
self.extra_code += &ctx.extra_code;
self.extra_code += &format!(
r##"fn {0}({1}) {{
{2}{3}}}"##,
func_name, args, ctx.main_code, body
);
Ok(())
}
fn is_attribute_pointer(&self) -> bool {
self.is_attribute_pointer
}
}
#[derive(Debug, PartialEq)]
pub struct RenderContext<'a> {
pub property_layout: &'a PropertyLayout,
pub particle_layout: &'a ParticleLayout,
pub vertex_code: String,
pub fragment_code: String,
pub render_extra: String,
pub texture_layout: &'a TextureLayout,
pub textures: Vec<Handle<Image>>,
pub sprite_grid_size: Option<UVec2>,
pub gradients: HashMap<u64, Gradient<Vec4>>,
pub size_gradients: HashMap<u64, Gradient<Vec3>>,
pub needs_uv: bool,
pub needs_normal: bool,
pub needs_particle_fragment: bool,
var_counter: u32,
expr_cache: HashMap<ExprHandle, String>,
is_attribute_pointer: bool,
}
impl<'a> RenderContext<'a> {
pub fn new(
property_layout: &'a PropertyLayout,
particle_layout: &'a ParticleLayout,
texture_layout: &'a TextureLayout,
) -> Self {
Self {
property_layout,
particle_layout,
vertex_code: String::new(),
fragment_code: String::new(),
render_extra: String::new(),
texture_layout,
textures: vec![],
sprite_grid_size: None,
gradients: HashMap::default(),
size_gradients: HashMap::default(),
needs_uv: false,
needs_normal: false,
needs_particle_fragment: false,
var_counter: 0,
expr_cache: Default::default(),
is_attribute_pointer: false,
}
}
pub fn set_needs_uv(&mut self) {
self.needs_uv = true;
}
pub fn set_needs_normal(&mut self) {
self.needs_normal = true;
}
pub fn set_needs_particle_fragment(&mut self) {
self.needs_particle_fragment = true;
}
fn add_color_gradient(&mut self, gradient: Gradient<Vec4>) -> String {
let func_id = calc_func_id(&gradient);
self.gradients.insert(func_id, gradient);
let func_name = format!("color_gradient_{0:016X}", func_id);
func_name
}
fn add_size_gradient(&mut self, gradient: Gradient<Vec3>) -> String {
let func_id = calc_func_id(&gradient);
self.size_gradients.insert(func_id, gradient);
let func_name = format!("size_gradient_{0:016X}", func_id);
func_name
}
pub fn with_attribute_pointer(mut self) -> Self {
self.is_attribute_pointer = true;
self
}
}
impl EvalContext for RenderContext<'_> {
fn modifier_context(&self) -> ModifierContext {
ModifierContext::Render
}
fn property_layout(&self) -> &PropertyLayout {
self.property_layout
}
fn particle_layout(&self) -> &ParticleLayout {
self.particle_layout
}
fn eval(&mut self, module: &Module, handle: ExprHandle) -> Result<String, ExprError> {
if let Some(s) = self.expr_cache.get(&handle) {
Ok(s.clone())
} else {
module.try_get(handle)?.eval(module, self).inspect(|s| {
self.expr_cache.insert(handle, s.clone());
})
}
}
fn make_local_var(&mut self) -> String {
let index = self.var_counter;
self.var_counter += 1;
format!("var{}", index)
}
fn push_stmt(&mut self, stmt: &str) {
self.vertex_code += stmt;
self.vertex_code += "\n";
}
fn make_fn(
&mut self,
func_name: &str,
args: &str,
module: &mut Module,
f: &mut dyn FnMut(&mut Module, &mut dyn EvalContext) -> Result<String, ExprError>,
) -> Result<(), ExprError> {
let texture_layout = module.texture_layout();
let mut ctx =
RenderContext::new(self.property_layout, self.particle_layout, &texture_layout)
.with_attribute_pointer();
let body = f(module, &mut ctx)?;
self.render_extra += &ctx.render_extra;
self.render_extra += &format!(
r##"fn {0}({1}) {{
{2};
}}
"##,
func_name, args, body
);
Ok(())
}
fn is_attribute_pointer(&self) -> bool {
self.is_attribute_pointer
}
}
pub trait RenderModifier: Modifier {
fn apply_render(
&self,
module: &mut Module,
context: &mut RenderContext,
) -> Result<(), ExprError>;
fn boxed_render_clone(&self) -> Box<dyn RenderModifier>;
fn as_modifier(&self) -> &dyn Modifier;
}
impl Clone for Box<dyn RenderModifier> {
fn clone(&self) -> Self {
self.boxed_render_clone()
}
}
macro_rules! impl_mod_render {
($t:ty, $attrs:expr) => {
impl $crate::Modifier for $t {
fn context(&self) -> $crate::ModifierContext {
$crate::ModifierContext::Render
}
fn as_render(&self) -> Option<&dyn $crate::RenderModifier> {
Some(self)
}
fn as_render_mut(&mut self) -> Option<&mut dyn $crate::RenderModifier> {
Some(self)
}
fn into_boxed_render(self: Box<Self>) -> Option<Box<dyn RenderModifier>> {
Some(self)
}
fn attributes(&self) -> &[$crate::Attribute] {
$attrs
}
fn boxed_clone(&self) -> $crate::BoxedModifier {
Box::new(self.clone())
}
fn apply(
&self,
_module: &mut Module,
context: &mut ShaderWriter,
) -> Result<(), ExprError> {
Err(ExprError::InvalidModifierContext(
context.modifier_context(),
ModifierContext::Render,
))
}
}
};
}
pub(crate) use impl_mod_render;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub enum EventEmitCondition {
Always,
OnDie,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
pub struct EmitSpawnEventModifier {
pub condition: EventEmitCondition,
pub count: ExprHandle,
pub child_index: u32,
}
impl EmitSpawnEventModifier {
fn eval(
&self,
module: &mut Module,
context: &mut dyn EvalContext,
) -> Result<String, ExprError> {
let channel_index = self.child_index;
let count_val = context.eval(module, self.count)?;
let count_var = context.make_local_var();
context.push_stmt(&format!("let {} = {};", count_var, count_val));
let cond = match self.condition {
EventEmitCondition::Always => format!(
"if (is_alive) {{ append_spawn_events_{channel_index}((*effect_metadata).base_child_index, particle_index, {}); }}",
count_var
),
EventEmitCondition::OnDie => format!(
"if (was_alive && !is_alive) {{ append_spawn_events_{channel_index}((*effect_metadata).base_child_index, particle_index, {}); }}",
count_var
),
};
Ok(cond)
}
}
impl Modifier for EmitSpawnEventModifier {
fn context(&self) -> ModifierContext {
ModifierContext::Update
}
fn attributes(&self) -> &[Attribute] {
&[]
}
fn boxed_clone(&self) -> BoxedModifier {
Box::new(*self)
}
fn apply(&self, module: &mut Module, context: &mut ShaderWriter) -> Result<(), ExprError> {
let code = self.eval(module, context)?;
context.main_code += &code;
context.set_emits_gpu_spawn_events(true)?;
Ok(())
}
}
pub fn register_modifiers(type_registry: &AppTypeRegistry) {
{
let mut type_registry = type_registry.write();
type_registry.register::<AccelModifier>();
type_registry.register::<RadialAccelModifier>();
type_registry.register::<TangentAccelModifier>();
type_registry.register::<SetAttributeModifier>();
type_registry.register::<InheritAttributeModifier>();
type_registry.register::<ConformToSphereModifier>();
type_registry.register::<LinearDragModifier>();
type_registry.register::<KillSphereModifier>();
type_registry.register::<KillAabbModifier>();
type_registry.register::<ParticleTextureModifier>();
type_registry.register::<SetColorModifier>();
type_registry.register::<ColorOverLifetimeModifier>();
type_registry.register::<SetSizeModifier>();
type_registry.register::<SizeOverLifetimeModifier>();
type_registry.register::<OrientModifier>();
type_registry.register::<FlipbookModifier>();
type_registry.register::<ScreenSpaceSizeModifier>();
type_registry.register::<RoundModifier>();
type_registry.register::<SetPositionCircleModifier>();
type_registry.register::<SetPositionSphereModifier>();
type_registry.register::<SetPositionCone3dModifier>();
type_registry.register::<SetVelocityCircleModifier>();
type_registry.register::<SetVelocitySphereModifier>();
type_registry.register::<SetVelocityTangentModifier>();
type_registry.register::<crate::modifier::registry::Modifiers>();
}
register_reflect_modifier::<AccelModifier>(type_registry, |module| {
let accel = module.lit(Vec3::X);
Box::new(AccelModifier::new(accel))
});
register_reflect_modifier::<RadialAccelModifier>(type_registry, |module| {
let origin = module.lit(Vec3::ZERO);
let accel = module.lit(1.0);
Box::new(RadialAccelModifier::new(origin, accel))
});
register_reflect_modifier::<TangentAccelModifier>(type_registry, |module| {
let origin = module.lit(Vec3::ZERO);
let axis = module.lit(Vec3::X);
let accel = module.lit(1.0);
Box::new(TangentAccelModifier::new(origin, axis, accel))
});
register_reflect_modifier::<SetAttributeModifier>(type_registry, |module| {
let value = module.lit(1.0);
Box::new(SetAttributeModifier::new(Attribute::LIFETIME, value))
});
register_reflect_modifier::<InheritAttributeModifier>(type_registry, |_| {
Box::new(InheritAttributeModifier::new(Attribute::LIFETIME))
});
register_reflect_modifier::<ConformToSphereModifier>(type_registry, |module| {
let origin = module.lit(Vec3::ZERO);
let radius = module.lit(1.0);
let influence_dist = module.lit(10.0);
let attraction_accel = module.lit(1.0);
let max_attraction_speed = module.lit(1.0);
Box::new(ConformToSphereModifier::new(
origin,
radius,
influence_dist,
attraction_accel,
max_attraction_speed,
))
});
register_reflect_modifier::<LinearDragModifier>(type_registry, |module| {
let drag = module.lit(1.0);
Box::new(LinearDragModifier::new(drag))
});
register_reflect_modifier::<KillSphereModifier>(type_registry, |module| {
let center = module.lit(Vec3::ZERO);
let sqr_radius = module.lit(1.0);
Box::new(KillSphereModifier::new(center, sqr_radius))
});
register_reflect_modifier::<KillAabbModifier>(type_registry, |module| {
let center = module.lit(Vec3::ZERO);
let sqr_radius = module.lit(1.0);
Box::new(KillAabbModifier::new(center, sqr_radius))
});
register_reflect_modifier::<ParticleTextureModifier>(type_registry, |module| {
let slot = module.lit(0u32);
Box::new(ParticleTextureModifier::new(slot))
});
register_reflect_modifier::<SetColorModifier>(type_registry, |_| {
Box::new(SetColorModifier::new(Vec4::ONE))
});
register_reflect_modifier::<ColorOverLifetimeModifier>(type_registry, |_| {
Box::new(ColorOverLifetimeModifier::new(Gradient::constant(
Vec4::ONE,
)))
});
register_reflect_modifier::<SetSizeModifier>(type_registry, |_| {
Box::new(SetSizeModifier {
size: Vec3::ONE.into(),
})
});
register_reflect_modifier::<SizeOverLifetimeModifier>(type_registry, |_| {
Box::new(SizeOverLifetimeModifier {
gradient: Gradient::constant(Vec3::ONE),
screen_space_size: false,
})
});
register_reflect_modifier::<OrientModifier>(type_registry, |_| {
Box::new(OrientModifier::new(OrientMode::default()))
});
register_reflect_modifier::<FlipbookModifier>(type_registry, |_| {
Box::new(FlipbookModifier::default())
});
register_reflect_modifier::<ScreenSpaceSizeModifier>(type_registry, |_| {
Box::new(ScreenSpaceSizeModifier)
});
register_reflect_modifier::<RoundModifier>(type_registry, |module| {
Box::new(RoundModifier::constant(module, 1.0))
});
register_reflect_modifier::<SetPositionCircleModifier>(type_registry, |module| {
Box::new(SetPositionCircleModifier {
center: module.lit(Vec3::ZERO),
axis: module.lit(Vec3::Z),
radius: module.lit(1.0),
dimension: ShapeDimension::Surface,
})
});
register_reflect_modifier::<SetPositionSphereModifier>(type_registry, |module| {
Box::new(SetPositionSphereModifier {
center: module.lit(Vec3::ZERO),
radius: module.lit(1.0),
dimension: ShapeDimension::Surface,
})
});
register_reflect_modifier::<SetPositionCone3dModifier>(type_registry, |module| {
Box::new(SetPositionCone3dModifier {
height: module.lit(1.0),
base_radius: module.lit(1.0),
top_radius: module.lit(0.0),
dimension: ShapeDimension::Surface,
})
});
register_reflect_modifier::<SetVelocityCircleModifier>(type_registry, |module| {
Box::new(SetVelocityCircleModifier {
center: module.lit(Vec3::ZERO),
axis: module.lit(Vec3::Z),
speed: module.lit(1.0),
})
});
register_reflect_modifier::<SetVelocitySphereModifier>(type_registry, |module| {
Box::new(SetVelocitySphereModifier {
center: module.lit(Vec3::ZERO),
speed: module.lit(1.0),
})
});
register_reflect_modifier::<SetVelocityTangentModifier>(type_registry, |module| {
Box::new(SetVelocityTangentModifier {
origin: module.lit(Vec3::ZERO),
axis: module.lit(Vec3::X),
speed: module.lit(1.0),
})
});
}
#[cfg(test)]
mod tests {
use bevy::prelude::*;
use naga::front::wgsl::Frontend;
use super::*;
use crate::{BuiltInOperator, ExprWriter, ScalarType};
fn make_test_modifier() -> SetPositionSphereModifier {
let mut m = Module::default();
SetPositionSphereModifier {
center: m.lit(Vec3::ZERO),
radius: m.lit(1.),
dimension: ShapeDimension::Surface,
}
}
#[test]
fn modifier_into_render() {
let original = SetSizeModifier {
size: Vec3::ONE.into(),
};
let original = Box::new(original);
let before: *const dyn RenderModifier = &*original;
let modifier: Box<dyn Modifier> = original;
let modifier = modifier.into_boxed_render();
assert!(modifier.is_some());
let modifier = modifier.unwrap();
let after: *const dyn RenderModifier = &*modifier;
assert_eq!(before.addr(), after.addr());
let modifier = modifier.boxed_render_clone();
let after: *const dyn RenderModifier = &*modifier;
assert_ne!(before.addr(), after.addr());
}
#[test]
fn modifier_context_display() {
assert_eq!("None", format!("{}", ModifierContext::empty()));
assert_eq!("Init", format!("{}", ModifierContext::Init));
assert_eq!("Update", format!("{}", ModifierContext::Update));
assert_eq!("Render", format!("{}", ModifierContext::Render));
assert_eq!(
"Init | Update",
format!("{}", ModifierContext::Init | ModifierContext::Update)
);
assert_eq!(
"Update | Render",
format!("{}", ModifierContext::Update | ModifierContext::Render)
);
assert_eq!(
"Init | Render",
format!("{}", ModifierContext::Init | ModifierContext::Render)
);
assert_eq!(
"Init | Update | Render",
format!("{}", ModifierContext::all())
);
}
#[test]
fn reflect() {
let m = make_test_modifier();
let reflect: &dyn Reflect = m.as_reflect();
assert!(reflect.is::<SetPositionSphereModifier>());
let m_reflect = reflect.downcast_ref::<SetPositionSphereModifier>().unwrap();
assert_eq!(*m_reflect, m);
}
#[test]
fn serde() {
use serde::de::DeserializeSeed as _;
let m = make_test_modifier();
let bm: BoxedModifier = Box::new(m);
let type_registry = AppTypeRegistry::new_with_derived_types();
register_modifiers(&type_registry);
let registry = type_registry.read();
let serializer = bevy::reflect::serde::ReflectSerializer::new(bm.as_reflect(), ®istry);
let s = ron::to_string(&serializer).unwrap();
println!("modifier: {:?}", s);
let mut de = ron::de::Deserializer::from_str(&s).unwrap();
let reflect_deser = bevy::reflect::serde::ReflectDeserializer::new(®istry);
let boxed_partial = reflect_deser.deserialize(&mut de).unwrap();
let type_info = boxed_partial
.get_represented_type_info()
.expect("reflected value has no represented type info");
let type_id = type_info.type_id();
let reflect_modifier = registry
.get_type_data::<crate::modifier::registry::ReflectModifier>(type_id)
.expect("no ReflectModifier type data for type");
let rfr = registry
.get_type_data::<bevy::reflect::ReflectFromReflect>(type_id)
.expect("no ReflectFromReflect data for type");
let concrete_reflect = rfr
.from_reflect(boxed_partial.as_partial_reflect())
.expect("from_reflect failed");
let mut module = Module::default();
let mut modifier: BoxedModifier = (reflect_modifier.factory)(&mut module);
let reflect_mut: &mut dyn Reflect = Reflect::as_reflect_mut(&mut *modifier);
reflect_mut
.set(concrete_reflect)
.expect("failed to assign reflect value to modifier instance");
let m_serde = modifier;
let rm: &dyn Reflect = m.as_reflect();
let rm_serde: &dyn Reflect = m_serde.as_reflect();
assert_eq!(
rm.get_represented_type_info().unwrap().type_id(),
rm_serde.get_represented_type_info().unwrap().type_id()
);
assert!(rm_serde.is::<SetPositionSphereModifier>());
let rm_reflect = rm_serde
.downcast_ref::<SetPositionSphereModifier>()
.unwrap();
assert_eq!(*rm_reflect, m);
}
#[test]
fn validate_init() {
let mut module = Module::default();
let center = module.lit(Vec3::ZERO);
let axis = module.lit(Vec3::Y);
let radius = module.lit(1.);
let modifiers: &[&dyn Modifier] = &[
&SetPositionCircleModifier {
center,
axis,
radius,
dimension: ShapeDimension::Volume,
},
&SetPositionSphereModifier {
center,
radius,
dimension: ShapeDimension::Volume,
},
&SetPositionCone3dModifier {
base_radius: radius,
top_radius: radius,
height: radius,
dimension: ShapeDimension::Volume,
},
&SetVelocityCircleModifier {
center,
axis,
speed: radius,
},
&SetVelocitySphereModifier {
center,
speed: radius,
},
&SetVelocityTangentModifier {
origin: center,
axis,
speed: radius,
},
];
for &modifier in modifiers.iter() {
assert!(modifier.context().contains(ModifierContext::Init));
let property_layout = PropertyLayout::default();
let particle_layout = ParticleLayout::default();
let mut context =
ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
assert!(modifier.apply(&mut module, &mut context).is_ok());
let main_code = context.main_code;
let extra_code = context.extra_code;
let mut particle_layout = ParticleLayout::new();
for &attr in modifier.attributes() {
particle_layout = particle_layout.append(attr);
}
let particle_layout = particle_layout.build();
let attributes_code = particle_layout.generate_code();
let code = format!(
r##"fn frand() -> f32 {{
return 0.0;
}}
const tau: f32 = 6.283185307179586476925286766559;
struct Particle {{
{attributes_code}
}};
{extra_code}
@compute @workgroup_size(64)
fn main() {{
var particle = Particle();
var transform: mat4x4<f32> = mat4x4<f32>();
{main_code}
}}"##
);
let mut frontend = Frontend::new();
let res = frontend.parse(&code);
if let Err(err) = &res {
println!(
"Modifier: {:?}",
modifier.get_represented_type_info().unwrap().type_path()
);
println!("Code: {:?}", code);
println!("Err: {:?}", err);
}
assert!(res.is_ok());
}
}
#[test]
fn validate_update() {
let writer = ExprWriter::new();
let origin = writer.lit(Vec3::ZERO).expr();
let center = origin;
let axis = origin;
let y_axis = writer.lit(Vec3::Y).expr();
let one = writer.lit(1.).expr();
let radius = one;
let modifiers: &[&dyn Modifier] = &[
&AccelModifier::new(origin),
&RadialAccelModifier::new(origin, one),
&TangentAccelModifier::new(origin, y_axis, one),
&ConformToSphereModifier::new(origin, one, one, one, one),
&LinearDragModifier::new(writer.lit(3.5).expr()),
&KillAabbModifier::new(writer.lit(Vec3::ZERO).expr(), writer.lit(Vec3::ONE).expr()),
&SetPositionCircleModifier {
center,
axis,
radius,
dimension: ShapeDimension::Volume,
},
&SetPositionSphereModifier {
center,
radius,
dimension: ShapeDimension::Volume,
},
&SetPositionCone3dModifier {
base_radius: radius,
top_radius: radius,
height: radius,
dimension: ShapeDimension::Volume,
},
&SetVelocityCircleModifier {
center,
axis,
speed: radius,
},
&SetVelocitySphereModifier {
center,
speed: radius,
},
&SetVelocityTangentModifier {
origin: center,
axis,
speed: radius,
},
];
let mut module = writer.finish();
for &modifier in modifiers.iter() {
assert!(modifier.context().contains(ModifierContext::Update));
let property_layout = PropertyLayout::default();
let particle_layout = ParticleLayout::default();
let mut context =
ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
assert!(modifier.apply(&mut module, &mut context).is_ok());
let update_code = context.main_code;
let update_extra = context.extra_code;
let mut particle_layout = ParticleLayout::new();
for &attr in modifier.attributes() {
particle_layout = particle_layout.append(attr);
}
let particle_layout = particle_layout.build();
let attributes_code = particle_layout.generate_code();
let code = format!(
r##"fn frand() -> f32 {{
return 0.0;
}}
const tau: f32 = 6.283185307179586476925286766559;
struct Particle {{
{attributes_code}
}};
struct ParticleBuffer {{
particles: array<Particle>,
}};
struct SimParams {{
delta_time: f32,
time: f32,
virtual_delta_time: f32,
virtual_time: f32,
real_delta_time: f32,
real_time: f32,
}};
struct Spawner {{
transform: mat3x4<f32>, // transposed (row-major)
spawn: atomic<i32>,
seed: u32,
count_unused: u32,
effect_index: u32,
}};
fn proj(u: vec3<f32>, v: vec3<f32>) -> vec3<f32> {{
return dot(v, u) / dot(u,u) * u;
}}
{update_extra}
@group(0) @binding(0) var<uniform> sim_params : SimParams;
@group(1) @binding(0) var<storage, read_write> particle_buffer : ParticleBuffer;
@group(2) @binding(0) var<storage, read_write> spawner : Spawner; // NOTE - same group as init
@compute @workgroup_size(64)
fn main() {{
var particle: Particle = particle_buffer.particles[0];
var transform: mat4x4<f32> = mat4x4<f32>();
var is_alive = true;
{update_code}
}}"##
);
let mut frontend = Frontend::new();
let res = frontend.parse(&code);
if let Err(err) = &res {
println!(
"Modifier: {:?}",
modifier.get_represented_type_info().unwrap().type_path()
);
println!("Code: {:?}", code);
println!("Err: {:?}", err);
}
assert!(res.is_ok());
}
}
#[test]
fn validate_render() {
let mut base_module = Module::default();
let slot_zero = base_module.lit(0u32);
let modifiers: &[&dyn RenderModifier] = &[
&ParticleTextureModifier::new(slot_zero),
&ColorOverLifetimeModifier::default(),
&SizeOverLifetimeModifier::default(),
&OrientModifier::new(OrientMode::ParallelCameraDepthPlane),
&OrientModifier::new(OrientMode::FaceCameraPosition),
&OrientModifier::new(OrientMode::AlongVelocity),
];
for &modifier in modifiers.iter() {
let mut module = base_module.clone();
let property_layout = PropertyLayout::default();
let particle_layout = ParticleLayout::default();
let texture_layout = module.texture_layout();
let mut context =
RenderContext::new(&property_layout, &particle_layout, &texture_layout);
modifier
.apply_render(&mut module, &mut context)
.expect("Failed to apply modifier to render context.");
let vertex_code = context.vertex_code;
let fragment_code = context.fragment_code;
let render_extra = context.render_extra;
let mut particle_layout = ParticleLayout::new();
for &attr in modifier.attributes() {
particle_layout = particle_layout.append(attr);
}
let particle_layout = particle_layout.build();
let attributes_code = particle_layout.generate_code();
let code = format!(
r##"
struct ColorGrading {{
balance: mat3x3<f32>,
saturation: vec3<f32>,
contrast: vec3<f32>,
gamma: vec3<f32>,
gain: vec3<f32>,
lift: vec3<f32>,
midtone_range: vec2<f32>,
exposure: f32,
hue: f32,
post_saturation: f32,
}}
struct View {{
clip_from_world: mat4x4<f32>,
unjittered_clip_from_world: mat4x4<f32>,
world_from_clip: mat4x4<f32>,
world_from_view: mat4x4<f32>,
view_from_world: mat4x4<f32>,
clip_from_view: mat4x4<f32>,
view_from_clip: mat4x4<f32>,
world_position: vec3<f32>,
exposure: f32,
// viewport(x_origin, y_origin, width, height)
viewport: vec4<f32>,
frustum: array<vec4<f32>, 6>,
color_grading: ColorGrading,
mip_bias: f32,
}}
fn frand() -> f32 {{ return 0.0; }}
fn get_camera_position_effect_space() -> vec3<f32> {{ return vec3<f32>(); }}
fn get_camera_rotation_effect_space() -> mat3x3<f32> {{ return mat3x3<f32>(); }}
const tau: f32 = 6.283185307179586476925286766559;
struct Particle {{
{attributes_code}
}};
struct VertexOutput {{
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
}};
@group(0) @binding(0) var<uniform> view: View;
{render_extra}
@compute @workgroup_size(64)
fn main() {{
var particle = Particle();
var position = vec3<f32>(0.0, 0.0, 0.0);
var velocity = vec3<f32>(0.0, 0.0, 0.0);
var size = vec3<f32>(1.0, 1.0, 1.0);
var axis_x = vec3<f32>(1.0, 0.0, 0.0);
var axis_y = vec3<f32>(0.0, 1.0, 0.0);
var axis_z = vec3<f32>(0.0, 0.0, 1.0);
var color = vec4<f32>(1.0, 1.0, 1.0, 1.0);
{vertex_code}
var out: VertexOutput;
return out;
}}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {{
var color = vec4<f32>(0.0);
var uv = vec2<f32>(0.0);
{fragment_code}
return vec4<f32>(1.0);
}}"##
);
let mut frontend = Frontend::new();
let res = frontend.parse(&code);
if let Err(err) = &res {
println!(
"Modifier: {:?}",
modifier.get_represented_type_info().unwrap().type_path()
);
println!("Code: {:?}", code);
println!("Err: {:?}", err);
}
assert!(res.is_ok());
}
}
#[test]
fn eval_cached() {
let mut module = Module::default();
let property_layout = PropertyLayout::default();
let particle_layout = ParticleLayout::default();
let x = module.builtin(BuiltInOperator::Rand(ScalarType::Float.into()));
let texture_layout = module.texture_layout();
let init: &mut dyn EvalContext =
&mut ShaderWriter::new(ModifierContext::Init, &property_layout, &particle_layout);
let update: &mut dyn EvalContext =
&mut ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
let render: &mut dyn EvalContext =
&mut RenderContext::new(&property_layout, &particle_layout, &texture_layout);
for ctx in [init, update, render] {
let s = ctx.eval(&module, x).unwrap();
assert_eq!(s, "var0");
let s2 = ctx.eval(&module, x).unwrap();
assert_eq!(s2, s);
}
}
}