Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Particle struct type

They do not have direct access to:

  • The uniforms struct (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.