Specimen / simulation
Murmuration
A dense GPU particle swarm that flocks like a starling murmuration at dusk - cohering, separating, aligning, and flowing around a slow invisible attractor with curl-noise wander. True per-neighbor Reynolds flocking computed on the GPU (a Neighbor POP index list iterated in a GLSL POP), rendered as luminous additive point sprites.
advanced 18 operators
Node graph preview
Murmuration graph
Show raw TDN YAML
Paste in TouchDesigner with Embody running.
format: tdn
version: '2.0'
build: null
generator: Embody/6.0.16
td_build: 099.2025.32820
source_file: Embody-6.16.toe
exported_at: '2026-06-12T00:27:58Z'
network_path: /specimen_lab/murmuration
options:
include_dat_content: true
include_storage: true
type_defaults:
annotateCOMP:
parameters:
order: =me.digits or 0
layerzone: =0 if hasattr(me, 'EncloseOPs') and me.EncloseOPs else 1
flags:
- display
color: [0.45, 0.45, 0.45]
glslPOP:
parameters:
outputattrs: '*'
size: [130, 90]
color: [0.67, 0.67, 0.67]
infoDAT:
flags:
- viewer
size: [130, 90]
color: [0.67, 0.67, 0.67]
textDAT:
parameters:
language: glsl
flags:
- viewer
size: [130, 90]
color: [0.67, 0.67, 0.67]
par_templates:
about:
- default: '1.0'
help: Annotate COMP default setup version.
name: Version
readOnly: true
startSection: true
style: Str
- help: Click to open help page.
name: Help
style: Pulse
text:
- default: Annotate
help: Text in the title bar.
label: Title Text
name: Titletext
startSection: true
style: Str
- clampMin: true
default: 30
help: Height of the title bar. Title font height adjusts automatically to fill.
label: Title Height
name: Titleheight
normMax: 100.0
normMin: 5.0
style: Int
- default: left
help: Alignment of title text.
label: Title Align
menuLabels:
- Left
- Center
- Right
menuNames:
- left
- center
- right
name: Titlealign
style: Menu
- help: Text in the body area. Use an expression for newlines etc.
label: Body Text
name: Bodytext
startSection: true
style: Str
- clampMin: true
default: 10
help: Size of text in the body area.
label: Body Font Size
name: Bodyfontsize
normMax: 100.0
normMin: 8.0
style: Int
- help: Limit the width of the text area.
label: Limit Body Text Width
name: Bodylimitwidth
style: Toggle
- clampMin: true
default: 1000
help: Width limit that will cause wraparound or cut-off. Measured in Panel units.
label: Max Body Text Width
name: Bodymaxwidth
normMax: 2000.0
normMin: 100.0
style: Int
settings:
- default: comment
help: 'Switch between Comment, Network Box, and Annotate Modes. '
menuLabels:
- Comment
- Network Box
- Annotate
menuNames:
- comment
- networkbox
- annotate
name: Mode
style: Menu
- help: Converts quotes, ellipsis, and dashes to more typographically nice unicode versions.
label: Smart Quote
name: Smartquote
startSection: true
style: Toggle
- default: true
help: Wrap body text when it extends past right bound.
label: Body Word Wrap
name: Bodywordwrap
style: Toggle
- default: 1
help: Background color base.
label: Back Color
name: Backcolorr
startSection: true
style: RGBA
- clampMax: true
clampMin: true
default: 1
help: Back color alpha.
label: Back Color Alpha
name: Backcoloralpha
style: Float
- clampMax: true
clampMin: true
default: 1
help: Opacity of the entire Annotate.
label: Annotate Opacity
name: Opacity
style: Float
op_viewer:
- help: Turn the visibility of the viewer specified in the OP parameter below on or off.
label: Viewer Display
name: Opviewerdisplay
style: Toggle
- help: The operator whose viewer is displayed in the Annotate.
label: OP
name: Opviewer
style: OP
- help: Allow interaction with the OP viewer.
label: OP Viewer Interactive
name: Opviewerinteractive
style: Toggle
- default: 'False'
help: Use the Size/Aspect Override to control viewer's size in the background.
label: Size/Aspect Override
menuLabels:
- Natural
- Specify
- Auto-Fit
menuNames:
- natural
- specify
- autofit
name: Opvieweroversize
startSection: true
style: Menu
- clampMin: true
default: 800
help: Diplay viewer as-if it were being displayed at this resolution. This is particularly useful for zooming into operators that don't have a built-in resolution, like CHOPs, SOPs, and DATs.
label: Size/Aspect
name: Opviewersize
normMax: 1000.0
normMin: 1.0
style: WH
- default: 1
help: Scale the viewer by this factor.
label: Scale
name: Opviewerscale
normMax: 2.0
startSection: true
style: Float
- help: Move the border of the viewer towards left edge of Annotate when negative or towards right edge when positive.
label: Justify X
name: Opviewerjustifyx
normMin: -1.0
style: Float
- label: Justify Y
name: Opviewerjustifyy
normMin: -1.0
style: Float
- help: When True, allow viewer to display in the Annotate title area as well as body.
label: Cover Body and Title
name: Opviewerfillbodytitle
style: Toggle
- clampMin: true
default: 1
help: Zoom the viewer by this scale factor without increasing the size of its display area in the Annotate.
label: OP Viewer Zoom
min: 0.001
name: Opviewerzoom
normMax: 5.0
normMin: 1.0
startSection: true
style: Float
- help: Offsets the displayed area within the viewer. Combined with OP Viewer Zoom, this lets you display a specific area of a viewer, such as a CHOP channel or table cell.
label: OP Viewer Offset
name: Opvieweroffsetx
style: XYZW
- clampMax: true
clampMin: true
default: 1
help: Alpha value of the background area in the OP Viewer.
label: Fill Alpha
name: Opviewerfillalpha
startSection: true
style: Float
type: baseCOMP
custom_pars:
Murmuration:
- name: Cohesion
style: Float
startSection: true
default: 0.04
max: 2.0
clampMin: true
normMax: 0.5
help: Steer toward the local flock centroid (Reynolds cohesion).
- name: Alignment
style: Float
default: 0.55
max: 3.0
clampMin: true
normMax: 2.0
help: Match the mean heading of neighbors (Reynolds alignment).
- name: Separation
style: Float
default: 0.9
max: 5.0
clampMin: true
normMax: 3.0
help: Inverse-square repulsion from close neighbors. Prevents collapse - the anti-blob force.
- name: Attractor
style: Float
default: 0.15
max: 2.0
clampMin: true
help: Pull toward the slow wandering roost attractor.
- name: Spiral
style: Float
default: 0.55
max: 3.0
clampMin: true
normMax: 2.0
help: Tangential swirl around the attractor (vortex shear / split-reform).
- name: Curl
style: Float
default: 0.4
max: 3.0
clampMin: true
normMax: 2.0
help: Curl-noise wander for organic break-up.
- name: Bodyradius
style: Float
label: Body Radius
startSection: true
default: 2.4
min: 0.5
max: 6.0
clampMin: true
normMin: 1.0
normMax: 4.0
help: Soft containment radius - the size of the coherent body.
- name: Morphspeed
style: Float
label: Morph Speed
default: 1
max: 4.0
clampMin: true
normMax: 3.0
help: Overall evolution rate (attractor drift + curl). 0 freezes the motion.
- name: Maxparticles
style: Int
label: Max Particles
startSection: true
default: 5000
min: 100.0
clampMin: true
normMin: 1000.0
normMax: 40000.0
help: Maximum live particle pool.
- name: Birthrate
style: Float
label: Birth Rate
default: 2000
clampMin: true
normMax: 8000.0
help: Particles born per second.
color: [0.67, 0.67, 0.67]
operators:
- name: out1
type: outTOP
parameters:
label: =me.name
flags:
- viewer
position: [2900, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
inputs:
- bloom_glow
- name: null_sim
type: nullPOP
flags:
- display
- render
position: [1400, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
inputs:
- glsl_flock
- name: annotate1
type: annotateCOMP
sequences:
ext:
- object: op.TDAnnotate.mod.AnnotateExt.AnnotateExt(me)
promote: true
custom_pars:
Text:
$t: text
Titletext: Source & Emission
Bodytext: Sphere POP seeds start positions; Particle POP births + integrates (timeintegration ON). Its targetpop is null_sim - the downstream feedback target that closes the GPU sim loop each frame.
Settings:
$t: settings
Mode: annotate
Backcolorr: [0.12, 0.14, 0.2]
Opacity: 0.55
OP Viewer:
$t: op_viewer
Opvieweroversize: natural
About:
$t: about
position: [-70, -70]
size: [570, 330]
palette_clone: true
- name: annotate2
type: annotateCOMP
sequences:
ext:
- object: op.TDAnnotate.mod.AnnotateExt.AnnotateExt(me)
promote: true
custom_pars:
Text:
$t: text
Titletext: Flocking Brain - GPU Reynolds + Feedback
Bodytext: Neighbor POP emits a per-point neighbor INDEX list (Nebr). The GLSL POP compute shader iterates those real neighbors for true Reynolds steering - cohesion, alignment, and inverse-square separation - plus a slow moving attractor, curl-noise wander, soft containment and drag. It writes PartForce; null_sim feeds the result back to the Particle POP.
Settings:
$t: settings
Mode: annotate
Backcolorr: [0.1, 0.16, 0.16]
Opacity: 0.55
OP Viewer:
$t: op_viewer
Opvieweroversize: natural
About:
$t: about
position: [730, -240]
size: [870, 500]
palette_clone: true
- name: annotate3
type: annotateCOMP
sequences:
ext:
- object: op.TDAnnotate.mod.AnnotateExt.AnnotateExt(me)
promote: true
custom_pars:
Text:
$t: text
Titletext: Color, Sprite Render & Bloom
Bodytext: A side-branch GLSL POP maps speed -> a 3-stop dusk color ramp (deep blue-violet / ice-cyan / warm amber) and a PointScale. The Point Sprite MAT renders the swarm as additive soft round dots (sprite_dot drives the sprite shape); Bloom adds the luminous glow; out1 is the output TOP.
Settings:
$t: settings
Mode: annotate
Backcolorr: [0.18, 0.13, 0.16]
Opacity: 0.55
OP Viewer:
$t: op_viewer
Opvieweroversize: natural
About:
$t: about
position: [1870, -470]
size: [1230, 730]
palette_clone: true
- name: bloom_glow
type: bloomTOP
position: [2600, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
inputs:
- render_swarm
- name: glsl_color
type: glslPOP
parameters:
computedat: glsl_color_compute
sequences:
attr:
- customname: Color
numcomps: '4'
value0: 0.5
value1: 0.6
value2: 1
- customname: PointScale
value0: 1
position: [2000, 0]
inputs:
- null_sim
- name: glsl_flock
type: glslPOP
parameters:
computedat: glsl_flock_compute
sequences:
vec:
- name: uWeights
type: vec4
valuex: =parent().par.Cohesion.eval()
valuey: =parent().par.Alignment.eval()
valuez: =parent().par.Separation.eval()
valuew: =parent().par.Attractor.eval()
- name: uDynamics
type: vec4
valuex: =parent().par.Bodyradius.eval()
valuey: 3.5
valuez: =parent().par.Curl.eval()
valuew: =absTime.seconds*0.3*parent().par.Morphspeed.eval()
- name: uAttractor
type: vec4
valuex: =math.sin(absTime.seconds*0.11*parent().par.Morphspeed.eval())*1.8 + math.sin(absTime.seconds*0.067*parent().par.Morphspeed.eval())*0.9
valuey: =0.3 + math.sin(absTime.seconds*0.09*parent().par.Morphspeed.eval())*1.1
valuez: =math.cos(absTime.seconds*0.13*parent().par.Morphspeed.eval())*1.8 + math.cos(absTime.seconds*0.051*parent().par.Morphspeed.eval())*0.9
valuew: =parent().par.Spiral.eval()
position: [1100, 0]
inputs:
- neighbor_analysis
- name: mat_sprite
type: pointspriteMAT
parameters:
colormap: sprite_dot
pointsize: 9
skelrootpath: =parent()
blending: true
destblend: one
depthtest: false
depthwriting: false
position: [2300, -230]
size: [130, 90]
color: [0.67, 0.67, 0.67]
- name: sprite_dot
type: glslTOP
parameters:
pixeldat: sprite_dot_pixel
computedat: sprite_dot_compute
outputresolution: custom
resolutionw: 64
resolutionh: 64
position: [2140, -230]
size: [130, 90]
color: [0.67, 0.67, 0.67]
- name: particle_pop
type: particlePOP
parameters:
targetpop: null_sim
maxparticles: =parent().par.Maxparticles.eval()
birthrate: =parent().par.Birthrate.eval()
life: 8
position: [300, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
inputs:
- source_points
- name: render_swarm
type: rendersimpleTOP
parameters:
camdistance: 7.5
bgcolorr: 0.01
bgcolorg: 0.012
bgcolorb: 0.03
bgcolora: 1
pop: glsl_color
materialsource: matnode
mat: mat_sprite
position: [2300, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
- name: source_points
type: spherePOP
size: [130, 90]
color: [0.67, 0.67, 0.67]
- name: glsl_color_info
type: infoDAT
parameters:
op: glsl_color
position: [2110, -170]
dock: glsl_color
dat_read_only: true
- name: glsl_flock_info
type: infoDAT
parameters:
op: glsl_flock
position: [1210, -170]
dock: glsl_flock
dat_read_only: true
- name: sprite_dot_info
type: infoDAT
parameters:
op: sprite_dot
position: [2260, -400]
dock: sprite_dot
dat_read_only: true
- name: sprite_dot_pixel
type: textDAT
parameters:
extension: frag
position: [2130, -400]
dock: sprite_dot
dat_content: |-
out vec4 fragColor;
void main(){
vec2 uv = vUV.st * 2.0 - 1.0;
float r = length(uv);
float a = smoothstep(1.0, 0.15, r); // bright full core, soft round edge
fragColor = vec4(vec3(a), 1.0);
}
dat_content_format: text
- name: neighbor_analysis
type: neighborPOP
parameters:
maxdistance: 0.2
numhashbuckets: 8192
maxneighbors: 16
maxnebrsavg: 16
incquerypt: false
addprefix: true
position: [800, 0]
size: [130, 90]
color: [0.67, 0.67, 0.67]
inputs:
- particle_pop
- name: glsl_color_compute
type: textDAT
position: [1940, -170]
dock: glsl_color
dat_content: |
// Murmuration - speed -> dusk color ramp + point size (render side branch)
void main(){
uint id = TDIndex();
if(id >= TDNumElements()) return;
vec3 V = TDIn_PartVel(0, id);
float sp = length(V);
float t = clamp((sp - 0.12) / 0.55, 0.0, 1.0);
vec3 violet = vec3(0.16, 0.09, 0.55); // slow - deep blue-violet
vec3 cyan = vec3(0.55, 0.92, 1.00); // mid - ice cyan/white
vec3 amber = vec3(1.00, 0.68, 0.26); // fast - warm amber/gold
vec3 col = (t < 0.5) ? mix(violet, cyan, t * 2.0)
: mix(cyan, amber, (t - 0.5) * 2.0);
Color[id] = vec4(col * 0.7, 1.0);
PointScale[id] = 1.0 + t * 0.9;
P[id] = TDIn_P(0, id);
}
dat_content_format: text
- name: glsl_flock_compute
type: textDAT
position: [1040, -170]
dock: glsl_flock
dat_content: |
// Murmuration - GPU flocking compute (GLSL POP), TRUE per-neighbor Reynolds.
// Iterates the Neighbor POP index list (Nebr) to compute cohesion, alignment,
// and inverse-square separation directly - the separation is dominated by the
// closest neighbor so it never cancels in a symmetric clump (even spacing).
// uWeights = (cohesion, alignment, separation, attractor)
// uDynamics = (bodyRadius, maxForce, curl, time)
// uAttractor = (ax, ay, az, spiral)
vec3 hash33(vec3 p){
p = vec3(dot(p, vec3(127.1,311.7,74.7)), dot(p, vec3(269.5,183.3,246.1)), dot(p, vec3(113.5,271.9,124.6)));
return fract(sin(p) * 43758.5453) * 2.0 - 1.0;
}
float vnoise(vec3 p){
vec3 i=floor(p); vec3 f=fract(p); vec3 u=f*f*(3.0-2.0*f);
return mix(mix(mix(dot(hash33(i+vec3(0,0,0)),f-vec3(0,0,0)), dot(hash33(i+vec3(1,0,0)),f-vec3(1,0,0)),u.x),
mix(dot(hash33(i+vec3(0,1,0)),f-vec3(0,1,0)), dot(hash33(i+vec3(1,1,0)),f-vec3(1,1,0)),u.x),u.y),
mix(mix(dot(hash33(i+vec3(0,0,1)),f-vec3(0,0,1)), dot(hash33(i+vec3(1,0,1)),f-vec3(1,0,1)),u.x),
mix(dot(hash33(i+vec3(0,1,1)),f-vec3(0,1,1)), dot(hash33(i+vec3(1,1,1)),f-vec3(1,1,1)),u.x),u.y),u.z);
}
vec3 snoiseVec3(vec3 x){ return vec3(vnoise(x), vnoise(x+vec3(123.4,0,0)), vnoise(x+vec3(0,234.5,0))); }
vec3 curlNoise(vec3 p){
const float e=0.1; vec3 dx=vec3(e,0,0), dy=vec3(0,e,0), dz=vec3(0,0,e);
vec3 px0=snoiseVec3(p-dx),px1=snoiseVec3(p+dx),py0=snoiseVec3(p-dy),py1=snoiseVec3(p+dy),pz0=snoiseVec3(p-dz),pz1=snoiseVec3(p+dz);
float x=(py1.z-py0.z)-(pz1.y-pz0.y), y=(pz1.x-pz0.x)-(px1.z-px0.z), z=(px1.y-px0.y)-(py1.x-py0.x);
return normalize(vec3(x,y,z)/(2.0*e) + 1e-6);
}
void main(){
uint id = TDIndex();
if(id >= TDNumElements()) return;
vec3 P0 = TDIn_P(0, id);
vec3 V0 = TDIn_PartVel(0, id);
int nN = int(TDIn_NumNebrs(0, id));
vec3 sumPos = vec3(0.0);
vec3 sumVel = vec3(0.0);
vec3 sep = vec3(0.0);
int cnt = 0;
const int MAXN = 16;
for(int i = 0; i < MAXN; i++){
if(i >= nN) break;
uint nIdx = TDIn_Nebr(0, id, i);
vec3 nP = TDIn_P(0, uint(nIdx));
vec3 nV = TDIn_PartVel(0, uint(nIdx));
vec3 diff = P0 - nP;
float dist = length(diff) + 1e-5;
sumPos += nP;
sumVel += nV;
float push = clamp(0.03 / (dist*dist), 0.0, 6.0); // inverse-square-capped: closest neighbor
sep += (diff / dist) * push; // closest neighbor dominates (no cancel)
cnt++;
}
vec3 force = vec3(0.0);
if(cnt > 0){
vec3 nbrCenter = sumPos / float(cnt);
vec3 avgVel = sumVel / float(cnt);
force += uWeights.x * (nbrCenter - P0); // cohesion
force += uWeights.y * (avgVel - V0); // alignment
force += uWeights.z * sep; // separation: summed per-neighbor push (crowded => stronger)
}
// moving attractor - slow radial pull + tangential spiral
vec3 toA = uAttractor.xyz - P0;
float dA = length(toA) + 1e-5;
vec3 dirA = toA / dA;
force += uWeights.w * dirA;
vec3 tang = normalize(cross(vec3(0.0,1.0,0.0), dirA) + 1e-6);
force += uAttractor.w * tang;
// curl wander
vec3 cp = P0 * 1.6 + vec3(0.0, uDynamics.w, 0.0);
force += uDynamics.z * curlNoise(cp);
// clamp the steering forces
float maxF = uDynamics.y;
float fl = length(force);
if(fl > maxF) force *= maxF / fl;
// containment + drag AFTER the clamp
float R = uDynamics.x;
if(dA > R) force += dirA * (dA - R) * 1.5;
force -= V0 * 0.7;
PartForce[id] = force;
P[id] = P0;
}
dat_content_format: text
- name: sprite_dot_compute
type: textDAT
position: [2000, -400]
dock: sprite_dot
annotations:
- name: annotate1
mode: annotate
title: Source & Emission
text: Sphere POP seeds start positions; Particle POP births + integrates (timeintegration ON). Its targetpop is null_sim - the downstream feedback target that closes the GPU sim loop each frame.
position: [-70, -70]
size: [570, 330]
color: [0.12, 0.14, 0.2]
opacity: 0.55
- name: annotate2
mode: annotate
title: Flocking Brain - GPU Reynolds + Feedback
text: Neighbor POP emits a per-point neighbor INDEX list (Nebr). The GLSL POP compute shader iterates those real neighbors for true Reynolds steering - cohesion, alignment, and inverse-square separation - plus a slow moving attractor, curl-noise wander, soft containment and drag. It writes PartForce; null_sim feeds the result back to the Particle POP.
position: [730, -240]
size: [870, 500]
color: [0.1, 0.16, 0.16]
opacity: 0.55
- name: annotate3
mode: annotate
title: Color, Sprite Render & Bloom
text: A side-branch GLSL POP maps speed -> a 3-stop dusk color ramp (deep blue-violet / ice-cyan / warm amber) and a PointScale. The Point Sprite MAT renders the swarm as additive soft round dots (sprite_dot drives the sprite shape); Bloom adds the luminous glow; out1 is the output TOP.
position: [1870, -470]
size: [1230, 730]
color: [0.18, 0.13, 0.16]
opacity: 0.55