use serde::Serialize;
use serde_json::Value as Json;
use crate::{Component, Entity, JsonError, Transform, World};
mod motion;
pub use motion::{MotionState, Mover};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Argument {
Constant,
Age,
ActiveTime,
EmissionTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Interpolation {
Step,
Linear,
Bezier,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct Key {
pub at: f32,
pub value: [f32; 4],
pub handle_in: [f32; 4],
pub handle_out: [f32; 4],
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Curve {
pub width: u8,
pub interpolation: Interpolation,
pub looped: bool,
pub keys: Vec<Key>,
}
impl Curve {
pub fn constant(values: &[f32]) -> Curve {
let mut value = [0.0; 4];
for (slot, v) in value.iter_mut().zip(values) {
*slot = *v;
}
Curve {
width: values.len().clamp(1, 4) as u8,
interpolation: Interpolation::Linear,
looped: false,
keys: vec![Key {
at: 0.0,
value,
handle_in: [0.0; 4],
handle_out: [0.0; 4],
}],
}
}
pub fn value_at(&self, x: f32) -> [f32; 4] {
let keys = &self.keys;
let (Some(first), Some(last)) = (keys.first(), keys.last()) else {
return [0.0; 4];
};
let x = if self.looped {
if last.at <= 0.0 {
return first.value;
}
x.rem_euclid(last.at)
} else {
x
};
if keys.len() == 1 || x <= first.at {
return first.value;
}
if x >= last.at {
return last.value;
}
let b = keys.partition_point(|k| k.at <= x);
let (a, b) = (&keys[b - 1], &keys[b]);
let d = b.at - a.at;
if d <= 0.0 {
return b.value;
}
let u = (x - a.at) / d;
let mut out = [0.0; 4];
for (c, slot) in out
.iter_mut()
.enumerate()
.take(usize::from(self.width.clamp(1, 4)))
{
*slot = match self.interpolation {
Interpolation::Step => a.value[c],
Interpolation::Linear => a.value[c] + (b.value[c] - a.value[c]) * u,
Interpolation::Bezier => {
let (p0, p1, p2, p3) = (
a.value[c],
a.value[c] + a.handle_out[c],
b.value[c] - b.handle_in[c],
b.value[c],
);
let k = 1.0 - u;
k * (k * k * p0 + 3.0 * u * (k * p1 + u * p2)) + u * u * u * p3
}
};
}
out
}
fn check(&self, what: &str) -> Result<(), String> {
if !(1..=4).contains(&self.width) {
return Err(format!("{what}: {} components", self.width));
}
if self.keys.is_empty() {
return Err(format!("{what}: no keys"));
}
let mut previous = f32::NEG_INFINITY;
for (i, k) in self.keys.iter().enumerate() {
if !(k.at.is_finite()
&& k.value
.iter()
.chain(&k.handle_in)
.chain(&k.handle_out)
.all(|v| v.is_finite()))
{
return Err(format!("{what}: key {i} is not finite"));
}
if k.at < previous {
return Err(format!("{what}: key {i} at {} goes back from {previous}", k.at));
}
previous = k.at;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Variation {
Delta { max: [f32; 4], stream: u32 },
Fraction { max: [f32; 4], stream: u32 },
Range {
min: [f32; 4],
max: [f32; 4],
stream: u32,
},
Scaled { input: u32, factor: Curve },
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Value {
pub argument: Argument,
pub curve: Curve,
pub variation: Vec<Variation>,
}
impl Value {
pub fn constant(values: &[f32]) -> Value {
Value {
argument: Argument::Constant,
curve: Curve::constant(values),
variation: Vec::new(),
}
}
pub fn is_fixed(&self) -> bool {
self.variation.is_empty() && (self.argument == Argument::Constant || self.curve.keys.len() == 1)
}
fn check(&self, what: &str, inputs: usize) -> Result<(), String> {
self.curve.check(what)?;
for v in &self.variation {
match v {
Variation::Delta { max, .. } | Variation::Fraction { max, .. } => {
if !max.iter().all(|m| m.is_finite()) {
return Err(format!("{what}: a variation that is not finite"));
}
}
Variation::Range { min, max, .. } => {
if !min.iter().chain(max).all(|m| m.is_finite()) {
return Err(format!("{what}: a variation that is not finite"));
}
}
Variation::Scaled { input, factor } => {
if usize::try_from(*input).is_ok_and(|i| i >= inputs) {
return Err(format!("{what}: input {input} of {inputs}"));
}
factor.check(what)?;
}
}
}
Ok(())
}
}
pub fn unit(seed: u64, emitter: u32, scope: u64, stream: u32, component: u32) -> f32 {
fn mix(z: u64) -> u64 {
let z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
let z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
let z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
let mut x = mix(seed);
for v in [u64::from(emitter), scope, u64::from(stream), u64::from(component)] {
x = mix(x ^ v);
}
(x >> 40) as f32 / 16_777_216.0
}
impl Value {
pub fn eval(&self, x: f32, seed: u64, emitter: u32, scope: u64, input: &dyn Fn(u32) -> f32) -> [f32; 4] {
let at = if self.argument == Argument::Constant {
0.0
} else {
x
};
let mut v = self.curve.value_at(at);
let width = usize::from(self.curve.width.clamp(1, 4));
for var in &self.variation {
match var {
Variation::Delta { max, stream } => {
for (c, slot) in v.iter_mut().enumerate().take(width) {
let u = unit(seed, emitter, scope, *stream, c as u32);
*slot += (2.0 * u - 1.0) * max[c];
}
}
Variation::Fraction { max, stream } => {
for (c, slot) in v.iter_mut().enumerate().take(width) {
let u = unit(seed, emitter, scope, *stream, c as u32);
*slot += (2.0 * u - 1.0) * max[c] * *slot;
}
}
Variation::Range { min, max, stream } => {
for (c, slot) in v.iter_mut().enumerate().take(width) {
let u = unit(seed, emitter, scope, *stream, c as u32);
*slot += min[c] + u * (max[c] - min[c]);
}
}
Variation::Scaled { input: i, factor } => {
let f = factor.value_at(input(*i));
for (c, slot) in v.iter_mut().enumerate().take(width) {
*slot *= f[c.min(usize::from(factor.width.clamp(1, 4)) - 1)];
}
}
}
}
v
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Input {
pub name: String,
pub default: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Follow {
None,
Frame,
Position,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Anchor {
Parent,
System,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct Attachment {
pub anchor: Anchor,
pub follow: Follow,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Spawn {
Once,
Periodic {
interval: Value,
count: Value,
total: Option<u32>,
max_alive: Option<u32>,
},
Distance {
step: Value,
count: Value,
total: Option<u32>,
max_alive: Option<u32>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Axis {
X,
Y,
Z,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Shape {
Point,
Disk { radius: Value, concentration: Value },
Rectangle { size: [Value; 2], concentration: Value },
Sphere { radius: Value, volume: bool },
Box { size: [Value; 3], volume: bool },
Cylinder {
radius: Value,
height: Value,
volume: bool,
axis: Axis,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum Heading {
Shape { facing: [f32; 3] },
World([f32; 3]),
Frame([f32; 3]),
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Spread {
None,
Cone { angle: Value, concentration: Value },
Ellipse {
x: Value,
y: Value,
concentration: Value,
},
Rectangle {
x: Value,
y: Value,
concentration: Value,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Drive {
None,
Speed { speed: Value, multiplier: Value },
Acceleration { acceleration: Value, multiplier: Value },
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Turns {
pub interval: f32,
pub max_angle: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Drag {
pub factor: Value,
pub rotation: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Movement {
pub drive: Drive,
pub gravity: Value,
pub turns: Option<Turns>,
pub drag: Option<Drag>,
pub fields: Option<FieldResponse>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct FieldResponse {
pub speed: Value,
pub speed_multiplier: Value,
pub acceleration: Value,
pub acceleration_multiplier: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Law {
pub lifetime: Option<Value>,
pub attachment: Attachment,
pub movement: Option<Movement>,
pub initial_speed: Value,
pub size_scale: [Value; 3],
pub tint: Value,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Blend {
Replace,
Alpha,
Additive,
Subtractive,
Multiply,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum Orientation {
CameraPlane,
Camera,
CameraYaw,
FrameYaw,
World([f32; 3]),
Frame([f32; 3]),
Heading,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct Map {
pub texture: Entity,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Atlas {
pub columns: u32,
pub cells: u32,
pub cell: Value,
pub blend_cells: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Term {
pub value: Value,
pub clamp: bool,
pub channels: Channels,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Channels {
Rgba,
Rgb,
Alpha,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
pub struct DistanceFade {
pub near: Option<[f32; 2]>,
pub far: Option<[f32; 2]>,
pub cut_near: Option<f32>,
pub cut_far: Option<f32>,
}
impl DistanceFade {
pub fn is_none(&self) -> bool {
*self == DistanceFade::default()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct SoftEdge {
pub distance: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Quad {
pub map: Option<Map>,
pub blend: Blend,
pub orientation: Orientation,
pub size: [Value; 2],
pub uniform: bool,
pub size_variance: [f32; 2],
pub offset: [Value; 3],
pub rotation: [Value; 3],
pub spin: [Value; 3],
pub scale_before_rotation: bool,
pub atlas: Option<Atlas>,
pub tint: Vec<Term>,
pub alpha_fade_threshold: Value,
pub alpha_cutoff: Value,
pub soft: Option<SoftEdge>,
pub distance: Option<DistanceFade>,
pub octagonal: bool,
pub lit: Option<Lit>,
pub bloom: Option<[f32; 4]>,
pub premultiply_alpha: bool,
pub depth_bias: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Lit {
pub normal_map: Option<Map>,
pub specular_map: Option<Map>,
pub glossiness: f32,
pub specularity: f32,
pub shadow: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct LayeredQuad {
pub quad: Quad,
pub base: Layer,
pub layers: Vec<LaidLayer>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Layer {
pub map: Option<Map>,
pub tint: Value,
pub scroll: [Value; 2],
pub offset: [Value; 2],
pub scale: [Value; 2],
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct LaidLayer {
pub layer: Layer,
pub target: LayerTarget,
pub operation: LayerOperation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum LayerTarget {
Colour,
Opacity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum LayerOperation {
Multiply,
Add,
Overlay,
}
impl LayerOperation {
pub fn apply(self, b: f32, l: f32, w: f32) -> f32 {
match self {
LayerOperation::Multiply => b * (1.0 + (l - 1.0) * w),
LayerOperation::Add => b + l * w,
LayerOperation::Overlay => {
let overlay = if b < 0.5 {
2.0 * b * l
} else {
1.0 - 2.0 * (1.0 - b) * (1.0 - l)
};
b + (overlay - b) * w
}
}
}
}
pub fn compose_layers(
premultiply_alpha: bool,
base: [f32; 4],
laid: &[(LayerTarget, LayerOperation, [f32; 4])],
) -> [f32; 4] {
let colour = |s: [f32; 4]| {
let k = if premultiply_alpha { s[3] } else { 1.0 };
[s[0] * k, s[1] * k, s[2] * k]
};
let [r, g, b] = colour(base);
let mut out = [r, g, b, base[3]];
for &(target, operation, sample) in laid {
let c = colour(sample);
let w = sample[3];
match target {
LayerTarget::Colour => {
for (o, l) in out.iter_mut().zip(c) {
*o = operation.apply(*o, l, w);
}
}
LayerTarget::Opacity => {
let brightness = ((c[0] + c[1] + c[2]) / 3.0).clamp(0.0, 1.0);
out[3] = operation.apply(out[3], brightness, w);
}
}
}
out
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Point {
pub map: Option<Map>,
pub blend: Blend,
pub size: Value,
pub tint: Vec<Term>,
pub alpha_cutoff: Value,
pub soft: Option<SoftEdge>,
pub distance: Option<DistanceFade>,
pub bloom: Option<[f32; 4]>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MeshParticle {
pub mesh: Entity,
pub clip: Option<MeshClip>,
pub blend: Blend,
pub orientation: MeshOrientation,
pub size: [Value; 3],
pub size_variance: [f32; 3],
pub uniform: bool,
pub rotation: [Value; 3],
pub spin: [Value; 3],
pub tint: Vec<Term>,
pub scroll: [Value; 2],
pub offset: [Value; 2],
pub atlas: Option<Atlas>,
pub soft: Option<SoftEdge>,
pub distance: Option<DistanceFade>,
pub lit: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MeshClip {
pub clip: Entity,
pub speed: f32,
pub looped: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum MeshOrientation {
Frame,
World,
Heading { forward: [f32; 3] },
CameraPlane,
CameraYaw,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Trail {
pub map: Option<Map>,
pub blend: Blend,
pub orientation: TrailOrientation,
pub width: Value,
pub width_scale: Value,
pub segment_interval: f32,
pub segment_life: f32,
pub max_segments: u32,
pub subdivisions: u32,
pub texture_fraction: Value,
pub scroll_u: Value,
pub variance_u: f32,
pub variance_v: Value,
pub attached_uv: bool,
pub dynamic_opacity: bool,
pub fade_start: Value,
pub fade_end: Value,
pub alpha_fade_threshold: Value,
pub alpha_cutoff: Value,
pub atlas: Option<Atlas>,
pub tint: Vec<Term>,
pub fade_out: f32,
pub distortion: Option<TrailDistortion>,
pub lit: Option<Lit>,
pub premultiply_alpha: bool,
pub soft: Option<SoftEdge>,
pub distance: DistanceFade,
pub bloom: Option<[f32; 4]>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct TrailDistortion {
pub normal_map: Option<Map>,
pub intensity: Value,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum TrailOrientation {
Camera,
Frame,
World,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Streak {
pub map: Option<Map>,
pub blend: Blend,
pub width: Option<Value>,
pub length: Value,
pub start_colour: Value,
pub end_colour: Value,
pub tint: Vec<Term>,
pub alpha_cutoff: Value,
pub soft: Option<SoftEdge>,
pub distance: DistanceFade,
pub bloom: Option<[f32; 4]>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Refraction {
pub mode: RefractionMode,
pub shape: RefractionShape,
pub orientation: Orientation,
pub normal_map: Option<Map>,
pub mask: Option<Map>,
pub offset: [Value; 3],
pub size: [Value; 3],
pub uniform: bool,
pub size_variance: [f32; 3],
pub intensity: Value,
pub stir_speed: Value,
pub radius: Value,
pub normal_offset: [Value; 2],
pub normal_scroll: [Value; 2],
pub normal_scale: [Value; 2],
pub tint: Vec<Term>,
pub blend: Blend,
pub alpha_cutoff: Value,
pub distance: DistanceFade,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum RefractionMode {
Stir,
Push,
Twist,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum RefractionShape {
Rectangle,
Hemiellipsoid,
Ellipsoid,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct RadialBlur {
pub mask: Option<Map>,
pub blend: Blend,
pub offset: [Value; 3],
pub size: [Value; 2],
pub uniform: bool,
pub radius: Value,
pub iterations: u32,
pub tint: Vec<Term>,
pub alpha_cutoff: Value,
pub distance: DistanceFade,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct LightDraw {
pub kind: LightKind,
pub colour: Value,
pub intensity: Value,
pub range: Value,
pub shadows: bool,
pub fade_out: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum LightKind {
Point,
Spot {
inner: f32,
outer: f32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Flare {
pub layers: Vec<FlareLayer>,
pub blend: Blend,
pub source_size: f32,
pub opacity_transition: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Reflection {
None,
Radial,
Horizontal,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct FlareLayer {
pub map: Option<Map>,
pub count: u32,
pub width: Value,
pub height: Value,
pub uniform: bool,
pub scale_variation: [f32; 2],
pub colour: Value,
pub colour_multiplier: [f32; 4],
pub reflection: Reflection,
pub offset: f32,
pub offset_variation: f32,
pub attenuation_radius: f32,
pub bloom: Option<[f32; 4]>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum ForceKind {
Wind,
Gravity,
Turbulence,
Cancel,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum ForceVolume {
Boundless,
Box {
half: [f32; 3],
},
Sphere {
radius: f32,
},
Cylinder {
radius: f32,
half_height: f32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ForceField {
pub kind: ForceKind,
pub volume: ForceVolume,
pub direction: [f32; 3],
pub strength: Value,
pub multiplier: f32,
pub random_multiplier: [f32; 2],
pub fade_out: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Ripples {
pub map: Option<Map>,
pub depth: f32,
pub size: f32,
pub descent: f32,
pub duration: f32,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Draw {
Quad(Quad),
LayeredQuad(Box<LayeredQuad>),
Point(Point),
Mesh(MeshParticle),
Trail(Trail),
Streak(Streak),
Refraction(Refraction),
RadialBlur(RadialBlur),
Light(LightDraw),
Flare(Flare),
Field(ForceField),
Ripples(Ripples),
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Emitter {
pub name: Option<String>,
pub parent: Option<u32>,
pub placement: Transform,
pub jitter_translation: [f32; 3],
pub jitter_rotation: [f32; 3],
pub attachment: Attachment,
pub delay: f32,
pub duration: Option<f32>,
pub motion: Option<NodeMotion>,
pub spawn: Spawn,
pub shape: Shape,
pub heading: Heading,
pub spread: Spread,
pub law: Law,
pub draw: Option<Draw>,
pub spawns: Vec<Entity>,
pub states: Vec<u32>,
pub order: f32,
pub lod_range: Option<[f32; 2]>,
pub copies: Option<Pick>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum Pick {
All,
One,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct NodeMotion {
pub forward: [f32; 3],
pub speed: Value,
pub speed_multiplier: Value,
pub acceleration: Value,
pub acceleration_multiplier: Value,
pub lift: Value,
pub lift_in_world: bool,
pub displacement: Option<Value>,
pub spin: [Value; 3],
pub spin_multiplier: [Value; 3],
pub align: bool,
pub turns: Option<Turns>,
pub drag: Option<Drag>,
pub min_speed: Option<f32>,
pub max_speed: Option<f32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum Termination {
Natural,
Fade(f32),
Instant,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ParticleSystem {
pub seed: u64,
pub emitters: Vec<Emitter>,
pub inputs: Vec<Input>,
pub states: u32,
pub warmup: f32,
pub time_scale: Option<Value>,
pub max_view_distance: Option<f32>,
pub termination: Termination,
pub end: Option<f32>,
pub lost: Vec<(String, u32)>,
}
impl Component for ParticleSystem {
const NAME: &'static str = "core.particles";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn to_json(&self) -> Result<Option<Json>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn references(&self, out: &mut Vec<Entity>) {
for e in &self.emitters {
out.extend(e.spawns.iter().copied());
if let Some(d) = &e.draw {
d.references(out);
}
}
}
fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
for (i, e) in self.emitters.iter().enumerate() {
let at = |what: &str| format!("emitter {i}: {what}");
if let Some(p) = e.parent
&& usize::try_from(p).map_or(true, |p| p >= i)
{
return Err(at(&format!("parent {p} is not an earlier emitter")));
}
let finite = e
.placement
.cols
.iter()
.flatten()
.chain(&e.jitter_translation)
.chain(&e.jitter_rotation);
if !finite.chain([&e.delay, &e.order]).all(|v| v.is_finite()) {
return Err(at("a placement that is not finite"));
}
if e.states.iter().any(|&s| s >= self.states.max(1)) {
return Err(at("a state the system does not have"));
}
e.check(self.inputs.len()).map_err(|m| at(&m))?;
}
if self.end.is_some_and(|t| !t.is_finite() || t < 0.0) {
return Err("an end that is not a finite time".into());
}
if !(self.warmup.is_finite() && self.warmup >= 0.0) {
return Err(format!("a warm-up of {} seconds", self.warmup));
}
if self
.max_view_distance
.is_some_and(|d| !(d.is_finite() && d > 0.0))
{
return Err("a view distance that is not a positive number".into());
}
if let Some(v) = &self.time_scale {
v.check("time scale", self.inputs.len())?;
}
if let Termination::Fade(t) = self.termination
&& !(t.is_finite() && t >= 0.0)
{
return Err(format!("a fade of {t} seconds"));
}
Ok(())
}
}
impl Emitter {
fn check(&self, inputs: usize) -> Result<(), String> {
let mut values: Vec<(&str, &Value)> = vec![
("initial speed", &self.law.initial_speed),
("tint", &self.law.tint),
("size scale x", &self.law.size_scale[0]),
("size scale y", &self.law.size_scale[1]),
("size scale z", &self.law.size_scale[2]),
];
if let Some(v) = &self.law.lifetime {
values.push(("lifetime", v));
}
match &self.spawn {
Spawn::Once => {}
Spawn::Periodic { interval, count, .. } => {
values.extend([("interval", interval), ("count", count)])
}
Spawn::Distance { step, count, .. } => values.extend([("step", step), ("count", count)]),
}
match &self.shape {
Shape::Point => {}
Shape::Disk {
radius,
concentration,
} => values.extend([("radius", radius), ("concentration", concentration)]),
Shape::Rectangle { size, concentration } => {
values.extend(size.iter().map(|v| ("size", v)));
values.push(("concentration", concentration));
}
Shape::Sphere { radius, .. } => values.push(("radius", radius)),
Shape::Box { size, .. } => values.extend(size.iter().map(|v| ("size", v))),
Shape::Cylinder { radius, height, .. } => values.extend([("radius", radius), ("height", height)]),
}
match &self.spread {
Spread::None => {}
Spread::Cone { angle, concentration } => {
values.extend([("angle", angle), ("concentration", concentration)])
}
Spread::Ellipse { x, y, concentration } | Spread::Rectangle { x, y, concentration } => {
values.extend([("angle", x), ("angle", y), ("concentration", concentration)]);
}
}
if let Some(m) = &self.law.movement {
match &m.drive {
Drive::None => {}
Drive::Speed { speed, multiplier } => {
values.extend([("speed", speed), ("speed multiplier", multiplier)])
}
Drive::Acceleration {
acceleration,
multiplier,
} => {
values.extend([
("acceleration", acceleration),
("acceleration multiplier", multiplier),
]);
}
}
values.push(("gravity", &m.gravity));
if let Some(t) = &m.turns {
values.push(("turn angle", &t.max_angle));
}
if let Some(d) = &m.drag {
values.push(("drag", &d.factor));
}
if let Some(f) = &m.fields {
values.extend([
("field speed", &f.speed),
("field speed multiplier", &f.speed_multiplier),
("field acceleration", &f.acceleration),
("field acceleration multiplier", &f.acceleration_multiplier),
]);
}
}
if let Some(n) = &self.motion {
for (what, limit) in [("node min speed", n.min_speed), ("node max speed", n.max_speed)] {
if limit.is_some_and(|x| !x.is_finite()) {
return Err(format!("{what} {limit:?} is not finite"));
}
}
if let (Some(min), Some(max)) = (n.min_speed, n.max_speed)
&& min > max
{
return Err(format!("node min speed {min} is above its max speed {max}"));
}
values.extend([
("node speed", &n.speed),
("node speed multiplier", &n.speed_multiplier),
("node acceleration", &n.acceleration),
("node acceleration multiplier", &n.acceleration_multiplier),
("node lift", &n.lift),
]);
values.extend(n.displacement.iter().map(|v| ("node displacement", v)));
values.extend(n.spin.iter().chain(&n.spin_multiplier).map(|v| ("node spin", v)));
if let Some(t) = &n.turns {
values.push(("node turn angle", &t.max_angle));
}
if let Some(d) = &n.drag {
values.push(("node drag", &d.factor));
}
}
for (what, v) in values {
v.check(what, inputs)?;
}
if let Some(d) = &self.draw {
d.check(inputs)?;
}
Ok(())
}
}
fn push_map(out: &mut Vec<Entity>, m: &Option<Map>) {
out.extend(m.iter().map(|m| m.texture));
}
impl Draw {
fn references(&self, out: &mut Vec<Entity>) {
match self {
Draw::Quad(q) => {
push_map(out, &q.map);
if let Some(l) = &q.lit {
push_map(out, &l.normal_map);
push_map(out, &l.specular_map);
}
}
Draw::LayeredQuad(q) => {
push_map(out, &q.quad.map);
if let Some(l) = &q.quad.lit {
push_map(out, &l.normal_map);
push_map(out, &l.specular_map);
}
push_map(out, &q.base.map);
for l in &q.layers {
push_map(out, &l.layer.map);
}
}
Draw::Point(p) => push_map(out, &p.map),
Draw::Mesh(m) => {
out.push(m.mesh);
out.extend(m.clip.iter().map(|c| c.clip));
}
Draw::Trail(t) => {
push_map(out, &t.map);
if let Some(d) = &t.distortion {
push_map(out, &d.normal_map);
}
if let Some(l) = &t.lit {
push_map(out, &l.normal_map);
push_map(out, &l.specular_map);
}
}
Draw::Streak(s) => push_map(out, &s.map),
Draw::Refraction(r) => {
push_map(out, &r.normal_map);
push_map(out, &r.mask);
}
Draw::RadialBlur(b) => push_map(out, &b.mask),
Draw::Flare(f) => {
for l in &f.layers {
push_map(out, &l.map);
}
}
Draw::Ripples(r) => push_map(out, &r.map),
Draw::Light(_) | Draw::Field(_) => {}
}
}
fn values<'a>(&'a self, out: &mut Vec<(&'static str, &'a Value)>) {
fn terms<'a>(out: &mut Vec<(&'static str, &'a Value)>, t: &'a [Term]) {
out.extend(t.iter().map(|t| ("tint", &t.value)));
}
fn atlas<'a>(out: &mut Vec<(&'static str, &'a Value)>, a: &'a Option<Atlas>) {
out.extend(a.iter().map(|a| ("atlas cell", &a.cell)));
}
fn quad_values<'a>(out: &mut Vec<(&'static str, &'a Value)>, q: &'a Quad) {
out.extend(q.size.iter().map(|v| ("size", v)));
out.extend(q.offset.iter().map(|v| ("offset", v)));
out.extend(q.rotation.iter().map(|v| ("rotation", v)));
out.extend(q.spin.iter().map(|v| ("spin", v)));
out.extend([
("alpha fade threshold", &q.alpha_fade_threshold),
("alpha cutoff", &q.alpha_cutoff),
]);
out.push(("depth bias", &q.depth_bias));
terms(out, &q.tint);
atlas(out, &q.atlas);
}
match self {
Draw::Quad(q) => quad_values(out, q),
Draw::LayeredQuad(q) => {
quad_values(out, &q.quad);
for l in std::iter::once(&q.base).chain(q.layers.iter().map(|l| &l.layer)) {
out.push(("layer tint", &l.tint));
out.extend(l.scroll.iter().map(|v| ("layer scroll", v)));
out.extend(l.offset.iter().map(|v| ("layer offset", v)));
out.extend(l.scale.iter().map(|v| ("layer scale", v)));
}
}
Draw::Point(p) => {
out.extend([("size", &p.size), ("alpha cutoff", &p.alpha_cutoff)]);
terms(out, &p.tint);
}
Draw::Mesh(m) => {
out.extend(m.size.iter().map(|v| ("size", v)));
out.extend(m.rotation.iter().map(|v| ("rotation", v)));
out.extend(m.spin.iter().map(|v| ("spin", v)));
out.extend(m.scroll.iter().map(|v| ("scroll", v)));
out.extend(m.offset.iter().map(|v| ("offset", v)));
terms(out, &m.tint);
atlas(out, &m.atlas);
}
Draw::Trail(t) => {
out.extend([
("width", &t.width),
("width scale", &t.width_scale),
("texture fraction", &t.texture_fraction),
("scroll", &t.scroll_u),
("variance", &t.variance_v),
("fade start", &t.fade_start),
("fade end", &t.fade_end),
("alpha fade threshold", &t.alpha_fade_threshold),
("alpha cutoff", &t.alpha_cutoff),
]);
out.extend(t.distortion.iter().map(|d| ("distortion", &d.intensity)));
terms(out, &t.tint);
atlas(out, &t.atlas);
}
Draw::Streak(s) => {
out.extend([
("length", &s.length),
("start colour", &s.start_colour),
("end colour", &s.end_colour),
("alpha cutoff", &s.alpha_cutoff),
]);
out.extend(s.width.iter().map(|w| ("width", w)));
terms(out, &s.tint);
}
Draw::Refraction(r) => {
out.extend(r.offset.iter().map(|v| ("offset", v)));
out.extend(r.size.iter().map(|v| ("size", v)));
out.extend([
("intensity", &r.intensity),
("stir speed", &r.stir_speed),
("radius", &r.radius),
("alpha cutoff", &r.alpha_cutoff),
]);
out.extend(
r.normal_offset
.iter()
.chain(&r.normal_scroll)
.chain(&r.normal_scale)
.map(|v| ("normal map", v)),
);
terms(out, &r.tint);
}
Draw::RadialBlur(b) => {
out.extend(b.offset.iter().map(|v| ("offset", v)));
out.extend(b.size.iter().map(|v| ("size", v)));
out.extend([("radius", &b.radius), ("alpha cutoff", &b.alpha_cutoff)]);
terms(out, &b.tint);
}
Draw::Light(l) => out.extend([
("colour", &l.colour),
("intensity", &l.intensity),
("range", &l.range),
]),
Draw::Flare(f) => {
for l in &f.layers {
out.extend([
("flare width", &l.width),
("flare height", &l.height),
("flare colour", &l.colour),
]);
}
}
Draw::Field(f) => out.push(("force", &f.strength)),
Draw::Ripples(_) => {}
}
}
fn check(&self, inputs: usize) -> Result<(), String> {
let mut values = Vec::new();
self.values(&mut values);
for (what, v) in values {
v.check(what, inputs)?;
}
let atlas = match self {
Draw::Quad(q) => q.atlas.as_ref(),
Draw::LayeredQuad(q) => q.quad.atlas.as_ref(),
Draw::Mesh(m) => m.atlas.as_ref(),
Draw::Trail(t) => t.atlas.as_ref(),
_ => None,
};
if atlas.is_some_and(|a| a.columns == 0 || a.cells == 0) {
return Err("an atlas of no cells".into());
}
if let Draw::Quad(q) = self
&& let Orientation::World(n) | Orientation::Frame(n) = q.orientation
&& !n.iter().all(|v| v.is_finite())
{
return Err("an orientation that is not finite".into());
}
if let Draw::Mesh(m) = self
&& let MeshOrientation::Heading { forward } = m.orientation
&& !(forward.iter().all(|v| v.is_finite())
&& (forward.iter().map(|v| v * v).sum::<f32>().sqrt() - 1.0).abs() <= 1e-4)
{
return Err("a mesh particle's forward direction that is not a unit vector".into());
}
if let Draw::Trail(t) = self
&& !(t.segment_interval.is_finite() && t.segment_life.is_finite() && t.fade_out.is_finite())
{
return Err("a trail time that is not finite".into());
}
if let Draw::LayeredQuad(q) = self
&& q.quad.map.is_some()
{
return Err("a layered quad with a texture of its own (its layers are its texture)".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(at: f32, v: f32) -> Key {
Key {
at,
value: [v, 0.0, 0.0, 0.0],
handle_in: [0.0; 4],
handle_out: [0.0; 4],
}
}
fn curve(interpolation: Interpolation, keys: Vec<Key>) -> Curve {
Curve {
width: 1,
interpolation,
looped: false,
keys,
}
}
#[test]
fn curves_hold_run_and_loop() {
let lin = curve(Interpolation::Linear, vec![key(0.0, 0.0), key(2.0, 10.0)]);
assert_eq!(lin.value_at(-5.0)[0], 0.0);
assert_eq!(lin.value_at(1.0)[0], 5.0);
assert_eq!(lin.value_at(9.0)[0], 10.0);
let step = curve(
Interpolation::Step,
vec![key(0.0, 1.0), key(2.0, 2.0), key(4.0, 3.0)],
);
assert_eq!(step.value_at(1.9)[0], 1.0);
assert_eq!(step.value_at(2.0)[0], 2.0);
let mut looped = lin.clone();
looped.looped = true;
assert_eq!(looped.value_at(3.0)[0], 5.0);
let mut still = curve(Interpolation::Linear, vec![key(0.0, 7.0)]);
still.looped = true;
assert_eq!(still.value_at(100.0)[0], 7.0);
}
#[test]
fn a_bezier_segment_is_the_cubic_through_its_handles() {
let mut a = key(0.0, 0.0);
a.handle_out = [1.0, 0.0, 0.0, 0.0];
let mut b = key(1.0, 1.0);
b.handle_in = [1.0, 0.0, 0.0, 0.0];
let c = curve(Interpolation::Bezier, vec![a, b]);
let want = |u: f32| {
let k = 1.0 - u;
3.0 * k * k * u + u * u * u
};
for u in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
assert!((c.value_at(u)[0] - want(u)).abs() < 1e-6, "{u}");
}
}
#[test]
fn draws_are_deterministic_uniform_and_keyed() {
let a = unit(7, 1, 2, 3, 0);
assert_eq!(a, unit(7, 1, 2, 3, 0));
assert!((0.0..1.0).contains(&a));
for other in [
unit(8, 1, 2, 3, 0),
unit(7, 2, 2, 3, 0),
unit(7, 1, 3, 3, 0),
unit(7, 1, 2, 4, 0),
unit(7, 1, 2, 3, 1),
] {
assert_ne!(a, other);
}
let n = 20_000;
let mean = (0..n).map(|i| f64::from(unit(1, 0, i, 0, 0))).sum::<f64>() / n as f64;
assert!((mean - 0.5).abs() < 0.01, "{mean}");
}
#[test]
fn variations_apply_in_order_within_their_bounds() {
let mut v = Value::constant(&[10.0]);
v.variation.push(Variation::Delta {
max: [2.0; 4],
stream: 0,
});
v.variation.push(Variation::Fraction {
max: [0.1; 4],
stream: 1,
});
let none = |_: u32| 0.0;
for scope in 0..200 {
let x = v.eval(0.0, 5, 0, scope, &none)[0];
assert!((8.0 * 0.9..=12.0 * 1.1).contains(&x), "{x}");
}
assert_eq!(v.eval(0.0, 5, 0, 9, &none), v.eval(3.0, 5, 0, 9, &none));
let mut scaled = Value::constant(&[4.0]);
scaled.variation.push(Variation::Scaled {
input: 0,
factor: curve(Interpolation::Linear, vec![key(0.0, 0.0), key(1.0, 1.0)]),
});
assert_eq!(scaled.eval(0.0, 1, 0, 0, &|_| 0.25)[0], 1.0);
}
#[test]
fn a_constant_value_is_fixed() {
let v = Value::constant(&[1.0, 2.0]);
assert!(v.is_fixed());
assert_eq!(v.curve.value_at(9.0)[..2], [1.0, 2.0]);
let mut w = v.clone();
w.variation.push(Variation::Delta {
max: [0.1; 4],
stream: 3,
});
assert!(!w.is_fixed());
}
#[test]
fn a_mesh_particle_turned_along_its_travel_names_a_unit_forward() {
let zero = || Value::constant(&[0.0]);
let mesh = |forward: [f32; 3]| {
Draw::Mesh(MeshParticle {
mesh: Entity::from_index(1),
clip: None,
blend: Blend::Alpha,
orientation: MeshOrientation::Heading { forward },
size: [zero(), zero(), zero()],
size_variance: [1.0; 3],
uniform: true,
rotation: [zero(), zero(), zero()],
spin: [zero(), zero(), zero()],
tint: Vec::new(),
scroll: [zero(), zero()],
offset: [zero(), zero()],
atlas: None,
soft: None,
distance: None,
lit: false,
})
};
assert!(mesh([0.0, 1.0, 0.0]).check(0).is_ok());
assert!(mesh([0.0, 0.0, -1.0]).check(0).is_ok());
for bad in [[0.0; 3], [0.0, 2.0, 0.0], [f32::NAN, 0.0, 0.0]] {
assert!(mesh(bad).check(0).is_err(), "{bad:?}");
}
}
#[test]
fn laid_layers_change_their_target_by_their_operation_at_their_opacity() {
use LayerOperation::{Add, Multiply, Overlay};
use LayerTarget::{Colour, Opacity};
let base = [0.8, 0.4, 0.2, 0.5];
let c = compose_layers(false, base, &[(Colour, Multiply, [0.5, 0.5, 0.5, 0.5])]);
for (got, want) in c.iter().zip([0.6, 0.3, 0.15, 0.5]) {
assert!((got - want).abs() < 1e-6, "{c:?}");
}
let c = compose_layers(false, base, &[(Colour, Add, [0.1, 0.2, 0.3, 1.0])]);
assert!(
(c[0] - 0.9).abs() < 1e-6
&& (c[1] - 0.6).abs() < 1e-6
&& (c[2] - 0.5).abs() < 1e-6
&& c[3] == 0.5
);
let c = compose_layers(false, base, &[(Colour, Overlay, [0.5, 0.5, 0.5, 1.0])]);
assert!((c[0] - 0.8).abs() < 1e-6 && (c[1] - 0.4).abs() < 1e-6);
let c = compose_layers(false, base, &[(Opacity, Multiply, [0.2, 0.4, 0.6, 1.0])]);
assert_eq!(c[..3], base[..3]);
assert!((c[3] - 0.2).abs() < 1e-6);
let c = compose_layers(true, base, &[(Opacity, Add, [1.0, 1.0, 1.0, 0.5])]);
assert!((c[0] - 0.4).abs() < 1e-6 && (c[3] - 0.75).abs() < 1e-6);
let c = compose_layers(
false,
base,
&[
(Colour, Add, [0.2, 0.2, 0.2, 1.0]),
(Colour, Multiply, [0.5, 0.5, 0.5, 1.0]),
],
);
assert!((c[0] - 0.5).abs() < 1e-6);
}
}