Agent forces
What pushes, turns and carries each agent. The physics of an agent simulation is a list of
parts folded into one update step by an integrator. A force returns a push for one
agent; a torque returns a turn; a field samples something at the agent's position (the
magnet at the cursor, a home on a grid); a drift is a built-in velocity. Every part is built
over your layout, which names the per-agent buffers and the per-frame values it reads, so the
same part works in any layer that provides those names.
Six kinds of agent, six integrators. integrator.forces3d moves a 3D swarm held in a shape
(force.pressure, force.containment, force.cursorXY, force.gravity, force.turbulence).
integrator.steering2d moves a flock (force.flocking, force.cursorSteer, force.wallTurn).
integrator.orientation2d turns agents in place (torque.alignToField, torque.rest, over
field.dipoleOrRadial and field.jitteredGridHome). integrator.drift2d glides motes
(drift.variedHeading, force.cursorGust). integrator.relief stands particles on the
child's image (channel, force.cursorInView). integrator.advect2d carries particles along
a fluid. Pick the integrator, list the parts, and register the result as the update step in
agentSim's bake.
Drag arrives as a per-frame multiplier the frame writes each frame: compute it with
agentFrame.expDecay so it does not depend on frame rate.
import {sim} from 'shaders/std'
Reach for it when
| When you need | Use |
|---|---|
| a flock that separates, aligns and coheres | integrator.steering2d with force.flocking |
| the cursor attracts or scatters a flock | force.cursorSteer |
| a swarm that fills a shape evenly and stays inside it | integrator.forces3d with force.pressure and force.containment |
| iron filings that swing onto a magnetic field | integrator.orientation2d with torque.alignToField, torque.rest, field.dipoleOrRadial |
| motes that glide in one direction with variety | integrator.drift2d with drift.variedHeading |
| a puff from the cursor on drifting motes | force.cursorGust |
| particles that rise by the brightness of the layer inside | integrator.relief with channel |
| particles carried along a fluid's velocity | integrator.advect2d |
Example
import {defineShader, sim, transformColor} from 'shaders/std'
import {tgpu, d, agents} from '@coreroot/gpu/kit'
import {makeCpuValueGetter, readAgentFrame, resolveRenderRes, SIZE_REF_RES} from '@coreroot/gpu/porters'
const {agentSim, renderAgents, torque, field, integrator, agentFrame} = sim.agents
const MAX_FILINGS = 12288
const RES = resolveRenderRes({desktop: 1024, mobile: 768})
const OVERSCAN = 0.14
// The values the torques, the field and the home read each frame.
const SimParams = d.struct({
colA: d.vec4f, colB: d.vec4f,
count: d.f32,
dt: d.f32, aspect: d.f32, domainX: d.f32,
cursorX: d.f32, cursorY: d.f32, axisX: d.f32, axisY: d.f32,
fieldType: d.f32, restMode: d.f32,
strength: d.f32, reachSq: d.f32,
alignK: d.f32, damping: d.f32, restK: d.f32, pullK: d.f32, homeK: d.f32,
omegaRef: d.f32, agitCool: d.f32,
gridCols: d.f32, cellW: d.f32, cellH: d.f32, jitter: d.f32,
bodyR: d.f32,
})
// Per-agent state (offset from home, angle, spin), excitement, the two canvases, the output.
const simLayout = tgpu.bindGroupLayout({
agents: {storage: d.arrayOf(d.vec4f, MAX_FILINGS), access: 'mutable'},
agit: {storage: d.arrayOf(d.f32, MAX_FILINGS), access: 'mutable'},
accumE: {storage: d.arrayOf(d.atomic(d.u32), RES * RES), access: 'mutable'},
accumS: {storage: d.arrayOf(d.atomic(d.u32), RES * RES), access: 'mutable'},
params: {uniform: SimParams},
outTex: {storageTexture: d.textureStorage2d('rgba16float', 'write-only')},
})
const filingsInit = agents.makeRestingDirectorInit(simLayout, 'filingsInit')
// The physics: every filing rests on a jittered grid, samples the magnet at the cursor, and
// swings onto its field line, with a weak pull back to its rest angle where the field is faint.
const filingsHome = field.jitteredGridHome(simLayout, {overscan: OVERSCAN})
const filingsUpdate = integrator.orientation2d(simLayout, {
home: filingsHome,
fieldAt: field.dipoleOrRadial(simLayout),
torques: [torque.alignToField(simLayout), torque.rest(simLayout)],
maxOffset: 0.12,
name: 'filingsUpdate',
})
// The look: slivers drawn along each filing's own angle, colored calm to excited.
const filingsRender = renderAgents.orientedWorld(simLayout, {
shape: 'streak', res: RES, splatRCap: 14, heading: 'angle', agitation: 'buffer', comet: false, home: filingsHome,
ramp: {colorSpace: 2, trails: 'none'},
names: {splat: 'filingsSplat', resolve: 'filingsResolve'},
})
// MagneticFilings: iron filings that reveal the field lines of a magnet at the cursor.
export const MagneticFilings = defineShader({
name: 'MagneticFilings',
usesPointer: true,
props: {
colorA: {default: '#85929e', transform: transformColor},
colorB: {default: '#ff9d5c', transform: transformColor},
count: {default: 5000},
strength: {default: 1},
reach: {default: 0.35},
response: {default: 0.5},
size: {default: 1},
},
...agentSim<typeof SimParams>({
layout: simLayout,
params: SimParams,
maxAgents: MAX_FILINGS,
output: {key: 'outTex', name: 'filingsTexture', size: [RES, RES], format: 'rgba16float'},
bake: () => ({
pipelines: {
init: {kernel: filingsInit, threads: 'max'},
update: {kernel: filingsUpdate, threads: 'agents'},
splat: filingsRender.splat,
resolve: filingsRender.resolve,
},
initStep: 'init',
program: ['update', 'splat', 'resolve'],
}),
frame: (sys, {getCpuValue}) => {
const g = makeCpuValueGetter(getCpuValue)
// The magnet's axis follows where the cursor has been travelling.
const motionAxis = agentFrame.createMotionAxis({smoothing: 0.1, teleport: 0.25})
return (frameParams) => {
const {dt, aspect, pointerX, pointerY} = readAgentFrame(frameParams)
const domainX = Math.max(aspect, 0.01)
const count = sys.resolveCount(g('count', 5000))
const {cols, cellW, cellH} = agentFrame.fitJitteredGrid(count, domainX, OVERSCAN)
const cursorX = pointerX * aspect
const cursorY = pointerY
const axis = motionAxis.update(cursorX, cursorY)
const reach = g('reach', 0.35)
const colA = getCpuValue('colorA') as {x: number; y: number; z: number; w: number} | undefined
const colB = getCpuValue('colorB') as {x: number; y: number; z: number; w: number} | undefined
sys.writeParams({
colA: d.vec4f(colA?.x ?? 0.52, colA?.y ?? 0.57, colA?.z ?? 0.64, colA?.w ?? 1),
colB: d.vec4f(colB?.x ?? 1, colB?.y ?? 0.62, colB?.z ?? 0.36, colB?.w ?? 1),
count,
dt, aspect, domainX,
cursorX, cursorY, axisX: axis.x, axisY: axis.y,
fieldType: 0, restMode: 0,
strength: g('strength', 1), reachSq: reach * reach,
alignK: 45, damping: 3 + Math.min(Math.max(g('response', 0.5), 0), 1) * 9,
restK: 6, pullK: 0.25, homeK: 2.5,
omegaRef: 6, agitCool: 1,
gridCols: cols, cellW, cellH, jitter: 0.85,
bodyR: Math.min(Math.max(g('size', 1), 0.5), 3) * 1.7 / SIZE_REF_RES,
})
return sys.frame({count})
}
},
fragment: {output: 'filingsTexture', fallback: 'transparent'},
}),
})
Types
| Type | What it is |
|---|---|
AdvectLayout | What a fluid-carried layout must name: agents, the fluid's velocity texture velTex and the params values. |
AgentChannel | One number in 0–1 read out of a color and its alpha: a brightness, a channel, a saturation. |
AgentDrift | A built-in velocity for an agent, from its index alone. |
AgentFieldAt | A sample of a field at a 2D position. |
AgentForce3 | A push on a 3D agent, from its position, velocity and per-agent seed. |
AgentHome2 | An agent's resting position, from its index. |
AgentSteer2 | A steering push on a 2D agent, from its position, velocity and its own top speed. |
AgentTorque | A turn on a turning agent, from the field at its position, its angle and its index. |
DriftLayout | What a drifting-mote layout must name: agents and the params values. |
FieldSampleValue | A field sample's value: rhat, strength, phi. |
OrientationLayout | What a turning-agent layout must name: agents, the agit excitement buffer and the params values. |
PressureForce | What force.pressure returns: the force, plus the two density-grid steps and the grid's cell count for the program. |
ReliefLayout | What an image-relief layout must name: pos, vel, col, the child picture src and the params values. |
ShapeField3 | The signed distance to a shape's surface at a shape-local 3D point, negative inside. |
SteeringLayout | What a flock's layout must name: agents, the agit excitement buffer and the params values. |
Vec4StateArray | A per-agent buffer of four numbers each (agents, pos, vel, …). |
VolumeSwarmLayout | What a 3D swarm's layout must name: pos, vel, the dens grid and the params values. |
Words
- integratorThe update step of an agent simulation: one per kind of agent.
- integrator.steering2dA flock under summed steering, kept cruising, arcing off the canvas edges.
- integrator.forces3dA 3D swarm under summed forces, with drag, a speed limit and a position clamp.
- integrator.orientation2dAgents that turn in place under summed torques and drift a little toward the field.
- integrator.drift2dMotes that glide on a built-in drift plus a fading gust, wrapping around the canvas.
- integrator.reliefParticles standing on the child's image, rising by a color channel, springing home.
- integrator.advect2dParticles carried by a fluid's velocity texture, easing back to an even spread.
- forceThe pushes an agent can feel.
- force.flockingSeparation, alignment and cohesion for a flock, with live weights and radii.
- force.cursorSteerThe cursor on a flock: attract, scatter like a predator, or nothing.
- force.wallTurnA soft turn back toward the canvas near an edge, so a flock arcs instead of bouncing.
- force.pressureSpread a 3D swarm evenly by pushing agents away from crowded cells of a density grid.
- force.containmentKeep a 3D swarm inside a shape: a spring at the surface, a pull back from far away, and a drag when the shape rotates.
- force.cursorXYThe cursor as a magnet on a 3D swarm, pushing away (or pulling in when the force is negative).
- force.gravityA constant pull on a 3D swarm in the direction of
params.gravX, gravY. - force.turbulenceA per-agent jitter that keeps a 3D swarm alive, scaled by
params.agitation. - force.cursorGustA puff from the cursor on drifting motes, zero while the force slider sits at 0.
- force.cursorInViewThe cursor on an image relief, applied where each particle appears on screen.
- torqueThe turns an agent that spins in place can feel, for
integrator.orientation2d. - torque.alignToFieldSwing onto the field's line like a compass needle.
- torque.restA weak swing back to each agent's rest angle, felt only where the field is faint.
- fieldWhat a turning agent samples at its position: the magnetic field at the cursor, and its resting place on a jittered grid.
- field.dipoleOrRadialThe field of a magnet at the cursor: a bar magnet along its travel, or a single pole.
- field.jitteredGridHomeEach agent's resting position on a jittered grid that runs past the canvas edges.
- driftA built-in velocity for motes that glide, for
integrator.drift2d. - drift.variedHeadingOne shared heading and speed, each varied a little per mote.
- channelWhich part of the child's color drives a particle:
luminance,luminanceInverted,red,green,blue,saturationoralpha, each a number in 0–1. - FieldSampleWhat a field sample carries: the direction away from the source (
rhat), thestrength, and the line anglephiin radians. - channel.alphaThe pixel's alpha.
- channel.blueThe blue channel.
- channel.byModeThe same seven by a select prop's number: 0 luminance, 1 inverted, 2 red, 3 green, 4 blue, 5 saturation, 6 alpha.
- channel.greenThe green channel.
- channel.luminanceThe color's brightness, 0 black to 1 white.
- channel.luminanceInvertedOne minus the brightness, so dark areas rise.
- channel.redThe red channel.
- channel.saturationHow colorful the pixel is, 0 grey to 1 pure.