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

inert preview
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