SMRTicles¶
Smart Particles¶
Stateful Massively-parallel Runtime for particles
or:
Simulations with Multiple Render Targets
or:
Whatever you’d like
Noisemaker’s shader pipeline includes common architecture for GPU-accelerated agent-based particle simulations. It provides a unified framework for initializing, simulating, and rendering perhaps millions of particles with shared state management and composable behaviors.
Philosophy¶
Traditional particle systems duplicate substantial boilerplate code across effects:
Agent initialization and respawn logic
Color sampling from input textures
Trail accumulation and decay
Point-sprite rendering and blending
SMRTicles factors this common infrastructure into two wrapper effects (pointsEmit and pointsRender) that sandwich effect-specific behavior middleware. This architecture achieves:
Code Reduction: ~40% less shader code per effect
Consistency: All effects share the same deposit/blend approach
Composability: Mix and match behaviors in a single pipeline
Maintainability: Bug fixes propagate to all particle effects
Architecture Overview¶
Pipeline Structure¶
Every SMRTicles composition follows this three-stage pipeline:
(input texture)
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ pointsEmit (render) │
│ • Initialize agents with positions, velocities, colors, count │
│ • Handle respawn when agents die or attrition triggers │
│ • Sample colors from input texture │
│ • Output: global_xyz, global_vel, global_rgba │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Behavior Middleware (points) │
│ • Read xyz/vel/rgba state from pipeline │
│ • Apply effect-specific movement/physics/sensing │
│ • Write updated state back (ping-ponged by runtime) │
│ • Examples: attractor, flock, flow, physarum, life, physical │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ pointsRender (render) │
│ • Diffuse: decay existing trail by intensity factor │
│ • Deposit: scatter agent colors via point-sprite rendering │
│ • Blend: composite trail with input by inputIntensity factor │
│ • Output: final image to outputTex │
└─────────────────────────────────────────────────────────────────────┘
DSL Usage¶
SMRTicles compositions use method chaining in the Polymorphic DSL:
// Basic particle system with physics
noise().pointsEmit().physical().pointsRender().write(o0)
// Slime mold simulation
noise().pointsEmit(stateSize: 512).physarum().pointsRender().write(o0)
// Strange attractor with 3D viewport
noise().pointsEmit().attractor().pointsRender(viewMode: ortho).write(o0)
// Flow field tracing over an input image
noise().pointsEmit().flow().pointsRender().write(o0)
State Textures¶
SMRTicles uses three shared global textures for agent state, created by pointsEmit and consumed by all downstream effects:
Texture |
Format |
Contents |
|---|---|---|
|
rgba32f |
[x, y, z, alive_flag] — Position in normalized [0,1] space, w=1 alive |
|
rgba32f |
[vx, vy, vz, seed] — Velocity vector and per-agent random seed |
|
rgba8 |
[r, g, b, a] — Agent color (sampled from input or mono white) |
State Texture Sizing¶
Agent count is controlled by the stateSize parameter, which sets the dimensions of the state textures. Total agents = stateSize × stateSize.
stateSize |
Agent Count |
Use Case |
|---|---|---|
64 |
4,096 |
Debugging, low-end devices |
128 |
16,384 |
Light simulations |
256 |
65,536 |
Default, good balance |
512 |
262,144 |
High density effects |
1024 |
1,048,576 |
Physarum, massive swarms |
2048 |
4,194,304 |
Maximum (GPU memory dependent) |
Alive Flag Protocol¶
The xyz.w component encodes agent lifecycle state:
Value |
Meaning |
|---|---|
|
Alive and active |
|
Dead — respawn at random position next frame |
|
Dead — respawn at current xy (in-place respawn) |
Effects signal agent death by writing xyz.w = 0.0 (or -1.0 for in-place). The pointsEmit wrapper detects this on the next frame and handles respawn.
Wrapper Effects¶
pointsEmit¶
Namespace: render
Purpose: Initialize and maintain agent state. Runs every frame to handle respawns.
Key Parameters:
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
int |
256 |
State texture dimensions (64–2048) |
|
enum |
random |
Initial distribution: random, grid, center, ring, clusters, spiral |
|
float |
0.0 |
Random seed for reproducibility |
|
float |
0.0 |
Per-frame respawn chance (0–10%) |
|
button |
— |
Force all agents to respawn |
Spawn Patterns:
random: Uniform random distribution across canvas
grid: Regular grid layout
center: Concentrated in center region
ring: Circular ring distribution
clusters: N random cluster centers with Gaussian spread
spiral: Archimedean spiral from center
pointsRender¶
Namespace: render
Purpose: Accumulate agent trails and composite with input.
Key Parameters:
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
float |
50.0 |
Percentage of agents to render (0–100) |
|
float |
75.0 |
Trail persistence (0=instant fade, 100=no decay) |
|
float |
10.15 |
Input blend factor (0=trail only, 100=input visible) |
|
enum |
flat |
Viewport mode: flat (2D) or ortho (3D) |
|
float |
varies |
3D rotation in radians (when viewMode=ortho) |
|
float |
0.8 |
Zoom factor for 3D view |
|
float |
0.0 |
Position offset for 3D view |
Internal Passes:
Diffuse: Decay existing trail by intensity factor
Copy: Prepare write buffer for hardware blending
Deposit: Point-sprite rendering of agents to trail (additive blend)
Blend: Composite trail over input texture
Note
Additional sprite rendering modes (textured sprites, billboards, custom shapes) are planned for future releases.
Behavior Middleware¶
All behavior effects live in the points namespace and follow a consistent pattern:
Read
global_xyz,global_vel,global_rgbafrom pipelineApply effect-specific logic
Write updated state back (MRT output to same textures)
Passthrough
inputTextooutputTexfor 2D chain continuity
Available Behaviors¶
Effect |
Description |
|---|---|
|
Physics simulation with gravity, wind, drag, and wander forces |
|
Luminance-based flow field tracing with configurable behaviors |
|
Gradient descent flow (hydraulic erosion pattern) |
|
Boids flocking: separation, alignment, cohesion |
|
Slime mold chemotaxis with sensor-based steering |
|
Diffusion-limited aggregation (crystal growth) |
|
Particle life: type-based attraction/repulsion matrix |
|
Particle Lenia: continuous cellular automata with kernel-based attraction |
|
Strange attractors: Lorenz, Rössler, Aizawa, Thomas, etc. |
Implementation Notes¶
MRT (Multiple Render Targets)¶
Behavior effects use MRT to update all three state textures in a single pass:
// GLSL fragment shader
layout(location = 0) out vec4 outXYZ;
layout(location = 1) out vec4 outVel;
layout(location = 2) out vec4 outRGBA;
void main() {
// Read previous state
vec4 xyz = texelFetch(xyzTex, ivec2(gl_FragCoord.xy), 0);
vec4 vel = texelFetch(velTex, ivec2(gl_FragCoord.xy), 0);
vec4 rgba = texelFetch(rgbaTex, ivec2(gl_FragCoord.xy), 0);
// Apply behavior logic...
// Write updated state
outXYZ = xyz;
outVel = vel;
outRGBA = rgba;
}
Point-Sprite Deposit¶
The deposit pass uses drawMode: "points" to scatter agents:
// Vertex shader
void main() {
// Map gl_VertexID to state texture coordinate
int texSize = int(sqrt(float(vertexCount)));
ivec2 coord = ivec2(gl_VertexID % texSize, gl_VertexID / texSize);
// Read agent position from state texture
vec4 xyz = texelFetch(xyzTex, coord, 0);
// Cull dead agents
if (xyz.w < 0.5) {
gl_Position = vec4(-10.0); // Off-screen
return;
}
// Transform to clip space
gl_Position = vec4(xyz.xy * 2.0 - 1.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
Ping-Pong State¶
The runtime automatically ping-pongs state textures between frames. Effects read from the previous frame’s state and write to the current frame’s buffer. The updateFrameSurfaceBindings() method ensures within-frame visibility when multiple effects share textures.
Creating Custom Behaviors¶
To create a new SMRTicles behavior:
Create effect directory:
shaders/effects/points/myeffect/Define the effect (
definition.js):
import { Effect } from '../../../src/runtime/effect.js'
export default new Effect({
name: "MyEffect",
namespace: "points",
func: "myEffect",
tags: ["sim", "agents"],
description: "Custom particle behavior",
textures: {}, // Use shared global textures
outputXyz: "global_xyz",
outputVel: "global_vel",
outputRgba: "global_rgba",
globals: {
myParam: {
type: "float",
default: 1.0,
uniform: "myParam",
ui: { label: "my param", control: "slider" }
}
},
passes: [
{
name: "agent",
program: "agent",
drawBuffers: 3,
inputs: {
xyzTex: "global_xyz",
velTex: "global_vel",
rgbaTex: "global_rgba"
},
uniforms: { myParam: "myParam" },
outputs: {
outXYZ: "global_xyz",
outVel: "global_vel",
outRGBA: "global_rgba"
}
},
{
name: "passthrough",
program: "passthrough",
inputs: { inputTex: "inputTex" },
outputs: { fragColor: "outputTex" }
}
]
})
Implement shaders: Create
glsl/agent.glslandwgsl/agent.wgslwith your behavior logic.Test: Use the MCP tools to verify compilation and rendering:
# Verify shader compiles
mcp compileEffect --effect_id points/myeffect --backend webgl2
# Check for non-monochrome output
mcp renderEffectFrame --effect_id points/myeffect --backend webgl2
Performance Considerations¶
State size scaling: Memory and bandwidth scale quadratically with
stateSize. Use the smallest size that achieves your visual goal.Density culling: The
densityparameter inpointsRenderculls agents before vertex processing, saving GPU work.Trail vs. chemistry: Effects like
physarummaintain separate pheromone textures for simulation accuracy, independent of visual trail intensity.3D projection: The ortho view mode in
pointsRenderadds rotation matrix computation per vertex. Use flat mode for 2D-only effects.Neighbor queries: Effects like
flockandlifehave O(n²) neighbor lookups. Use spatial hashing (built into the shaders) or limitstateSizefor real-time performance.