Custom Functions
Custom functions let you define reusable WGSL code that can be called from your rules. This keeps complex logic organized and avoids code duplication.
Defining Functions
Use .with_function() to add WGSL functions:
#![allow(unused)] fn main() { Simulation::<Particle>::new() .with_function(r#" fn swirl(pos: vec3<f32>, strength: f32) -> vec3<f32> { let d = length(pos.xz); return vec3(-pos.z, 0.0, pos.x) * strength / (d + 0.1); } "#) .with_rule(Rule::Custom(r#" p.velocity += swirl(p.position, 2.0) * uniforms.delta_time; "#.into())) .run(); }
Function Scope
Your custom functions have access to:
- All WGSL built-in functions (
sin,cos,length,normalize, etc.) - Built-in utility functions (see Shader Utilities)
- Other custom functions defined before this one
- The
Particlestruct type
They do not have direct access to:
- The
uniformsstruct (pass values as parameters instead) - The particle arrays (operate on values passed to the function)
Multiple Functions
Add multiple functions for complex effects:
#![allow(unused)] fn main() { .with_function(r#" fn wave_height(x: f32, z: f32, t: f32) -> f32 { return sin(x * 3.0 + t) * cos(z * 2.0 + t * 0.7) * 0.2; } "#) .with_function(r#" fn wave_force(pos: vec3<f32>, t: f32) -> vec3<f32> { let h = wave_height(pos.x, pos.z, t); let target_y = h; return vec3(0.0, (target_y - pos.y) * 2.0, 0.0); } "#) .with_rule(Rule::Custom(r#" p.velocity += wave_force(p.position, uniforms.time) * uniforms.delta_time; "#.into())) }
Example: Orbital Mechanics
#![allow(unused)] fn main() { .with_function(r#" fn gravity_force(pos: vec3<f32>, center: vec3<f32>, mass: f32) -> vec3<f32> { let diff = center - pos; let dist_sq = dot(diff, diff); if dist_sq < 0.01 { return vec3(0.0); } let dist = sqrt(dist_sq); return normalize(diff) * mass / dist_sq; } "#) .with_function(r#" fn orbital_velocity(pos: vec3<f32>, center: vec3<f32>, mass: f32) -> vec3<f32> { let r = length(pos - center); let speed = sqrt(mass / r); let radial = normalize(pos - center); // Perpendicular to radial, in XZ plane return vec3(-radial.z, 0.0, radial.x) * speed; } "#) }
Example: Turbulence
Combine custom functions with built-in noise:
#![allow(unused)] fn main() { .with_function(r#" fn turbulence(pos: vec3<f32>, time: f32, strength: f32) -> vec3<f32> { let scale = 2.0; let t = time * 0.5; return vec3( noise3(pos * scale + vec3(t, 0.0, 0.0)), noise3(pos * scale + vec3(0.0, t, 100.0)), noise3(pos * scale + vec3(100.0, 0.0, t)) ) * strength; } "#) .with_rule(Rule::Custom(r#" p.velocity += turbulence(p.position, uniforms.time, 1.5) * uniforms.delta_time; "#.into())) }
Tips
- Parameter passing: Pass uniforms as function parameters rather than accessing them directly.
- Return types: Always specify return types for WGSL functions.
- Order matters: Functions can only call functions defined before them.
- Keep it simple: Complex logic is fine, but avoid excessive branching for GPU performance.