From be5a2ee5d3492dd0b96c2009f07f8eb6658b932d Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:33:14 +0530 Subject: [PATCH] feat(skills): expand touchdesigner-mcp with GLSL, post-FX, audio, geometry references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 6 new reference files with generic reusable patterns: - glsl.md: uniforms, built-in functions, shader templates, Bayer dither - postfx.md: bloom, CRT scanlines, chromatic aberration, feedback glow - layout-compositor.md: layoutTOP, overTOP grids, panel dividers - operator-tips.md: wireframe rendering, feedback TOP setup - geometry-comp.md: instancing, POP vs SOP rendering, shape morphing - audio-reactive.md: band extraction (audiofilterCHOP), beat detection, MIDI Expand SKILL.md with: - TD 2025 API quirks (connection syntax, GLSL TOP rules, expression gotchas) - Trimmed param name table (8 known LLM traps, defers to td_get_par_info) - Slider-to-shader wiring (td_execute_python + ParMode.EXPRESSION) - Frame capture with run()/delayFrames (TOP.save() timing fix) - TD 099 POP vs SOP rendering rules - Incremental build strategy for large scripts - Remote TD setup (PC over Ethernet) - Audio synthesis via CHOPs (LFO-driven envelope pattern) Expand pitfalls.md (#46-63): - Connection syntax, moviefileoutTOP bug, batch frame capture - TOP.save() time advancement, feedback masking, incremental builds - MCP reconnection after project.load(), TOX reverse-engineering - sliderCOMP naming, create() suffix requirement - COMP reparenting (copyOPs), expressionCHOP crash All content is generic — no session-specific paths, hardware, aesthetics, or param-name-only entries (those belong in td_get_par_info). Bumps version 1.0.0 → 2.0.0. --- .../creative/touchdesigner-mcp/SKILL.md | 12 +- .../references/audio-reactive.md | 175 +++++++++++++ .../references/geometry-comp.md | 121 +++++++++ .../touchdesigner-mcp/references/glsl.md | 151 +++++++++++ .../references/layout-compositor.md | 131 ++++++++++ .../references/operator-tips.md | 106 ++++++++ .../touchdesigner-mcp/references/pitfalls.md | 246 ++++++++++++++++-- .../touchdesigner-mcp/references/postfx.md | 183 +++++++++++++ .../references/troubleshooting.md | 2 +- .../touchdesigner-mcp/scripts/setup.sh | 11 +- 10 files changed, 1103 insertions(+), 35 deletions(-) create mode 100644 optional-skills/creative/touchdesigner-mcp/references/audio-reactive.md create mode 100644 optional-skills/creative/touchdesigner-mcp/references/geometry-comp.md create mode 100644 optional-skills/creative/touchdesigner-mcp/references/glsl.md create mode 100644 optional-skills/creative/touchdesigner-mcp/references/layout-compositor.md create mode 100644 optional-skills/creative/touchdesigner-mcp/references/operator-tips.md create mode 100644 optional-skills/creative/touchdesigner-mcp/references/postfx.md mode change 100644 => 100755 optional-skills/creative/touchdesigner-mcp/scripts/setup.sh diff --git a/optional-skills/creative/touchdesigner-mcp/SKILL.md b/optional-skills/creative/touchdesigner-mcp/SKILL.md index d0bd348af..8a2ca886c 100644 --- a/optional-skills/creative/touchdesigner-mcp/SKILL.md +++ b/optional-skills/creative/touchdesigner-mcp/SKILL.md @@ -1,8 +1,8 @@ --- name: touchdesigner-mcp description: "Control a running TouchDesigner instance via twozero MCP — create operators, set parameters, wire connections, execute Python, build real-time visuals. 36 native tools." -version: 1.0.0 -author: kshitijk4poor +version: 2.0.0 +author: Hermes Agent license: MIT metadata: hermes: @@ -36,7 +36,7 @@ Hub health check: `GET http://localhost:40404/mcp` returns JSON with instance PI Run the setup script to handle everything: ```bash -bash "${HERMES_HOME:-$HOME/.hermes}/skills/creative/touchdesigner-mcp/scripts/setup.sh" +bash ~/.hermes/skills/creative/touchdesigner-mcp/scripts/setup.sh ``` The script will: @@ -332,6 +332,12 @@ See `references/network-patterns.md` for complete build scripts + shader code. | `references/mcp-tools.md` | Full twozero MCP tool parameter schemas | | `references/python-api.md` | TD Python: op(), scripting, extensions | | `references/troubleshooting.md` | Connection diagnostics, debugging | +| `references/glsl.md` | GLSL uniforms, built-in functions, shader templates | +| `references/postfx.md` | Post-FX: bloom, CRT, chromatic aberration, feedback glow | +| `references/layout-compositor.md` | HUD layout patterns, panel grids, BSP-style layouts | +| `references/operator-tips.md` | Wireframe rendering, feedback TOP setup | +| `references/geometry-comp.md` | Geometry COMP: instancing, POP vs SOP, morphing | +| `references/audio-reactive.md` | Audio band extraction, beat detection, envelope following | | `scripts/setup.sh` | Automated setup script | --- diff --git a/optional-skills/creative/touchdesigner-mcp/references/audio-reactive.md b/optional-skills/creative/touchdesigner-mcp/references/audio-reactive.md new file mode 100644 index 000000000..74e756ccb --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/audio-reactive.md @@ -0,0 +1,175 @@ +# Audio-Reactive Reference + +Patterns for driving visuals from audio — spectrum analysis, beat detection, envelope following. + +## Audio Input + +```python +# Live input from audio interface +audio_in = root.create(audiodeviceinCHOP, 'audio_in') +audio_in.par.rate = 44100 + +# OR: from audio file (for testing) +audio_file = root.create(audiofileinCHOP, 'audio_in') +audio_file.par.file = '/path/to/track.wav' +audio_file.par.play = True +audio_file.par.repeat = 'on' # NOT par.loop +audio_file.par.playmode = 'locked' +``` + +--- + +## Audio Band Extraction (Verified TD 2025.32460) + +Use `audiofilterCHOP` for band separation (NOT `selectCHOP` by channel index): + +```python +# Audio input +af = root.create(audiofileinCHOP, 'audio_in') +af.par.file = path +af.par.play = True +af.par.repeat = 'on' +af.par.playmode = 'locked' + +# Low band: lowpass @ 250Hz +flt_low = root.create(audiofilterCHOP, 'flt_low') +flt_low.par.filter = 'lowpass' +flt_low.par.cutofffrequency = 250 +flt_low.par.rolloff = 2 +flt_low.inputConnectors[0].connect(af) + +# Mid band: highpass@250 → lowpass@4000 +flt_mid_hp = root.create(audiofilterCHOP, 'flt_mid_hp') +flt_mid_hp.par.filter = 'highpass' +flt_mid_hp.par.cutofffrequency = 250 +flt_mid_hp.par.rolloff = 2 +flt_mid_hp.inputConnectors[0].connect(af) + +flt_mid_lp = root.create(audiofilterCHOP, 'flt_mid_lp') +flt_mid_lp.par.filter = 'lowpass' +flt_mid_lp.par.cutofffrequency = 4000 +flt_mid_lp.par.rolloff = 2 +flt_mid_lp.inputConnectors[0].connect(flt_mid_hp) + +# High band: highpass @ 4000Hz +flt_high = root.create(audiofilterCHOP, 'flt_high') +flt_high.par.filter = 'highpass' +flt_high.par.cutofffrequency = 4000 +flt_high.par.rolloff = 2 +flt_high.inputConnectors[0].connect(af) + +# Per-band: RMS → lag → gain → clamp +for name, filt in [('low', flt_low), ('mid', flt_mid_lp), ('high', flt_high)]: + rms = root.create(analyzeCHOP, f'rms_{name}') + rms.par.function = 'rmspower' # NOT 'rms' + rms.inputConnectors[0].connect(filt) + + lag = root.create(lagCHOP, f'lag_{name}') + lag.par.lag1 = 0.05 # attack (NOT par.lagin) + lag.par.lag2 = 0.25 # release (NOT par.lagout) + lag.inputConnectors[0].connect(rms) + + math = root.create(mathCHOP, f'scale_{name}') + math.par.gain = 8.0 + math.inputConnectors[0].connect(lag) + + # mathCHOP has NO par.clamp — use limitCHOP + lim = root.create(limitCHOP, f'clamp_{name}') + lim.par.type = 'clamp' + lim.par.min = 0.0 + lim.par.max = 1.0 + lim.inputConnectors[0].connect(math) + + null = root.create(nullCHOP, f'out_{name}') + null.inputConnectors[0].connect(lim) + null.viewer = True +``` + +**Key TD 2025 corrections:** +- `analyzeCHOP.par.function = 'rmspower'` NOT `'rms'` +- `lagCHOP.par.lag1` / `par.lag2` NOT `par.lagin` / `par.lagout` +- `mathCHOP` has NO `par.clamp` — use separate `limitCHOP` + +--- + +## Beat / Onset Detection + +### Kick Detection (slope → trigger) + +```python +slope = root.create(slopeCHOP, 'kick_slope') +slope.inputConnectors[0].connect(op('out_low')) + +trig = root.create(triggerCHOP, 'kick_trig') +trig.par.threshold = 0.12 +trig.par.attack = 0.005 # NOT par.attacktime +trig.par.decay = 0.15 # NOT par.decaytime +trig.par.triggeron = 'increase' +trig.inputConnectors[0].connect(slope) + +kick_out = root.create(nullCHOP, 'out_kick') +kick_out.inputConnectors[0].connect(trig) +``` + +--- + +## Passing Audio to GLSL + +```python +glsl.par.vec0name = 'uLow' +glsl.par.vec0valuex.expr = "op('out_low')['chan1']" +glsl.par.vec0valuex.mode = ParMode.EXPRESSION + +glsl.par.vec1name = 'uKick' +glsl.par.vec1valuex.expr = "op('out_kick')['chan1']" +glsl.par.vec1valuex.mode = ParMode.EXPRESSION +``` + +```glsl +uniform float uLow; +uniform float uKick; +float scale = 1.0 + uKick * 0.4 + uLow * 0.2; +``` + +--- + +## Standard Audio Bus Pattern + +Recommended structure: + +``` +audiodeviceinCHOP (audio_in) + ↓ + [null_audio_in] + ├──→ audiofilterCHOP (lowpass@250) → analyzeCHOP → lagCHOP → mathCHOP → limitCHOP → null + ├──→ audiofilterCHOP (bandpass@250-4k) → analyzeCHOP → lagCHOP → mathCHOP → limitCHOP → null + ├──→ audiofilterCHOP (highpass@4k) → analyzeCHOP → lagCHOP → mathCHOP → limitCHOP → null + │ + └──→ slopeCHOP → triggerCHOP (beat_trigger) +``` + +Keep this entire bus inside a `baseCOMP` (e.g., `audio_bus`) and reference via paths from visual networks. + +--- + +## MIDI Input + +```python +midi_in = root.create(midiinCHOP, 'midi_in') +midi_in.par.device = 0 # Check midiinDAT for device index +# Outputs channels named by MIDI note/CC: 'ch1n60', 'ch1c74', etc. + +# Map CC to a parameter +op('bloom1').par.threshold.mode = ParMode.EXPRESSION +op('bloom1').par.threshold.expr = "op('midi_in')['ch1c74'][0]" +``` + +--- + +## CRITICAL: DO NOT use Lag CHOP for spectrum smoothing + +Lag CHOP in timeslice mode expands 256-sample spectrum to 1600-2400 samples, averaging all values to near-zero (~1e-06). The shader receives no usable data. Use `mathCHOP(gain=8)` directly, or smooth in GLSL via temporal lerp with a feedback texture. + +Verified: +- Without Lag CHOP: bass bins = 5.0-5.4 (strong, usable) +- With Lag CHOP: ALL bins = 0.000001 (dead) diff --git a/optional-skills/creative/touchdesigner-mcp/references/geometry-comp.md b/optional-skills/creative/touchdesigner-mcp/references/geometry-comp.md new file mode 100644 index 000000000..d4b165e74 --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/geometry-comp.md @@ -0,0 +1,121 @@ +# Geometry COMP Reference + +## Creating Geometry COMPs + +```python +geo = root.create(geometryCOMP, 'geo1') +# Remove default torus +for c in list(geo.children): + if c.valid: c.destroy() +# Build your shape inside +``` + +## Correct Pattern (shapes inside geo) + +```python +# Create shape INSIDE the geo COMP +box = geo.create(boxSOP, 'cube') +box.par.sizex = 1.5; box.par.sizey = 1.5; box.par.sizez = 1.5 + +# For POP-based geometry (TD 099), POPs must be inside: +sph = geo.create(spherePOP, 'shape') +out1 = geo.create(outPOP, 'out1') +out1.inputConnectors[0].connect(sph.outputConnectors[0]) +``` + +## DO NOT: Common Mistakes + +```python +# BAD: Don't create geometry at parent level and wire into COMP +box = root.create(boxPOP, 'box1') # ← outside geo, won't render + +# BAD: Don't reference parent operators from inside COMP +choptopop1.par.chop = '../null1' # ← hidden dependency, breaks on move +``` + +## Instancing + +```python +geo.par.instancing = True +geo.par.instanceop = 'sopto1' # relative path to CHOP/SOP with instance data +geo.par.instancetx = 'tx' +geo.par.instancety = 'ty' +geo.par.instancetz = 'tz' +``` + +### Instance Attribute Names by OP Type + +| OP Type | Attribute Names | +|---------|-----------------| +| CHOP | Channel names: `tx`, `ty`, `tz` | +| SOP/POP | `P(0)`, `P(1)`, `P(2)` for position | +| DAT | Column header names from first row | +| TOP | `r`, `g`, `b`, `a` | + +### Mixed Data Sources + +```python +geo.par.instanceop = 'pos_chop' # Position from CHOP +geo.par.instancetx = 'tx' +geo.par.instancecolorop = 'color_top' # Color from TOP +geo.par.instancecolorr = 'r' +``` + +## Rendering Setup + +```python +# Camera +cam = root.create(cameraCOMP, 'cam1') +cam.par.tx = 0; cam.par.ty = 0; cam.par.tz = 4 + +# Render TOP +render = root.create(renderTOP, 'render1') +render.par.outputresolution = 'custom' +render.par.resolutionw = 1280; render.par.resolutionh = 720 +render.par.camera = cam.path +render.par.geometry = geo.path # accepts path string +``` + +## POPs vs SOPs for Rendering + +In TD 099, `geometryCOMP` renders **POPs** but NOT SOPs. A `boxSOP` inside a geometry COMP is invisible — no errors. + +```python +# WRONG — SOPs don't render (invisible, no errors) +box = geo.create(boxSOP, 'cube') # ✗ invisible + +# CORRECT — POPs render +box = geo.create(boxPOP, 'cube') # ✓ visible +``` + +| SOP | POP | Notes | +|-----|-----|-------| +| `boxSOP` | `boxPOP` | `sizex/y/z`, `surftype` | +| `sphereSOP` | `spherePOP` | `radx/y/z`, `freq`, `type` (geodesic/grid/sharedpoles/tetrahedron) | +| `torusSOP` | `torusPOP` | TD auto-creates in new geo COMPs | +| `circleSOP` | `circlePOP` | | +| `gridSOP` | `gridPOP` | | +| `tubeSOP` | `tubePOP` | | + +New geometry COMPs auto-create: `in1` (inPOP), `out1` (outPOP), `torus1` (torusPOP). Always clean before building. + +## Morphing Between Shapes (switchPOP) + +```python +sw = geo.create(switchPOP, 'shape_switch') +sw.par.index.expr = 'int(absTime.seconds / 3) % 4' +sw.inputConnectors[0].connect(tetra.outputConnectors[0]) # shape 0 +sw.inputConnectors[1].connect(box.outputConnectors[0]) # shape 1 +sw.inputConnectors[2].connect(octa.outputConnectors[0]) # shape 2 +sw.inputConnectors[3].connect(sphere.outputConnectors[0]) # shape 3 + +out = geo.create(outPOP, 'out1') +out.inputConnectors[0].connect(sw.outputConnectors[0]) +``` + +`spherePOP.par.type` options: `geodesic`, `grid`, `sharedpoles`, `tetrahedron`. Use `tetrahedron` for platonic solid polyhedra. + +## Misc + +- `connect()` replaces existing connections — no need to disconnect first +- `project.name` returns the TOE filename, `project.folder` returns the directory diff --git a/optional-skills/creative/touchdesigner-mcp/references/glsl.md b/optional-skills/creative/touchdesigner-mcp/references/glsl.md new file mode 100644 index 000000000..97c2dea80 --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/glsl.md @@ -0,0 +1,151 @@ +# GLSL Reference + +## Uniforms + +``` +TouchDesigner GLSL +───────────────────────────── +vec0name = 'uTime' → uniform float uTime; +vec0valuex = 1.0 → uTime value +``` + +### Pass Time + +```python +glsl_op.par.vec0name = 'uTime' +glsl_op.par.vec0valuex.mode = ParMode.EXPRESSION +glsl_op.par.vec0valuex.expr = 'absTime.seconds' +``` + +```glsl +uniform float uTime; +void main() { float t = uTime * 0.5; } +``` + +### Built-in Uniforms (TOP) + +```glsl +// Output resolution (always available) +vec2 res = uTDOutputInfo.res.zw; + +// Input texture (only when inputs connected) +vec2 inputRes = uTD2DInfos[0].res.zw; +vec4 color = texture(sTD2DInputs[0], vUV.st); + +// UV coordinates +vUV.st // 0-1 texture coords +``` + +**IMPORTANT:** `uTD2DInfos` requires input textures. For standalone shaders use `uTDOutputInfo`. + +## Built-in Utility Functions + +```glsl +// Noise +float TDPerlinNoise(vec2/vec3/vec4 v); +float TDSimplexNoise(vec2/vec3/vec4 v); + +// Color conversion +vec3 TDHSVToRGB(vec3 c); +vec3 TDRGBToHSV(vec3 c); + +// Matrix transforms +mat4 TDTranslate(float x, float y, float z); +mat3 TDRotateX/Y/Z(float radians); +mat3 TDRotateOnAxis(float radians, vec3 axis); +mat3 TDScale(float x, float y, float z); +mat3 TDRotateToVector(vec3 forward, vec3 up); +mat3 TDCreateRotMatrix(vec3 from, vec3 to); // vectors must be normalized + +// Resolution struct +struct TDTexInfo { + vec4 res; // (1/width, 1/height, width, height) + vec4 depth; +}; + +// Output (always use this — handles sRGB correctly) +fragColor = TDOutputSwizzle(color); + +// Instancing (MAT only) +int TDInstanceID(); +``` + +## glslTOP + +Docked DATs created automatically: +- `glsl1_pixel` — Pixel shader +- `glsl1_compute` — Compute shader +- `glsl1_info` — Compile info + +### Pixel Shader Template + +```glsl +out vec4 fragColor; +void main() { + vec4 color = texture(sTD2DInputs[0], vUV.st); + fragColor = TDOutputSwizzle(color); +} +``` + +### Compute Shader Template + +```glsl +layout (local_size_x = 8, local_size_y = 8) in; +void main() { + vec4 color = texelFetch(sTD2DInputs[0], ivec2(gl_GlobalInvocationID.xy), 0); + TDImageStoreOutput(0, gl_GlobalInvocationID, color); +} +``` + +### Update Shader + +```python +op('/project1/glsl1_pixel').text = shader_code +op('/project1/glsl1').cook(force=True) +# Check errors: +print(op('/project1/glsl1_info').text) +``` + +## glslMAT + +Docked DATs: +- `glslmat1_vertex` — Vertex shader (param: `vdat`) +- `glslmat1_pixel` — Pixel shader (param: `pdat`) +- `glslmat1_info` — Compile info + +Note: MAT uses `vdat`/`pdat`, TOP uses `vertexdat`/`pixeldat`. + +### Vertex Shader Template + +```glsl +uniform float uTime; +void main() { + vec3 pos = TDPos(); + pos.z += sin(pos.x * 3.0 + uTime) * 0.2; + vec4 worldSpacePos = TDDeform(pos); + gl_Position = TDWorldToProj(worldSpacePos); +} +``` + +## Bayer 8x8 Dither Matrix + +Reusable ordered dither function for retro/print aesthetics: + +```glsl +float bayer8(vec2 pos) { + int x = int(mod(pos.x, 8.0)), y = int(mod(pos.y, 8.0)), idx = x + y * 8; + int b[64] = int[64]( + 0,32,8,40,2,34,10,42,48,16,56,24,50,18,58,26, + 12,44,4,36,14,46,6,38,60,28,52,20,62,30,54,22, + 3,35,11,43,1,33,9,41,51,19,59,27,49,17,57,25, + 15,47,7,39,13,45,5,37,63,31,55,23,61,29,53,21 + ); + return float(b[idx]) / 64.0; +} +``` + +## glslPOP / glsladvancedPOP / glslcopyPOP + +All use compute shaders. Docked DATs follow naming convention: +- `glsl1_compute` / `glsladv1_compute` +- `glslcopy1_ptCompute` / `glslcopy1_vertCompute` / `glslcopy1_primCompute` diff --git a/optional-skills/creative/touchdesigner-mcp/references/layout-compositor.md b/optional-skills/creative/touchdesigner-mcp/references/layout-compositor.md new file mode 100644 index 000000000..b9498f1fe --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/layout-compositor.md @@ -0,0 +1,131 @@ +# Layout Compositor Reference + +Patterns for building modular multi-panel grids — useful for HUD interfaces, data dashboards, and multi-source visual composites. + +## Layout Approaches + +| Approach | Best For | Notes | +|----------|----------|-------| +| `layoutTOP` | Fixed grid, quick setup | GPU, simple tiling | +| Container COMP + `overTOP` | Full control, mixed-size panels | More setup, very flexible | +| GLSL compositor | Procedural / BSP-style | Most powerful, more complex | + +--- + +## layoutTOP + +Built-in grid compositor — fastest path for uniform tile grids. + +```python +layout = root.create(layoutTOP, 'layout1') +layout.par.resolutionw = 1920 +layout.par.resolutionh = 1080 +layout.par.cols = 3 +layout.par.rows = 2 +layout.par.gap = 4 +``` + +Connect inputs (up to cols×rows): +```python +layout.inputConnectors[0].connect(op('panel_radar')) +layout.inputConnectors[1].connect(op('panel_wave')) +layout.inputConnectors[2].connect(op('panel_data')) +``` + +**Variable-width columns:** Not directly supported. Use overTOP approach for non-uniform grids. + +--- + +## Container COMP Grid + +Build each element as its own `containerCOMP`. Compose with `overTOP`: + +```python +def create_panel(root, name, width, height, x=0, y=0): + panel = root.create(containerCOMP, name) + panel.par.w = width + panel.par.h = height + panel.viewer = True + return panel + +# Composite with overTOP chain +over1 = root.create(overTOP, 'over1') +over1.inputConnectors[0].connect(panel_radar) +over1.inputConnectors[1].connect(panel_wave) +over1.par.topx2 = 0 +over1.par.topy2 = 512 +``` + +**Tip:** Use a `resolutionTOP` before each `overTOP` input if panels are different sizes. + +--- + +## Panel Dividers (GLSL) + +```glsl +out vec4 fragColor; +uniform vec2 uGridDivisions; // e.g. vec2(3, 2) for 3 cols, 2 rows +uniform float uLineWidth; // pixels +uniform vec4 uLineColor; // e.g. vec4(0.0, 1.0, 0.8, 0.6) for cyan + +void main() { + vec2 res = uTDOutputInfo.res.zw; + vec2 uv = vUV.st; + vec4 bg = texture(sTD2DInputs[0], uv); + + float lineW = uLineWidth / res.x; + float lineH = uLineWidth / res.y; + + float vDiv = 0.0; + for (float i = 1.0; i < uGridDivisions.x; i++) { + float x = i / uGridDivisions.x; + vDiv = max(vDiv, step(abs(uv.x - x), lineW)); + } + + float hDiv = 0.0; + for (float i = 1.0; i < uGridDivisions.y; i++) { + float y = i / uGridDivisions.y; + hDiv = max(hDiv, step(abs(uv.y - y), lineH)); + } + + float line = max(vDiv, hDiv); + vec4 result = mix(bg, uLineColor, line * uLineColor.a); + fragColor = TDOutputSwizzle(result); +} +``` + +--- + +## Element Library Pattern + +Each visual element lives in its own `baseCOMP` as a reusable `.tox`: + +### Standard Interface +``` +inputs: + - in_audio (CHOP) — audio envelope / beat data + - in_data (CHOP) — optional data stream + - in_control (CHOP) — intensity, color, speed params + +outputs: + - out_top (TOP) — rendered element +``` + +### Network Structure +``` +/project1/ + audio_bus/ ← all audio analysis (see audio-reactive.md) + elements/ + elem_radar/ ← baseCOMP with out_top + elem_wave/ + elem_data/ + compositor/ + layout1 ← layoutTOP or overTOP chain + dividers1 ← GLSL divider lines + postfx/ ← bloom → chrom → CRT stack (see postfx.md) + null_out ← final output + output/ + windowCOMP ← full-screen output +``` + +**Key principle:** Elements don't know about each other. The compositor assembles them. Audio bus is referenced by all elements but lives separately. diff --git a/optional-skills/creative/touchdesigner-mcp/references/operator-tips.md b/optional-skills/creative/touchdesigner-mcp/references/operator-tips.md new file mode 100644 index 000000000..0e0f077cf --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/operator-tips.md @@ -0,0 +1,106 @@ +# Operator Tips + +## Wireframe Rendering Pattern + +Reusable setup for wireframe geometry on black background: + +```python +# 1. Material +mat = root.create(wireframeMAT, 'wire_mat') +mat.par.colorr = 1.0; mat.par.colorg = 0.0; mat.par.colorb = 0.0 +mat.par.linewidth = 3 + +# 2. Geometry COMP +geo = root.create(geometryCOMP, 'my_geo') +geo.par.rx.expr = 'absTime.seconds * 30' +geo.par.ry.expr = 'absTime.seconds * 45' +geo.par.material = mat.path # NOTE: 'material' not 'mat' + +# 3. Shape inside the geo +box = geo.create(boxSOP, 'cube') +box.par.sizex = 1.5; box.par.sizey = 1.5; box.par.sizez = 1.5 + +# 4. Camera +cam = root.create(cameraCOMP, 'cam1') +cam.par.tx = 0; cam.par.ty = 0; cam.par.tz = 4; cam.par.fov = 45 + +# 5. Render TOP +render = root.create(renderTOP, 'render1') +render.par.outputresolution = 'custom' +render.par.resolutionw = 1280; render.par.resolutionh = 720 +render.par.bgcolorr = 0; render.par.bgcolorg = 0; render.par.bgcolorb = 0 +render.par.camera = cam.path +render.par.geometry = geo.path + +# 6. Output null +out = root.create(nullTOP, 'out1') +out.inputConnectors[0].connect(render.outputConnectors[0]) +``` + +**Key rules:** +- Class names: `wireframeMAT` not `wireframeMat` (all-caps suffix) +- Geometry SOPs/POPs go INSIDE the geo comp +- Material: `geo.par.material` not `geo.par.mat` +- Render geometry: `render.par.geometry = geo.path` (string path) +- `wireframeMAT.par.wireframemode = 'topology'` for clean wireframe (vs `'tesselated'` for triangle edges) +- Alternative: Use `renderTOP.par.overridemat` instead of per-geo material + +## Feedback TOP + +### Basic Structure + +``` +input (initial state) ──┐ + ├──→ feedback_top ──→ processing ──→ null_out + │ ↑ + └── par.top = 'null_out' ────────────────┘ +``` + +### Setup Pattern + +```python +# 1. Processing chain +glsl = root.create(glslTOP, 'sim') +null_out = root.create(nullTOP, 'null_out') +glsl.outputConnectors[0].connect(null_out.inputConnectors[0]) + +# 2. Feedback referencing null_out +feedback = root.create(feedbackTOP, 'feedback') +feedback.par.top = 'null_out' + +# 3. Black initial state +const_init = root.create(constantTOP, 'const_init') +const_init.par.colorr = 0; const_init.par.colorg = 0; const_init.par.colorb = 0 + +# 4. Wire: initial → feedback, feedback → processing +feedback.inputConnectors[0].connect(const_init) +glsl.inputConnectors[0].connect(feedback) + +# 5. Reset to apply initial state +feedback.par.resetpulse.pulse() +``` + +### Common Errors + +| Error | Cause | Solution | +|-------|-------|----------| +| "Not enough sources specified" | No input connected | Connect initial state TOP | +| Unexpected initial pattern | Wrong initial state | Use Constant TOP (black) | + +### Tips + +1. Use float format for simulations: `glsl.par.format = 'rgba32float'` +2. Reset after setup: `feedback.par.resetpulse.pulse()` +3. Match resolutions — feedback, processing, and initial state must match +4. Soft boundary prevents edge artifacts: + ```glsl + float edge = 3.0 * texel.x; + float bx = smoothstep(0.0, edge, uv.x) * smoothstep(0.0, edge, 1.0 - uv.x); + float by = smoothstep(0.0, edge, uv.y) * smoothstep(0.0, edge, 1.0 - uv.y); + value *= bx * by; + ``` + +### Use Cases +- **Wave Simulation** — R=height, G=velocity, black initial state +- **Cellular Automata** — white=alive, black=dead, random noise initial state +- **Trail / Motion Blur** — blend current frame with feedback, black initial diff --git a/optional-skills/creative/touchdesigner-mcp/references/pitfalls.md b/optional-skills/creative/touchdesigner-mcp/references/pitfalls.md index 33c9b5f4d..7d1e322a4 100644 --- a/optional-skills/creative/touchdesigner-mcp/references/pitfalls.md +++ b/optional-skills/creative/touchdesigner-mcp/references/pitfalls.md @@ -143,20 +143,20 @@ Creating nodes with the same names you just destroyed in the SAME script causes ```python # td_execute_python: for c in list(root.children): - if c.valid and c.name.startswith('promo_'): + if c.valid and c.name.startswith('my_'): c.destroy() -# ... then create promo_audio, promo_shader etc. in same script → CRASHES +# ... then create my_audio, my_shader etc. in same script → CRASHES ``` **CORRECT (two separate calls):** ```python # Call 1: td_execute_python — clean only for c in list(root.children): - if c.valid and c.name.startswith('promo_'): + if c.valid and c.name.startswith('my_'): c.destroy() # Call 2: td_execute_python — build (separate MCP call) -audio = root.create(audiofileinCHOP, 'promo_audio') +audio = root.create(audiofileinCHOP, 'my_audio') # ... rest of build ``` @@ -361,21 +361,13 @@ win.par.winopen.pulse() `out.sample(x, y)` returns pixels from a single cook snapshot. Compare samples with 2+ second delays, or use screencapture on the display window. -### 32. Audio-reactive GLSL: dual-layer sync pipeline +### 32. Audio-reactive GLSL: TD-side pipeline -For audio-synced visuals, use BOTH layers for maximum effect: - -**Layer 1 (TD-side, real-time):** AudioFileIn → AudioSpectrum(timeslice=True, fftsize='256') → Math(gain=5) → choptoTOP(par.chop=math, layout='rowscropped') → GLSL input. The shader samples `sTD2DInputs[1]` at different x positions for bass/mid/hi. Record the TD output with MovieFileOut. - -**Layer 2 (Python-side, post-hoc):** scipy FFT on the SAME audio file → per-frame features (rms, bass, mid, hi, beat detection) → drive ASCII brightness, chromatic aberration, beat flashes during the render pass. - -Both layers locked to the same audio file = visuals genuinely sync to the beat at two independent stages. +For audio-synced visuals: AudioFileIn → AudioSpectrum(timeslice=True, fftsize='256') → Math(gain=5) → choptoTOP(par.chop=math, layout='rowscropped') → GLSL input. The shader samples `sTD2DInputs[1]` at different x positions for bass/mid/hi. Record the TD output with MovieFileOut. **Key gotcha:** AudioFileIn must be cued (`par.cue=True` → `par.cuepulse.pulse()`) then uncued (`par.cue=False`, `par.play=True`) before recording starts. Otherwise the spectrum is silent for the first few seconds. -### 33. twozero MCP: benchmark and prefer native tools - -Benchmarked April 2026: twozero MCP with 36 native tools. The old curl/REST method (port 9981) had zero native tools. +### 33. twozero MCP: prefer native tools **Always prefer native MCP tools over td_execute_python:** - `td_create_operator` over `root.create()` scripts (handles viewport positioning) @@ -425,13 +417,16 @@ TD can show `fps:0` in `td_get_perf` while ops still cook and `TOP.save()` still **a) Project is paused (playbar stopped).** TD's playbar can be toggled with spacebar. The `root` at `/` has no `.playbar` attribute (it's on the perform COMP). The easiest fix is sending a spacebar keypress via `td_input_execute`, though this tool can sometimes error. As a workaround, `TOP.save()` always works regardless of play state — use it to verify rendering is actually happening before spending time debugging FPS. -**b) Audio device CHOP blocking the main thread.** An `audiooutCHOP` with an active audio device can consume 300-400ms/s (2000%+ of frame budget), stalling the cook loop at FPS=0. Fix: keep the CHOP active but set `volume=0` to prevent the audio driver from blocking. Disabling it entirely (`active=False`) may also work but can prevent downstream audio processing CHOPs from cooking. +**b) Audio device CHOP blocking the main thread (MOST COMMON).** An `audiodeviceoutCHOP` with `active=True` can consume 300-400ms/s (2000%+ of frame budget), stalling the cook loop at FPS=0. **`volume=0` is NOT sufficient** — the audio driver still blocks. Fix: `par.active = False`. This completely stops the CHOP from interacting with the audio driver. If you need audio monitoring, enable it only during short playback checks, then disable before recording. + +Verified April 2026: disabling `audiodeviceoutCHOP` (`active=False`) restored FPS from 0 to 60 instantly, recovering from 2348% budget usage to 0.1%. Diagnostic sequence when FPS=0: -1. `td_get_perf` — check if any op has extreme CPU/s -2. `TOP.save()` on the output — if it produces a valid image, the pipeline works, just not at real-time rate -3. Check for blocking CHOPs (audioout, audiodevin, etc.) -4. Toggle play state (spacebar, or check if absTime.seconds is advancing) +1. `td_get_perf` — check if any op has extreme CPU/s (audiodeviceoutCHOP is the usual suspect) +2. If audiodeviceoutCHOP shows >100ms/s: set `par.active = False` immediately +3. `TOP.save()` on the output — if it produces a valid image, the pipeline works, just not at real-time rate +4. Check for other blocking CHOPs (audiodevin, etc.) +5. Toggle play state (spacebar, or check if absTime.seconds is advancing) ### 39. Recording while FPS=0 produces empty or near-empty files @@ -484,9 +479,20 @@ If `td_write_dat` fails, fall back to `td_execute_python`: op("/project1/shader_code").text = shader_string ``` -### 42. td_execute_python does NOT return stdout or print() output +### 42. td_execute_python DOES return print() output — use it for debugging -Despite what earlier versions of pitfall #33 stated, `print()` and `debug()` output from `td_execute_python` scripts does NOT appear in the MCP response. The response is always just `(ok)` + FPS/error summary. To read values back, use dedicated inspection tools (`td_get_operator_info`, `td_read_dat`, `td_read_chop`) instead of trying to print from within a script. +`print()` statements in `td_execute_python` scripts appear in the MCP response text. This is the correct way to read values back from scripts. The response format is: printed output first, then `[fps X.X/X] [N err/N warn]` on a separate line. + +However, the `result` variable (if you set one) does NOT appear verbatim — use `print()` for anything you need to read back: +```python +# CORRECT — appears in response: +print('value:', some_value) + +# WRONG — not reliably in response: +result = some_value +``` + +For structured data, use dedicated inspection tools (`td_get_operator_info`, `td_read_chop`) which return clean JSON. ### 43. td_get_operator_info JSON is appended with `[fps X.X/X]` — breaks json.loads() @@ -496,13 +502,203 @@ clean = response_text.rsplit('[fps', 1)[0] data = json.loads(clean) ``` -### 44. td_get_screenshot is asynchronous — returns `{"status": "pending"}` +### 44. td_get_screenshot is unreliable — returns `{"status": "pending"}` and may never deliver -Screenshots don't complete instantly. The tool returns `{"status": "pending", "requestId": "..."}` and the actual file appears later. Wait a few seconds before checking for the file. There is no callback or completion notification — poll the filesystem. +Screenshots don't complete instantly. The tool returns `{"status": "pending", "requestId": "..."}` and the actual file may appear later — or may NEVER appear at all. In testing (April 2026), screenshots stayed "pending" indefinitely with no file written to disk, even though the shader was cooking at 8-30fps. -### 45. Recording duration is manual — no auto-stop at audio end +**Do NOT rely on `td_get_screenshot` for frame capture.** For reliable frame capture, use MovieFileOut recording + ffmpeg frame extraction: +```bash +# Record in TD first, then extract frames: +ffmpeg -y -i /tmp/td_output.mov -t 25 -vf 'fps=24' /tmp/td_frames/frame_%06d.png +``` + +If you need a quick visual check, `td_get_screenshot` is worth trying (it sometimes works), but always have the recording fallback. There is no callback or completion notification — if the file doesn't appear after 5-10 seconds, it's not coming. + +### 45. Heavy shaders cook below record FPS — many duplicate frames in output + +A raymarched GLSL shader may only cook at 8-15fps even though MovieFileOut records at 60fps. The recording still works (TD writes the last-cooked frame each time), but the resulting file has many duplicate frames. When extracting frames for post-processing, use a lower fps filter to avoid redundant frames: +```bash +# Extract at 24fps from a 60fps recording of an 8fps shader: +ffmpeg -y -i /tmp/td_output.mov -t 25 -vf 'fps=24' /tmp/td_frames/frame_%06d.png +``` +Check actual cook FPS with `td_get_perf` before committing to a long recording. If FPS < 15, the output will be a slideshow regardless of the recording codec. + +### 46. Recording duration is manual — no auto-stop at audio end MovieFileOut records until `par.record = False` is set. If audio ends before you stop recording, the file keeps growing with repeated frames. Always stop recording promptly after the audio duration. For precision: set a timer on the agent side matching the audio length, then send `par.record = False`. Trim excess with ffmpeg as a safety net: ```bash ffmpeg -i raw.mov -t 25 -c copy trimmed.mov +``` + +### 47. AudioFileIn par.index stays at 0 in sequential mode — not a reliable progress indicator + +When `audiofileinCHOP` is in `playmode=2` (sequential), `par.index.eval()` returns 0.0 even while audio IS actively playing and the spectrum IS receiving data. Do NOT use `par.index` to check playback progress in sequential mode. + +**How to verify audio is actually playing:** +- Read the spectrum CHOP values via `td_read_chop` — if values are non-zero and CHANGE between reads 1-2s apart, audio is flowing +- Read the audio CHOP itself: non-zero waveform samples confirm the file is loaded and playing +- `par.play.eval()` returning True is necessary but NOT sufficient — it can be True with no audio flowing if cue is stuck + +### 48. GLSL shader whiteout — clamp audio spectrum values in the shader + +Raw spectrum values multiplied by Math CHOP gain can produce very large numbers (5-20+) that blow out the shader's lighting, producing flat white/grey. The shader MUST clamp audio inputs: + +```glsl +float bass = texture(sTD2DInputs[1], vec2(0.05, 0.25)).r; +bass = clamp(bass, 0.0, 3.0); // prevent whiteout +mids = clamp(mids, 0.0, 3.0); +hi = clamp(hi, 0.0, 3.0); +``` + +Discovered when gain=10 produced ~0.13 (too dark) during quiet passages but gain=50 produced ~9.4 (total whiteout). Fix: keep gain=10, use `highfreqboost=3.0` on AudioSpectrum, clamp in shader. + +### 49. Non-Commercial TD records at 1280x1280 (square) — always crop in post + +Even with `resolutionw=1280, resolutionh=720` on the GLSL TOP, Non-Commercial TD may output 1280x1280 to MovieFileOut. Always check dimensions with ffprobe and crop during extraction: + +```bash +# Center-crop from 1280x1280 to 1280x720: +ffmpeg -y -i /tmp/td_output.mov -t 25 -r 24 -vf "crop=1280:720:0:280" /tmp/frames/frame_%06d.png +``` + +Large ProRes files (1-2GB) at 1280x1280 decode at ~3fps, so 25s of footage takes ~3 minutes to extract. + +## Advanced Patterns (pitfalls 51+) + +### 51. Connection syntax: use `outputConnectors`/`inputConnectors`, NOT `outputs`/`inputs` + +```python +# CORRECT +src.outputConnectors[0].connect(dst.inputConnectors[0]) +# WRONG — raises IndexError or AttributeError +src.outputs[0].connect(dst.inputs[0]) +``` + +For feedback TOP, BOTH are required: +```python +fb.par.top = target.path +target.outputConnectors[0].connect(fb.inputConnectors[0]) +``` + +### 52. moviefileoutTOP `par.input` doesn't resolve via Python in TD 2025.32460 + +Setting `moviefileoutTOP.par.input` programmatically does NOT work. All forms fail silently with "Not enough sources specified." + +**Workaround — frame capture + ffmpeg:** +```python +out = op('/project1/out') +for i in range(300): + delay = i * 5 + run(f"op('/project1/out').save('/tmp/frames/f_{i:04d}.png')", delayFrames=delay) +# Then: ffmpeg -y -framerate 30 -i /tmp/frames/f_%04d.png -c:v prores -pix_fmt yuv420p /tmp/output.mov +``` + +### 53. Batch frame capture — use `me.fetch`/`me.store` for state across calls + +```python +start = me.fetch('cap_frame', 0) +for i in range(60): + frame = start + i + op('/project1/out').save(f'/tmp/frames/frame_{str(frame).zfill(4)}.png') +me.store('cap_frame', start + 60) +``` +Call 5 times for 300 frames. Each picks up where the last left off. + +### 54. GLSL TOP pixel shader requirements in TD 2025 + +```glsl +// REQUIRED — declare output +layout(location = 0) out vec4 fragColor; + +void main() { + vec3 col = vec3(1.0, 0.0, 0.0); + fragColor = TDOutputSwizzle(vec4(col, 1.0)); +} +``` +**Built-in uniforms available:** `uTDOutputInfo.res` (vec4), `uTDTimeInfo.seconds`, `sTD2DInputs[N]`. +**Auto-created DATs:** `name_pixel`, `name_vertex`, `name_compute` textDATs with example code. + +### 55. TOP.save() doesn't advance time — identical frames in tight loops + +`.save()` captures the current cooked frame without advancing TD's timeline: +```python +# WRONG — all frames identical +for i in range(300): + op('/project1/out').save(f'frames/f_{i:04d}.png') + +# CORRECT — use run() with delayFrames +for i in range(300): + delay = i * 5 + run(f"op('/project1/out').save('frames/f_{i:04d}.png')", delayFrames=delay) +``` +**NEVER use `time.sleep()` in TD** — it blocks the main thread and freezes the UI. + +### 56. Feedback loop masks input changes — force switch during capture + +With feedback TOP opacity 0.7+, the buffer dominates output. Switching input produces nearly identical frames. + +**Fix — force switch index per capture:** +```python +for i in range(300): + idx = (i // 8) % num_inputs + delay = i * 5 + run(f"op('/project1/vswitch').par.index={idx}; op('/project1/out').save('f_{i:04d}.png')", delayFrames=delay) +``` + +### 57. Large td_execute_python scripts fail — split into incremental calls + +10+ operator creations in one script cause timing issues. Split into 2-4 calls of 2-4 operators each. Within one call, `create()` handles work immediately. Across calls, `op('name')` may return `None` if the previous call hasn't committed. + +### 58. MCP instance reconnection after project.load() + +`project.load(path)` changes the PID. After loading, call `td_list_instances()` and use the new `target_instance`. For TOX files: import as child comp instead (doesn't disconnect). + +### 59. TOX reverse-engineering workflow + +```python +comp = root.loadTox(r'/path/to/file.tox') +comp.name = '_study_comp' +for child in comp.children: + print(f'{child.name} ({child.OPType})') +# Use td_get_operators_info, td_read_dat, check custom params +``` + +### 60. sliderCOMP naming — TD appends suffix + +TD auto-renames: `slider_brightness` → `slider_brightness1`. Always check names after creation. + +### 61. create() requires full operator type suffix + +```python +# CORRECT +proj.create('audiofileinCHOP', 'audio_in') +proj.create('glslTOP', 'render') + +# WRONG — raises "Unknown operator type" +proj.create('audiofilein', 'audio_in') +proj.create('glsl', 'render') +``` + +### 62. Reparenting COMPs — use copyOPs, not connect() + +Moving COMPs with `inputCOMPConnectors[0].connect()` fails. Use copy + destroy: +```python +copied = target.copyOPs([source]) # preserves internal wiring +source.destroy() +# Re-wire external connections manually after the move +``` + +### 63. Slider wiring — expressionCHOP with op() expressions crashes TD + +```python +# CRASHES TD — don't do this +echop = root.create(expressionCHOP, 'slider_ctrl') +echop.par.chan0expr = 'op("/project1/controls/slider_brightness1").par.value0' + +# WORKING — parameterCHOP as bridge +pchop = root.create(parameterCHOP, 'slider_vals') +pchop.par.ops = '/project1/controls' +pchop.par.parameters = 'value0' +pchop.par.custom = True +pchop.par.builtin = False ``` \ No newline at end of file diff --git a/optional-skills/creative/touchdesigner-mcp/references/postfx.md b/optional-skills/creative/touchdesigner-mcp/references/postfx.md new file mode 100644 index 000000000..6ff7b08f7 --- /dev/null +++ b/optional-skills/creative/touchdesigner-mcp/references/postfx.md @@ -0,0 +1,183 @@ +# Post-FX Reference + +Bloom, CRT scanlines, chromatic aberration, and feedback glow patterns for live visual work. + +--- + +## Bloom + +### Built-in Bloom TOP + +TD's `bloomTOP` is the fastest path — GPU-accelerated, no shader needed. + +```python +bloom = root.create(bloomTOP, 'bloom1') +bloom.par.threshold = 0.6 # Luminance threshold (0-1) +bloom.par.size = 0.03 # Spread radius (0-1) +bloom.par.strength = 1.5 # Bloom intensity +bloom.par.blendmode = 'add' # 'add' or 'screen' +``` + +**Audio reactive bloom:** +```python +bloom.par.strength.mode = ParMode.EXPRESSION +bloom.par.strength.expr = "op('audio_env')['envelope'][0] * 3.0 + 0.5" +``` + +### GLSL Bloom (More Control) + +For multi-pass bloom with color tinting: + +```glsl +// bloom_pixel.glsl — pass1: threshold + tint +out vec4 fragColor; +uniform float uThreshold; +uniform vec3 uBloomColor; + +void main() { + vec4 col = texture(sTD2DInputs[0], vUV.st); + float luma = dot(col.rgb, vec3(0.299, 0.587, 0.114)); + float bloom = max(0.0, luma - uThreshold); + fragColor = TDOutputSwizzle(vec4(col.rgb * bloom * uBloomColor, col.a)); +} +``` + +Then blur with `blurTOP` (size ~0.02-0.05), composite back over source with `addTOP` or `compositeTOP` in Add mode. + +--- + +## CRT / Scanlines + +Pure GLSL — create a `glslTOP` and paste into its `_pixel` DAT. + +```glsl +// crt_pixel.glsl +out vec4 fragColor; +uniform float uTime; +uniform float uScanlineIntensity; // 0.0 - 1.0, default 0.4 +uniform float uCurvature; // 0.0 - 0.15, default 0.05 +uniform float uVignette; // 0.0 - 1.0, default 0.8 + +vec2 curveUV(vec2 uv, float amount) { + uv = uv * 2.0 - 1.0; + vec2 offset = abs(uv.yx) / vec2(6.0, 4.0); + uv = uv + uv * offset * offset * amount; + return uv * 0.5 + 0.5; +} + +void main() { + vec2 res = uTDOutputInfo.res.zw; + vec2 uv = vUV.st; + + // CRT barrel distortion + uv = curveUV(uv, uCurvature * 10.0); + + // Kill pixels outside curved screen + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + vec4 col = texture(sTD2DInputs[0], uv); + + // Scanlines + float scanline = sin(uv.y * res.y * 3.14159) * 0.5 + 0.5; + col.rgb *= mix(1.0, scanline, uScanlineIntensity); + + // Horizontal noise flicker + float flicker = TDSimplexNoise(vec2(uv.y * 100.0, uTime * 8.0)) * 0.03; + col.rgb += flicker; + + // Vignette + vec2 vig = uv * (1.0 - uv.yx); + float v = pow(vig.x * vig.y * 15.0, uVignette); + col.rgb *= v; + + fragColor = TDOutputSwizzle(col); +} +``` + +--- + +## Chromatic Aberration + +Splits RGB channels and offsets them along screen axes. + +```glsl +out vec4 fragColor; +uniform float uAmount; // 0.001 - 0.02, default 0.006 + +void main() { + vec2 uv = vUV.st; + vec2 dir = uv - 0.5; + + float r = texture(sTD2DInputs[0], uv + dir * uAmount).r; + float g = texture(sTD2DInputs[0], uv).g; + float b = texture(sTD2DInputs[0], uv - dir * uAmount).b; + float a = texture(sTD2DInputs[0], uv).a; + + fragColor = TDOutputSwizzle(vec4(r, g, b, a)); +} +``` + +**Audio-reactive variant** — spike aberration on beats: +```glsl +uniform float uBeat; +void main() { + vec2 uv = vUV.st; + vec2 dir = uv - 0.5; + float amount = uAmount + uBeat * 0.04; + float r = texture(sTD2DInputs[0], uv + dir * amount * 1.2).r; + float g = texture(sTD2DInputs[0], uv).g; + float b = texture(sTD2DInputs[0], uv - dir * amount * 0.8).b; + fragColor = TDOutputSwizzle(vec4(r, g, b, 1.0)); +} +``` + +--- + +## Feedback Glow + +Warm persistent trails for glow effects. + +```glsl +out vec4 fragColor; +uniform float uDecay; // 0.92 - 0.98 for slow trails +uniform vec3 uGlowColor; // tint accumulated feedback + +void main() { + vec2 uv = vUV.st; + vec4 prev = texture(sTD2DInputs[0], uv); // feedback input + vec4 curr = texture(sTD2DInputs[1], uv); // current frame + + vec3 glow = prev.rgb * uDecay * uGlowColor; + vec3 result = max(glow, curr.rgb); + + fragColor = TDOutputSwizzle(vec4(result, 1.0)); +} +``` + +**Tips:** +- `uDecay = 0.95` → medium trail +- `uDecay = 0.98` → long comet tail +- Set `glslTOP` format to `rgba16float` for smooth gradients + +--- + +## Full Post-FX Stack + +Recommended order: + +``` +[scene / composite] + ↓ + bloomTOP ← luminance threshold bloom + ↓ + glslTOP (chrom) ← chromatic aberration + ↓ + glslTOP (crt) ← scanlines + barrel distortion + vignette + ↓ + null_out ← final output +``` + +**Performance note:** Each glslTOP is a full GPU pass. For 1920×1080 at 60fps this stack is comfortably real-time. For 4K, consider downsampling bloom input with `resolutionTOP` first. diff --git a/optional-skills/creative/touchdesigner-mcp/references/troubleshooting.md b/optional-skills/creative/touchdesigner-mcp/references/troubleshooting.md index b8e201f5c..c9817ebe0 100644 --- a/optional-skills/creative/touchdesigner-mcp/references/troubleshooting.md +++ b/optional-skills/creative/touchdesigner-mcp/references/troubleshooting.md @@ -137,7 +137,7 @@ actual = str(n.width) + 'x' + str(n.height) ### Config location -`$HERMES_HOME/config.yaml` (defaults to `~/.hermes/config.yaml` when `HERMES_HOME` is unset) +~/.hermes/config.yaml ### MCP entry format diff --git a/optional-skills/creative/touchdesigner-mcp/scripts/setup.sh b/optional-skills/creative/touchdesigner-mcp/scripts/setup.sh old mode 100644 new mode 100755 index 15dc662c1..f6bab2f50 --- a/optional-skills/creative/touchdesigner-mcp/scripts/setup.sh +++ b/optional-skills/creative/touchdesigner-mcp/scripts/setup.sh @@ -8,8 +8,7 @@ OK="${GREEN}✔${NC}"; FAIL="${RED}✘${NC}"; WARN="${YELLOW}⚠${NC}" TWOZERO_URL="https://www.404zero.com/pisang/twozero.tox" TOX_PATH="$HOME/Downloads/twozero.tox" -HERMES_HOME_DIR="${HERMES_HOME:-$HOME/.hermes}" -HERMES_CFG="${HERMES_HOME_DIR}/config.yaml" +HERMES_CFG="$HOME/.hermes/config.yaml" MCP_PORT=40404 MCP_ENDPOINT="http://localhost:${MCP_PORT}/mcp" @@ -18,10 +17,7 @@ manual_steps=() echo -e "\n${CYAN}═══ twozero MCP for TouchDesigner — Setup ═══${NC}\n" # ── 1. Check if TouchDesigner is running ── -# Match on process *name* (not full cmdline) to avoid self-matching shells -# that happen to have "TouchDesigner" in their args. macOS and Linux pgrep -# both support -x for exact name match. -if pgrep -x TouchDesigner >/dev/null 2>&1 || pgrep -x TouchDesignerFTE >/dev/null 2>&1; then +if pgrep -if "TouchDesigner" >/dev/null 2>&1; then echo -e " ${OK} TouchDesigner is running" td_running=true else @@ -69,6 +65,9 @@ if 'twozero_td' not in cfg['mcp_servers']: } with open(cfg_path, 'w') as f: yaml.dump(cfg, f, default_flow_style=False, sort_keys=False) + print('added') +else: + print('exists') " 2>/dev/null && echo -e " ${OK} twozero_td MCP entry added to config" \ || { echo -e " ${FAIL} Could not update config (is PyYAML installed?)"; \ manual_steps+=("Add twozero_td MCP entry to ${HERMES_CFG} manually"); }