10.2.2 Boids in TouchDesigner — Physics in CHOPs, Rendering in Instances
Port the NumPy boids simulation from Module 5.2.1 to a TouchDesigner network that runs 5,000 agents at 60 fps. The physics lives in a Script CHOP; the rendering lives in a Geometry COMP with GPU instancing.
Big Question — how do you scale a 50-agent Python sim to 5,000 agents at 60 fps?
Module 5.2.1 implemented Craig Reynolds’ boids in NumPy. The pure-Python simulation handles 50–100 agents at interactive speed; past that it drops below 30 fps and the visualisation stutters. To run thousands of boids — for a gallery installation, a music-reactive backdrop, or a VR scene — you need to (a) move the physics into a faster execution context and (b) replace per-agent rendering with batched GPU instancing. TouchDesigner is built for both. The physics moves into a Script CHOP that runs once per frame and outputs all agent positions as channel arrays. The rendering moves into a Geometry COMP with GPU instancing — one draw call for any number of agents.
The three boids rules from 5.2.1 — separation, alignment, cohesion — are the same. The data structures are the same (NumPy arrays of positions and velocities). What changes is where the computation lives and how the output is drawn.
Learning objectives
- Move per-frame physics from a Python script into a Script CHOP whose output is consumed by other operators.
- Use GPU instancing to render N copies of one geometry from one draw call, replacing N draw calls.
- Identify the performance crossover point where CPU NumPy is no longer fast enough and GPU/native pipelines become necessary.
- Recognise the three-tier architecture — physics layer, rendering layer, control layer — that underlies all real-time agent simulations.
Part 1 — Physics in a Script CHOP
A Script CHOP runs a Python onCook callback every frame. It outputs channels — named arrays of float values that other operators can read. For boids, the channels are:
tx,ty— position (length N)vx,vy— velocity (length N)- (optional)
heading— rotation angle for rendering
Inside the script, the per-frame body is essentially the NumPy code from 5.2.1, minus the loop over frames (TD provides that). Read agent state from me.par.X storage, compute separation/alignment/cohesion, update velocity and position, write back to the output channels.
# inside a Script CHOP onCook callback
def onCook(scriptOp):
n = me.par.Numagents.eval()
pos = positions # persisted state (module-level NumPy array)
vel = velocities
sep = separation(pos) # same as 5.2.1
align = alignment(pos, vel)
coh = cohesion(pos)
vel = clip_speed(vel + sep + align + coh, MAX_SPEED)
pos = (pos + vel) % WORLD_SIZE
scriptOp.clear()
scriptOp.appendChan('tx').vals = pos[:, 0].tolist()
scriptOp.appendChan('ty').vals = pos[:, 1].tolist()
scriptOp.appendChan('heading').vals = np.degrees(
np.arctan2(vel[:, 1], vel[:, 0])).tolist()
return The state (positions, velocities) is stored as module-level NumPy arrays inside the script. They persist between frames because the script lives in the cook cycle, not in a fresh Python process.
Part 2 — Rendering with GPU instancing
Drawing 5,000 small triangles individually requires 5,000 draw calls. Even at 100 µs per call, that is 500 ms per frame — useless. The cure is GPU instancing: one draw call submits one geometry plus a per-instance attribute array (positions, headings, sizes), and the GPU draws all N copies in parallel.
In TouchDesigner you set this up on the Geometry COMP:
- Build the agent geometry as a single SOP — a small triangle or sphere.
- On the Geometry COMP, set
InstancingtoOn. - In the Instance page of parameters, point the
Translate OPto your physics Script CHOP (thetx,tychannels become per-instance translations). - Point the
Rotate OPto the same CHOP (theheadingchannel becomes per-instance rotation). - The COMP now draws all N copies in one GPU draw call.
Script CHOP (physics output)
╲
╲ Translate OP, Rotate OP
╲
Geometry COMP (with instancing on) → Render TOP → OutModern GPUs handle 50,000–500,000 instances per frame easily. The bottleneck moves entirely from the rendering side to the physics side — exactly where you can apply NumPy and the GPU-friendly algorithms from Module 7.
Part 3 — Three-tier architecture
The Boids-in-TD setup illustrates a pattern that recurs for every real-time agent simulation:
Physics layer — Script CHOP (or GLSL Compute TOP for very large counts). Inputs: state from previous frame. Outputs: new state as channels.
Rendering layer — Geometry COMP with instancing. Inputs: channels from physics layer. Output: a Render TOP.
Control layer — Script DAT and CHOPs. Inputs: external events (MIDI, OSC, audio). Outputs: parameter changes on the physics layer.
This separation is what lets you scale: optimise the physics layer to run more agents, optimise the rendering layer to draw them faster, and add features in the control layer without touching either. Compare to the NumPy version where adding “respond to audio” means surgery on the inner loop.
Synthesis project — port your 5.2.1 boids to TD
Take the NumPy boids from lesson 5.2.1, drop the physics function into a Script CHOP, wire the output channels to a Geometry COMP with instancing, and watch 500–5,000 boids fly at 60 fps. The .py download is the standalone reference matching the v1 implementation; the .toe project shows the wired TD network.
boids_td_standalone.py — reference NumPy implementation (for the Script CHOP) boids_td_script.py — Script CHOP onCook code boids_td_starter.py — implement the three boids forces from scratchReflection questions
- What is the largest single cost in the pure-NumPy 5.2.1 version that disappears in the TD version?
- Why is GPU instancing useless for boids if your geometry is one triangle but heroic if it is a complex sphere mesh?
- At what agent count does pure NumPy stop being competitive with the TD version, and why?
Answers
Largest cost that disappears — rendering. The Python version uses PIL or matplotlib to draw a frame, which costs many milliseconds per agent. TD’s instanced renderer is one draw call regardless of count, so the rendering term effectively becomes zero. The physics term is the same in both versions.
Triangle vs sphere — for a triangle, the draw-call cost is already tiny and instancing’s win is modest. For a sphere with 1000 polygons, the per-agent cost is 1000× larger. Instancing absorbs all 1000 in one draw call, so the speedup is dramatic. Bigger geometry, bigger instancing win.
Crossover point — empirically around 500–2000 agents for pure NumPy, depending on the renderer. Past that, the Python rendering loop becomes the bottleneck. TD’s instanced renderer pushes the bottleneck back to physics, where NumPy can hold position until about 10,000 agents (per-frame per-pair distance computations are O(n²)).
Make it your own
- Audio reactivity: add an Audio In CHOP, compute spectrum, use bass amplitude to modulate
cohesion_weight. Boids cluster on drum hits. - GLSL Compute physics: move the Script CHOP body into a GLSL Compute TOP. Now physics runs on the GPU and you can do 50,000 agents.
- Predator agent: one special agent that all boids flee from; the predator chases the centroid of the flock. Classic stability test for any flocking implementation.
Summary
Common pitfalls to avoid
- Per-agent draw calls instead of instancing. The rendering cost stays
O(N)and you cannot scale. - State stored in script-local variables that disappear each frame. Use module-level globals or
me.storagefor persistent state. - Script CHOP reading parameters in Python without
.eval(). You get the parameter object, not its value. - Boids forces computed in a Python loop instead of vectorised NumPy. The
O(N²)cost compounds; vectorise like 5.2.1. - Wiring CHOP channels in wrong order to instancing parameters. Channel order matters; double-check
tx, ty, tzandrx, ry, rzmapping.
References
- [1] Reynolds, C. W. (1987). Flocks, herds and schools: A distributed behavioral model. ACM SIGGRAPH Computer Graphics, 21(4), 25–34. doi:10.1145/37402.37406
- [2] Derivative Inc. (2024). TouchDesigner Geometry COMP Documentation. derivative.ca/UserGuide/Geometry_COMP
- [3] Akeley, K., & Hanrahan, P. (1988). Three pieces of the graphics pipeline. Computer Graphics, 22(4), 197–204.
- [4] NVIDIA Corporation. (2022). Instanced Rendering Best Practices. developer.nvidia.com
- [5] Reynolds, C. W. (1999). Steering behaviors for autonomous characters. In Proceedings of Game Developers Conference 1999 (pp. 763–782).
- [6] Shiffman, D. (2012). The Nature of Code, Chapter 6: Autonomous Agents. natureofcode.com