Skip to main content

kvas_engine/
modifiers.rs

1//! Built-in behaviours you attach in the editor with no code. They run every frame in Play mode.
2//! (Spin, Bob, Pulse, Wobble, ColorCycle.) Scripts can add, remove and tweak them like any component.
3
4use crate::components::{Component, MeshRenderer, Transform};
5use crate::noise::{Noise, NoiseKind};
6use crate::reflect::Field;
7use glam::Vec3;
8use kvas_ecs::World;
9use serde::{Deserialize, Serialize};
10use std::f32::consts::TAU;
11
12/// Rotates continuously (degrees per second per axis).
13#[derive(Clone, Serialize, Deserialize)]
14#[serde(default)]
15pub struct Spin {
16    /// Euler rotation velocity in degrees per second for each local axis.
17    pub speed: Vec3,
18}
19
20impl Default for Spin {
21    fn default() -> Self {
22        Self { speed: Vec3::new(0.0, 45.0, 0.0) }
23    }
24}
25
26impl Component for Spin {
27    fn fields(&mut self) -> Vec<Field<'_>> {
28        vec![Field::vec3("speed", &mut self.speed)]
29    }
30}
31
32/// Floats back and forth along an axis.
33#[derive(Clone, Serialize, Deserialize)]
34#[serde(default)]
35pub struct Bob {
36    /// Parent-space oscillation direction; normalized by the update system.
37    pub axis: Vec3,
38    /// Maximum displacement from the initial position, in metres.
39    pub amplitude: f32,
40    /// Cycles per second.
41    pub speed: f32,
42    /// Initial oscillation phase in cycles.
43    pub phase: f32,
44    #[serde(skip)]
45    #[doc(hidden)]
46    pub origin: Option<Vec3>,
47}
48
49impl Default for Bob {
50    fn default() -> Self {
51        Self { axis: Vec3::Y, amplitude: 0.5, speed: 0.5, phase: 0.0, origin: None }
52    }
53}
54
55impl Component for Bob {
56    fn fields(&mut self) -> Vec<Field<'_>> {
57        vec![
58            Field::vec3("axis", &mut self.axis),
59            Field::f32("amplitude", &mut self.amplitude),
60            Field::f32("speed", &mut self.speed),
61            Field::f32("phase", &mut self.phase),
62        ]
63    }
64}
65
66/// Breathes: scale oscillates around its starting value.
67#[derive(Clone, Serialize, Deserialize)]
68#[serde(default)]
69pub struct Pulse {
70    /// 0.2 = +-20 % size.
71    pub amount: f32,
72    /// Scale oscillation frequency in cycles per second.
73    pub speed: f32,
74    #[serde(skip)]
75    #[doc(hidden)]
76    pub origin: Option<Vec3>,
77}
78
79impl Default for Pulse {
80    fn default() -> Self {
81        Self { amount: 0.15, speed: 1.0, origin: None }
82    }
83}
84
85impl Component for Pulse {
86    fn fields(&mut self) -> Vec<Field<'_>> {
87        vec![Field::f32("amount", &mut self.amount), Field::f32("speed", &mut self.speed)]
88    }
89}
90
91/// Organic drift: moves around its starting position following procedural noise.
92#[derive(Clone, Serialize, Deserialize)]
93#[serde(default)]
94pub struct Wobble {
95    /// Maximum noise displacement per parent-space axis, in metres.
96    pub amount: Vec3,
97    /// Temporal noise sampling frequency multiplier.
98    pub frequency: f32,
99    /// Noise family driving the displacement.
100    pub kind: NoiseKind,
101    /// Deterministic noise seed.
102    pub seed: i32,
103    #[serde(skip)]
104    #[doc(hidden)]
105    pub origin: Option<Vec3>,
106}
107
108impl Default for Wobble {
109    fn default() -> Self {
110        Self { amount: Vec3::splat(0.3), frequency: 0.7, kind: NoiseKind::Perlin, seed: 1, origin: None }
111    }
112}
113
114impl Component for Wobble {
115    fn fields(&mut self) -> Vec<Field<'_>> {
116        vec![
117            Field::vec3("amount", &mut self.amount),
118            Field::f32("frequency", &mut self.frequency),
119            Field::choice("noise", &mut self.kind),
120            Field::i32("seed", &mut self.seed),
121        ]
122    }
123}
124
125/// Cycles the material colour through the hue wheel.
126#[derive(Clone, Serialize, Deserialize)]
127#[serde(default)]
128pub struct ColorCycle {
129    /// Full hue cycles per second.
130    pub speed: f32,
131    /// HSV saturation used during cycling, normally in 0..1.
132    pub saturation: f32,
133    /// HSV brightness used during cycling, normally in 0..1.
134    pub value: f32,
135}
136
137impl Default for ColorCycle {
138    fn default() -> Self {
139        Self { speed: 0.15, saturation: 0.75, value: 1.0 }
140    }
141}
142
143impl Component for ColorCycle {
144    fn fields(&mut self) -> Vec<Field<'_>> {
145        vec![Field::f32("speed", &mut self.speed), Field::f32("saturation", &mut self.saturation), Field::f32("value", &mut self.value)]
146    }
147}
148
149/// Converts HSV to RGB. Hue wraps in cycles; saturation and value should be in 0..1.
150pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [f32; 3] {
151    let k = |n: f32| (n + h.rem_euclid(1.0) * 6.0).rem_euclid(6.0);
152    let f = |n: f32| v - v * s * k(n).min(4.0 - k(n)).clamp(0.0, 1.0);
153    [f(5.0), f(3.0), f(1.0)]
154}
155
156/// Advances every modifier by `dt`; `time` is seconds since the game started.
157/// Non-finite time/delta and negative deltas are ignored.
158pub fn update(world: &mut World, time: f32, dt: f32) {
159    if !time.is_finite() || !dt.is_finite() || dt < 0.0 { return; }
160    use kvas_ecs::Entity;
161    let spinning: Vec<(Entity, Vec3)> = world.iter::<Spin>().map(|(e, s)| (e, s.speed)).collect();
162    for (e, speed) in spinning {
163        if let Some(t) = world.get_mut::<Transform>(e) {
164            t.rotation += speed * dt;
165        }
166    }
167
168    for e in world.entities_with::<Bob>() {
169        let Some(pos) = world.get::<Transform>(e).map(|t| t.position) else { continue };
170        let b = world.get_mut::<Bob>(e).unwrap();
171        let origin = *b.origin.get_or_insert(pos);
172        let offset = b.axis.normalize_or_zero() * b.amplitude * ((time * b.speed + b.phase) * TAU).sin();
173        let target = origin + offset;
174        world.get_mut::<Transform>(e).unwrap().position = target;
175    }
176
177    for e in world.entities_with::<Pulse>() {
178        let Some(scale) = world.get::<Transform>(e).map(|t| t.scale) else { continue };
179        let p = world.get_mut::<Pulse>(e).unwrap();
180        let origin = *p.origin.get_or_insert(scale);
181        let k = 1.0 + p.amount * (time * p.speed * TAU).sin();
182        world.get_mut::<Transform>(e).unwrap().scale = origin * k;
183    }
184
185    for e in world.entities_with::<Wobble>() {
186        let Some(pos) = world.get::<Transform>(e).map(|t| t.position) else { continue };
187        let w = world.get_mut::<Wobble>(e).unwrap();
188        let origin = *w.origin.get_or_insert(pos);
189        let n = Noise::new(w.kind).with_seed(w.seed as u32).with_octaves(2);
190        let t = time * w.frequency;
191        let offset = Vec3::new(n.sample3(t, 0.0, 11.0), n.sample3(0.0, t, 23.0), n.sample3(37.0, 0.0, t)) * w.amount;
192        world.get_mut::<Transform>(e).unwrap().position = origin + offset;
193    }
194
195    for e in world.entities_with::<ColorCycle>() {
196        let c = world.get::<ColorCycle>(e).unwrap().clone();
197        if let Some(m) = world.get_mut::<MeshRenderer>(e) {
198            let rgb = hsv_to_rgb((time * c.speed).rem_euclid(1.0), c.saturation, c.value);
199            m.color = [rgb[0], rgb[1], rgb[2], m.color[3]];
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn hsv_primaries() {
210        let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-4);
211        assert!(near(hsv_to_rgb(0.0, 1.0, 1.0), [1.0, 0.0, 0.0]));
212        assert!(near(hsv_to_rgb(1.0 / 3.0, 1.0, 1.0), [0.0, 1.0, 0.0]));
213        assert!(near(hsv_to_rgb(2.0 / 3.0, 1.0, 1.0), [0.0, 0.0, 1.0]));
214    }
215}