@bottobot/td-mcp
Version:
TouchDesigner MCP Server v2.8.0 - 21 MCP tools, 629 operators with clean parameter data, 69 Python API classes, 14 tutorials, 32 workflow patterns. Includes version system, experimental techniques KB, core tool enhancements, and experimental build support
134 lines (133 loc) • 17.2 kB
JSON
{
"category": "generative-systems",
"displayName": "Generative Systems",
"description": "Generative and emergent systems in TouchDesigner including L-systems for botanical growth, cellular automata, strange attractors, and Replicator COMP for dynamic component instancing.",
"techniques": [
{
"id": "lsystem_botanical",
"name": "L-System Botanical Growth",
"subcategory": "l-systems",
"description": "Lindenmayer systems for generating procedural plant, tree, coral, and fractal geometry in TouchDesigner. Uses the native L-System SOP for rapid prototyping and Python-driven expansion for custom grammars.",
"difficulty": "beginner",
"operators": ["L-System SOP", "Script SOP", "Merge SOP"],
"tags": ["L-system", "procedural", "botanical", "growth", "fractal", "generative"],
"notes": "TD's L-System SOP has built-in turtle interpretation. Use the Premise and Rules parameters. Animate 'Generation' parameter via LFO CHOP for growth animation. Combine multiple L-System SOPs for complex scenes.",
"code": {
"language": "python",
"filename": "lsystem_custom.py",
"snippet": "# Custom L-System Expander in Python\n# Produces a string of turtle commands from grammar rules\n# Feed result to an L-System SOP or use Script SOP for custom rendering\n\ndef expand_lsystem(axiom, rules, generations):\n \"\"\"\n Expand an L-System grammar.\n axiom: starting string, e.g. 'F'\n rules: dict, e.g. {'F': 'F[+F]F[-F]F'}\n generations: number of expansion steps\n Returns the expanded string.\n \"\"\"\n current = axiom\n for _ in range(generations):\n next_str = ''\n for char in current:\n next_str += rules.get(char, char)\n current = next_str\n return current\n\n# Stochastic L-System with probability-weighted rules\nimport random\n\ndef expand_stochastic(axiom, rules, generations, seed=42):\n \"\"\"\n Stochastic L-System.\n rules: dict mapping symbol -> list of (probability, replacement) tuples\n e.g. {'F': [(0.33, 'F[+F]F'), (0.33, 'F[-F]F'), (0.34, 'FF')]}\n \"\"\"\n rng = random.Random(seed)\n current = axiom\n for _ in range(generations):\n next_str = ''\n for char in current:\n if char in rules:\n choices = rules[char]\n rand = rng.random()\n cumulative = 0.0\n replacement = char\n for prob, rep in choices:\n cumulative += prob\n if rand <= cumulative:\n replacement = rep\n break\n next_str += replacement\n else:\n next_str += char\n current = next_str\n return current\n\n# Example usage (run in Textport or Execute DAT)\n# Classic plant\nresult = expand_lsystem('X', {\n 'X': 'F[+X]F[-X]+X',\n 'F': 'FF'\n}, generations=5)\nprint(f'L-System length after 5 gen: {len(result)}')\n\n# Apply to L-System SOP\ndef apply_to_lsystem_sop(sop, axiom, rules, gen):\n string = expand_lsystem(axiom, rules, gen)\n sop.par.premise = axiom\n # TD L-System SOP uses its own internal expander\n # Set rules as 'Successor' parameters on the SOP\n sop.par.generations = gen\n # For each rule:\n for i, (symbol, replacement) in enumerate(rules.items()):\n sop.par[f'rulepredecessor{i+1}'] = symbol\n sop.par[f'rulesuccessor{i+1}'] = replacement"
},
"presets": {
"description": "Common L-System presets for the L-System SOP",
"examples": [
{
"name": "Symmetric Plant",
"axiom": "F",
"rules": { "F": "F[+F]F[-F]F" },
"angle": 25.7,
"generations": 5
},
{
"name": "Dragon Curve",
"axiom": "FX",
"rules": { "X": "X+YF+", "Y": "-FX-Y" },
"angle": 90,
"generations": 12
},
{
"name": "Sierpinski Triangle",
"axiom": "F-G-G",
"rules": { "F": "F-G+F+G-F", "G": "GG" },
"angle": 120,
"generations": 6
},
{
"name": "3D Bush",
"axiom": "A",
"rules": { "A": "[&FL!A]/////'[&FL!A]///////'[&FL!A]", "F": "S/////F", "S": "FL", "L": "['''^^{-f+f+f-|-f+f+f}]" },
"angle": 22.5,
"generations": 4,
"note": "3D interpretation — use in L-System SOP with 3D mode"
}
]
}
},
{
"id": "cellular_automata_gol",
"name": "Cellular Automata — Game of Life and Beyond",
"subcategory": "cellular-automata",
"description": "GPU-accelerated cellular automata including Conway's Game of Life, Brian's Brain, and custom rule sets running as GLSL shaders with Feedback TOP ping-pong.",
"difficulty": "intermediate",
"operators": ["GLSL TOP", "Feedback TOP", "Constant CHOP"],
"tags": ["cellular-automata", "Game-of-Life", "Conway", "emergence", "GPU", "simulation"],
"notes": "Initialize with Noise TOP or draw-in with CHOPexec. Rules encoded in GLSL as neighbor-count lookup. Experiment with neighborhood type (Moore/Von Neumann) and rule strings.",
"code": {
"language": "glsl",
"filename": "game_of_life.glsl",
"snippet": "// Conway's Game of Life — GLSL TOP with Feedback\n// Input 0: Feedback TOP (previous generation, R channel = alive/dead)\n// Output: next generation\n\nuniform float uThreshold; // e.g. 0.5\n\nint getCell(vec2 uv, vec2 offset) {\n vec2 texel = 1.0 / uTD2DInfos[0].res.zw;\n vec4 s = texture(sTD2DInputs[0], uv + offset * texel);\n return (s.r > uThreshold) ? 1 : 0;\n}\n\nvoid main() {\n vec2 uv = vUV.st;\n int self = getCell(uv, vec2(0,0));\n \n // Count Moore neighborhood (8 neighbors)\n int neighbors = \n getCell(uv, vec2(-1,-1)) + getCell(uv, vec2(0,-1)) + getCell(uv, vec2(1,-1)) +\n getCell(uv, vec2(-1, 0)) + getCell(uv, vec2(1, 0)) +\n getCell(uv, vec2(-1, 1)) + getCell(uv, vec2(0, 1)) + getCell(uv, vec2(1, 1));\n \n // Conway's rules: B3/S23\n int next = 0;\n if (self == 1 && (neighbors == 2 || neighbors == 3)) next = 1; // Survive\n if (self == 0 && neighbors == 3) next = 1; // Birth\n \n float v = float(next);\n fragColor = TDOutputSwizzle(vec4(v, v, v, 1.0));\n}"
},
"variants": [
{
"name": "Brian's Brain",
"description": "3-state CA: dead, alive, dying. Creates oscillating structures.",
"snippet": "// Brian's Brain: states 0=dead, 0.5=dying, 1=alive\nint state = (self > 0.75) ? 2 : (self > 0.25) ? 1 : 0; // 2=alive,1=dying,0=dead\nint aliveNeighbors = 0; // count neighbors with state==2\n// ... count as above but check > 0.75\nint next = 0;\nif (state == 2) next = 1; // alive -> dying\nif (state == 1) next = 0; // dying -> dead\nif (state == 0 && aliveNeighbors == 2) next = 2; // dead -> alive if exactly 2 alive neighbors"
},
{
"name": "Continuous CA (Lenia)",
"description": "Continuous Game of Life variant creating organic blob-like lifeforms.",
"snippet": "// Lenia: smooth kernel, continuous state\n// Uses Gaussian kernel for neighborhood computation\n// State is float 0..1, updated via growth function G(n) = exp(-((n-mu)^2)/(2*sigma^2))\nfloat kernel_sample = 0.0;\n// Sum weighted neighborhood with Gaussian kernel...\nfloat growth = exp(-pow(kernel_sample - 0.15, 2.0) / (2.0 * 0.015 * 0.015));\nfloat newState = clamp(self + 0.1 * (2.0 * growth - 1.0), 0.0, 1.0);\nfragColor = TDOutputSwizzle(vec4(newState, newState, newState, 1.0));"
}
]
},
{
"id": "strange_attractors",
"name": "Strange Attractors",
"subcategory": "strange-attractors",
"description": "Visualizing chaotic strange attractors (Lorenz, Rossler, De Jong, Clifford) as point clouds in TouchDesigner. Points are iterated in Python or CHOP and rendered via Instancing or Script SOP.",
"difficulty": "intermediate",
"operators": ["Script CHOP", "Script SOP", "Geometry COMP", "Point Cloud MAT"],
"tags": ["attractor", "chaos", "Lorenz", "Clifford", "De-Jong", "point-cloud", "fractal"],
"code": {
"language": "python",
"filename": "strange_attractors.py",
"snippet": "# Strange Attractors — Script SOP\n# Generates point positions by iterating attractor equations\nimport numpy as np\n\ndef cook(scriptOp):\n n = 200000 # number of points\n attractor = scriptOp.par.attractor.eval() # custom string par: 'lorenz', 'clifford', 'dejong'\n \n if attractor == 'lorenz':\n pts = lorenz_attractor(n)\n elif attractor == 'clifford':\n pts = clifford_attractor(n)\n elif attractor == 'rossler':\n pts = rossler_attractor(n)\n else:\n pts = dejong_attractor(n)\n \n # Write to SOP\n scriptOp.clear()\n for i in range(len(pts)):\n pt = scriptOp.appendPoint()\n pt.P = (pts[i, 0], pts[i, 1], pts[i, 2])\n\ndef lorenz_attractor(n, dt=0.005):\n \"\"\"Lorenz system: dx/dt = sigma(y-x), dy/dt = x(rho-z)-y, dz/dt = xy-beta*z\"\"\"\n sigma, rho, beta = 10.0, 28.0, 8.0/3.0\n pts = np.zeros((n, 3))\n x, y, z = 0.1, 0.0, 0.0\n for i in range(n):\n dx = sigma * (y - x)\n dy = x * (rho - z) - y\n dz = x * y - beta * z\n x += dx * dt; y += dy * dt; z += dz * dt\n pts[i] = [x * 0.03, y * 0.03, z * 0.03 - 0.8]\n return pts\n\ndef clifford_attractor(n, a=-1.7, b=1.8, c=-1.9, d=-0.4):\n \"\"\"Clifford: x1 = sin(a*y) + c*cos(a*x), y1 = sin(b*x) + d*cos(b*y)\"\"\"\n pts = np.zeros((n, 3))\n x, y = 0.0, 0.0\n for i in range(n):\n x1 = np.sin(a * y) + c * np.cos(a * x)\n y1 = np.sin(b * x) + d * np.cos(b * y)\n x, y = x1, y1\n pts[i] = [x * 0.4, y * 0.4, 0.0]\n return pts\n\ndef rossler_attractor(n, dt=0.005, a=0.2, b=0.2, c=5.7):\n \"\"\"Rossler: dx/dt = -y-z, dy/dt = x+ay, dz/dt = b+z(x-c)\"\"\"\n pts = np.zeros((n, 3))\n x, y, z = 1.0, 1.0, 1.0\n for i in range(n):\n dx = -y - z\n dy = x + a * y\n dz = b + z * (x - c)\n x += dx * dt; y += dy * dt; z += dz * dt\n pts[i] = [x * 0.04, y * 0.04, z * 0.04]\n return pts\n\ndef dejong_attractor(n, a=1.4, b=-2.3, c=2.4, d=-2.1):\n \"\"\"Peter De Jong: x1 = sin(a*y) - cos(b*x), y1 = sin(c*x) - cos(d*y)\"\"\"\n pts = np.zeros((n, 3))\n x, y = 0.0, 0.0\n for i in range(n):\n x1 = np.sin(a * y) - np.cos(b * x)\n y1 = np.sin(c * x) - np.cos(d * y)\n x, y = x1, y1\n pts[i] = [x * 0.45, y * 0.45, 0.0]\n return pts"
}
},
{
"id": "replicator_comp",
"name": "Replicator COMP Dynamic Instancing",
"subcategory": "replicator",
"description": "Using Replicator COMP to dynamically create, destroy, and modify instances of Base COMPs based on CHOP or DAT data. Essential for data-driven visualizations, particle-like UI, and generative installations.",
"difficulty": "intermediate",
"operators": ["Replicator COMP", "Base COMP", "CHOP", "DAT"],
"tags": ["Replicator", "dynamic", "instance", "data-driven", "generative", "COMP"],
"notes": "Replicator COMP calls onReplicatorPulse callback when it creates/destroys instances. Each replicated component receives a 'replicaIndex' member. Use 'Master Component' parameter to define the template Base COMP.",
"code": {
"language": "python",
"filename": "replicator_setup.py",
"snippet": "# Replicator COMP callback script\n# Place in the 'Callbacks DAT' of the Replicator COMP\n\ndef onReplicatorPulse(replicatorCOMP, event, replica):\n \"\"\"\n Called when Replicator creates or destroys a replica.\n event: 'onInit', 'onDestroy'\n replica: the Base COMP being created/destroyed\n \"\"\"\n if event == 'onInit':\n idx = replica.digits # index number of this replica\n # Get source data — e.g. from a Table DAT\n table = op('data_table') # DAT with columns: x, y, color_r, color_g, color_b\n if table and idx < table.numRows - 1: # -1 for header row\n row = idx + 1 # skip header\n # Set position\n x = float(table[row, 'x'])\n y = float(table[row, 'y'])\n replica.par.tx = x\n replica.par.ty = y\n # Set a color parameter inside the replica\n colorNode = replica.op('null_color') # a Null CHOP inside master comp\n if colorNode:\n colorNode.par.value0 = float(table[row, 'color_r'])\n colorNode.par.value1 = float(table[row, 'color_g'])\n colorNode.par.value2 = float(table[row, 'color_b'])\n \n elif event == 'onDestroy':\n # Cleanup if needed\n pass\n\n# Trigger replication from data\ndef update_replicator(replicatorCOMP, data_table):\n \"\"\"\n Update Replicator count to match data rows.\n Each row in data_table becomes one replica.\n \"\"\"\n count = max(0, data_table.numRows - 1) # subtract header\n replicatorCOMP.par.numreplicants = count\n replicatorCOMP.par.recreateall.pulse()\n\n# Dynamic data-driven example: replicate based on CHOP samples\ndef replicate_from_chop(replicatorCOMP, chop):\n \"\"\"\n Set replica count to match CHOP sample count.\n Each sample will become one replica, which can read chop[N] in its onInit.\n \"\"\"\n replicatorCOMP.par.numreplicants = chop.numSamples\n replicatorCOMP.par.recreateall.pulse()\n print(f'Created {chop.numSamples} replicas')"
}
},
{
"id": "agent_flocking",
"name": "Agent-Based Flocking (Boids)",
"subcategory": "agent-systems",
"description": "GPU-accelerated boids flocking simulation using GLSL ping-pong compute. Encodes agent position and velocity as texture pixels. Implements separation, alignment, and cohesion rules.",
"difficulty": "advanced",
"operators": ["GLSL TOP", "Feedback TOP", "GLSL Multi TOP"],
"tags": ["boids", "flocking", "agent", "simulation", "GPU", "emergent"],
"code": {
"language": "glsl",
"filename": "boids_update.glsl",
"snippet": "// Boids Update Shader — GLSL TOP ping-pong\n// Texture layout: each pixel = one agent\n// R=posx, G=posy, B=velx, A=vely (all normalized 0..1, remap to world space)\nuniform float uDt;\nuniform float uSepRadius; // separation distance\nuniform float uAliRadius; // alignment radius\nuniform float uCohRadius; // cohesion radius\nuniform float uSepWeight;\nuniform float uAliWeight;\nuniform float uCohWeight;\nuniform float uMaxSpeed; // e.g. 0.003\n\n// World space from 0..1 texture\nvec2 decode_pos(vec4 s) { return s.rg; }\nvec2 decode_vel(vec4 s) { return s.ba * 2.0 - 1.0; } // -1..1\n\nvoid main() {\n vec2 texel = 1.0 / uTD2DInfos[0].res.zw;\n vec2 uv = vUV.st;\n \n vec4 self = texture(sTD2DInputs[0], uv);\n vec2 pos = decode_pos(self);\n vec2 vel = decode_vel(self);\n \n vec2 sep = vec2(0.0), ali = vec2(0.0), coh = vec2(0.0);\n float sepN = 0.0, aliN = 0.0, cohN = 0.0;\n \n // Sample a subset of agents (cost: O(agents_sampled))\n // For full O(N^2): iterate all texels (expensive for large N)\n int W = int(uTD2DInfos[0].res.z);\n int H = int(uTD2DInfos[0].res.w);\n for (int i = 0; i < W; i += 4) { // stride=4 for performance\n for (int j = 0; j < H; j += 4) {\n vec2 nUV = (vec2(i, j) + 0.5) * texel;\n vec4 nb = texture(sTD2DInputs[0], nUV);\n vec2 npos = decode_pos(nb);\n vec2 nvel = decode_vel(nb);\n vec2 diff = pos - npos;\n float dist = length(diff);\n if (dist < 0.001) continue;\n if (dist < uSepRadius) { sep += normalize(diff) / dist; sepN++; }\n if (dist < uAliRadius) { ali += nvel; aliN++; }\n if (dist < uCohRadius) { coh += npos; cohN++; }\n }\n }\n \n vec2 steering = vec2(0.0);\n if (sepN > 0.0) steering += (sep / sepN) * uSepWeight;\n if (aliN > 0.0) steering += (ali / aliN - vel) * uAliWeight;\n if (cohN > 0.0) steering += ((coh / cohN) - pos) * uCohWeight;\n \n vel = vel + steering * uDt;\n float spd = length(vel);\n if (spd > uMaxSpeed) vel = vel / spd * uMaxSpeed;\n \n pos = fract(pos + vel); // wrap around\n \n fragColor = TDOutputSwizzle(vec4(pos, vel * 0.5 + 0.5));\n}"
}
}
],
"resources": [
{ "title": "TouchDesigner L-System SOP", "url": "https://docs.derivative.ca/L-System_SOP" },
{ "title": "TouchDesigner Replicator COMP", "url": "https://docs.derivative.ca/Replicator_COMP" },
{ "title": "Lenia: Biology of Artificial Life", "url": "https://arxiv.org/abs/1812.05433" },
{ "title": "Clifford Attractors", "url": "http://paulbourke.net/fractals/clifford/" },
{ "title": "Boids — Craig Reynolds", "url": "https://www.red3d.com/cwr/boids/" }
]
}