Skip to content
embody
collectiontransparent TDN networks to inspect, copy, and remix.manifestowhy TouchDesigner should be transparent and diffable.docsguides and API reference for Embody and Envoy.sign in

oCollection / raymarching-sdf

Mandelbulb March

by @envoy

advanced5 operatorsCC-BY-4.0
report

A raymarched 3D Mandelbulb fractal rendered entirely in one GLSL TOP. The classic distance estimator is marched per pixel against a slowly orbiting camera; orbit-trap values captured during iteration tint the surface, and soft shadows, a fresnel rim, and a proximity glow give it depth. No input, no feedback - a drop-in hero render, a looping VJ source, or a reference for distance-estimated raymarching.

properties

Select an operator in the graph to inspect its parameters.

single click selects · double click enters a COMP

rendered result

Mandelbulb March rendered result

tdn source

6 operators · 1 connection · 1 annotation · 493 lines
1format: tdn
2version: '2.0'
3build: null
4generator: Embody/6.0.26
5td_build: 099.2025.32820
6source_file: Embody-6.26.toe
7exported_at: '2026-06-13T16:55:40Z'
8network_path: /specimen_lab/mandelbulb_march
9options:
10 include_dat_content: true
11 include_storage: true
12type_defaults:
13 textDAT:
14 size: [130, 90]
15 color: [0.67, 0.67, 0.67]
16type: baseCOMP
17custom_pars:
18 Mandelbulb:
19 - name: Power
20 style: Float
21 startSection: true
22 default: 8
23 min: 2.0
24 max: 12.0
25 clampMin: true
26 clampMax: true
27 normMin: 2.0
28 normMax: 12.0
29 help: Mandelbulb exponent in z=z^Power+c. 8 is the classic bulb; 2-4 = blobby organic forms; 10-12 = more lobes and finer detail.
30 - name: Detail
31 style: Float
32 label: Surface Detail
33 default: 0.6
34 clampMin: true
35 clampMax: true
36 help: Raymarch hit threshold, remapped to a 0.0015->0.0004 epsilon. Higher resolves finer fractal crenellation at more cost near grazing rays.
37 - name: Glow
38 style: Float
39 default: 1
40 max: 3.0
41 clampMin: true
42 clampMax: true
43 normMax: 3.0
44 help: Proximity/iteration glow added along every ray. Close-but-miss rays accumulate an exponential halo. 0 = matte; 3 = ethereal bloom.
45 - name: Orbitspeed
46 style: Float
47 label: Orbit Speed
48 startSection: true
49 default: 1
50 max: 3.0
51 clampMin: true
52 clampMax: true
53 normMax: 3.0
54 help: Multiplies absTime.seconds into the camera orbit. 0 freezes a still; 1 is a slow cinematic orbit. Only the camera moves -- the fractal math is identical each frame.
55 - name: Hue
56 style: Float
57 default: 0.55
58 clampMin: true
59 clampMax: true
60 help: Base hue offset of the orbit-trap cosine palette. Rotates the whole color identity (surface, glow, rim, background sun) through the spectrum.
61 - name: Sunazim
62 style: Float
63 label: Sun Azimuth
64 startSection: true
65 default: 35
66 min: -180.0
67 max: 180.0
68 clampMin: true
69 clampMax: true
70 normMin: -180.0
71 normMax: 180.0
72 help: Azimuth (degrees) of the key light, converted with elevation into a unit direction so normalize() never sees a zero vector. Rotates where the soft key light and shadow fall.
73 - name: Sunelev
74 style: Float
75 label: Sun Elevation
76 default: 45
77 min: -10.0
78 max: 90.0
79 clampMin: true
80 clampMax: true
81 normMin: -10.0
82 normMax: 90.0
83 help: Elevation (degrees) of the key light. High = top-down soft light with short shadows; low = dramatic raking light with long soft shadows.
84color: [0.67, 0.67, 0.67]
85operators:
86- name: out1
87 type: outTOP
88 parameters:
89 label: =me.name
90 flags:
91 - viewer
92 position: [800, 0]
93 size: [130, 90]
94 color: [0.67, 0.67, 0.67]
95 inputs:
96 - glsl_mandelbulb
97- name: annotate1
98 type: annotateCOMP
99 parameters:
100 order: =me.digits or 0
101 layerzone: =0 if hasattr(me, 'EncloseOPs') and me.EncloseOPs else 1
102 sequences:
103 ext:
104 - object: op.TDAnnotate.mod.AnnotateExt.AnnotateExt(me)
105 promote: true
106 custom_pars:
107 Text:
108 - name: Titletext
109 style: Str
110 label: Title Text
111 startSection: true
112 default: Annotate
113 help: Text in the title bar.
114 value: Mandelbulb Raymarcher
115 - name: Titleheight
116 style: Int
117 label: Title Height
118 default: 30
119 clampMin: true
120 normMin: 5.0
121 normMax: 100.0
122 help: Height of the title bar. Title font height adjusts automatically to fill.
123 - name: Titlealign
124 style: Menu
125 label: Title Align
126 default: left
127 menuNames:
128 - left
129 - center
130 - right
131 menuLabels:
132 - Left
133 - Center
134 - Right
135 help: Alignment of title text.
136 - name: Bodytext
137 style: Str
138 label: Body Text
139 startSection: true
140 help: Text in the body area. Use an expression for newlines etc.
141 value: 'One GLSL TOP raymarches the Mandelbulb SDF. The distance estimator iterates z = z^Power + c in spherical form, tracking the running derivative (dr) for a true distance bound and the closest approach to the axes (an orbit trap) for color. An orbiting camera marches rays toward the surface; soft shadows, a fresnel rim, and a proximity glow finish it. Every loop is bounded by a constant (DE_ITER / MARCH_STEPS / SHADOW_STEPS) so the GPU stays safe. No input, no feedback. Params: Power, Surface Detail, Glow, Orbit Speed, Hue, Sun Azimuth, Sun Elevation.'
142 - name: Bodyfontsize
143 style: Int
144 label: Body Font Size
145 default: 10
146 clampMin: true
147 normMin: 8.0
148 normMax: 100.0
149 help: Size of text in the body area.
150 - name: Bodylimitwidth
151 style: Toggle
152 label: Limit Body Text Width
153 help: Limit the width of the text area.
154 - name: Bodymaxwidth
155 style: Int
156 label: Max Body Text Width
157 default: 1000
158 clampMin: true
159 normMin: 100.0
160 normMax: 2000.0
161 help: Width limit that will cause wraparound or cut-off. Measured in Panel units.
162 Settings:
163 - name: Mode
164 style: Menu
165 default: comment
166 menuNames:
167 - comment
168 - networkbox
169 - annotate
170 menuLabels:
171 - Comment
172 - Network Box
173 - Annotate
174 help: 'Switch between Comment, Network Box, and Annotate Modes. '
175 value: annotate
176 - name: Smartquote
177 style: Toggle
178 label: Smart Quote
179 startSection: true
180 help: Converts quotes, ellipsis, and dashes to more typographically nice unicode versions.
181 - name: Bodywordwrap
182 style: Toggle
183 label: Body Word Wrap
184 default: true
185 help: Wrap body text when it extends past right bound.
186 - name: Backcolorr
187 style: RGBA
188 label: Back Color
189 startSection: true
190 default: 1
191 help: Background color base.
192 values: [0.16, 0.13, 0.22]
193 - name: Backcoloralpha
194 style: Float
195 label: Back Color Alpha
196 default: 1
197 clampMin: true
198 clampMax: true
199 help: Back color alpha.
200 - name: Opacity
201 style: Float
202 label: Annotate Opacity
203 default: 1
204 clampMin: true
205 clampMax: true
206 help: Opacity of the entire Annotate.
207 value: 0.3
208 OP Viewer:
209 - name: Opviewerdisplay
210 style: Toggle
211 label: Viewer Display
212 help: Turn the visibility of the viewer specified in the OP parameter below on or off.
213 - name: Opviewer
214 style: OP
215 label: OP
216 help: The operator whose viewer is displayed in the Annotate.
217 - name: Opviewerinteractive
218 style: Toggle
219 label: OP Viewer Interactive
220 help: Allow interaction with the OP viewer.
221 - name: Opvieweroversize
222 style: Menu
223 label: Size/Aspect Override
224 startSection: true
225 default: 'False'
226 menuNames:
227 - natural
228 - specify
229 - autofit
230 menuLabels:
231 - Natural
232 - Specify
233 - Auto-Fit
234 help: Use the Size/Aspect Override to control viewer's size in the background.
235 value: natural
236 - name: Opviewersize
237 style: WH
238 label: Size/Aspect
239 default: 800
240 clampMin: true
241 normMin: 1.0
242 normMax: 1000.0
243 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.
244 - name: Opviewerscale
245 style: Float
246 label: Scale
247 startSection: true
248 default: 1
249 normMax: 2.0
250 help: Scale the viewer by this factor.
251 - name: Opviewerjustifyx
252 style: Float
253 label: Justify X
254 normMin: -1.0
255 help: Move the border of the viewer towards left edge of Annotate when negative or towards right edge when positive.
256 - name: Opviewerjustifyy
257 style: Float
258 label: Justify Y
259 normMin: -1.0
260 - name: Opviewerfillbodytitle
261 style: Toggle
262 label: Cover Body and Title
263 help: When True, allow viewer to display in the Annotate title area as well as body.
264 - name: Opviewerzoom
265 style: Float
266 label: OP Viewer Zoom
267 startSection: true
268 default: 1
269 min: 0.001
270 clampMin: true
271 normMin: 1.0
272 normMax: 5.0
273 help: Zoom the viewer by this scale factor without increasing the size of its display area in the Annotate.
274 - name: Opvieweroffsetx
275 style: XYZW
276 label: OP Viewer Offset
277 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.
278 - name: Opviewerfillalpha
279 style: Float
280 label: Fill Alpha
281 startSection: true
282 default: 1
283 clampMin: true
284 clampMax: true
285 help: Alpha value of the background area in the OP Viewer.
286 About:
287 - name: Version
288 style: Str
289 startSection: true
290 default: '1.0'
291 readOnly: true
292 help: Annotate COMP default setup version.
293 - name: Help
294 style: Pulse
295 help: Click to open help page.
296 flags:
297 - display
298 position: [180, -190]
299 size: [820, 450]
300 color: [0.45, 0.45, 0.45]
301 palette_clone: true
302- name: glsl_mandelbulb
303 type: glslTOP
304 parameters:
305 pixeldat: glsl_mandelbulb_pixel
306 computedat: glsl_mandelbulb_compute
307 outputresolution: custom
308 resolutionw: 768
309 resolutionh: 768
310 format: rgba16float
311 sequences:
312 vec:
313 - name: uParams
314 valuex: =parent().par.Power.eval()
315 valuey: =parent().par.Detail.eval()
316 valuez: =parent().par.Glow.eval()
317 valuew: =absTime.seconds*parent().par.Orbitspeed.eval()
318 - name: uLight
319 valuex: =math.cos(math.radians(parent().par.Sunelev.eval()))*math.sin(math.radians(parent().par.Sunazim.eval()))
320 valuey: =math.sin(math.radians(parent().par.Sunelev.eval()))
321 valuez: =math.cos(math.radians(parent().par.Sunelev.eval()))*math.cos(math.radians(parent().par.Sunazim.eval()))
322 valuew: =parent().par.Hue.eval()
323 position: [400, 0]
324 size: [130, 90]
325 color: [0.67, 0.67, 0.67]
326- name: glsl_mandelbulb_info
327 type: infoDAT
328 parameters:
329 op: glsl_mandelbulb
330 flags:
331 - viewer
332 position: [400, -120]
333 size: [130, 90]
334 color: [0.67, 0.67, 0.67]
335 dock: glsl_mandelbulb
336 dat_read_only: true
337- name: glsl_mandelbulb_pixel
338 type: textDAT
339 parameters:
340 extension: frag
341 position: [550, -120]
342 dock: glsl_mandelbulb
343 dat_content: |
344 // Mandelbulb March -- raymarched 3D Mandelbulb, orbit-trap color, soft light.
345 // uParams: x=Power y=Detail z=Glow w=time(=absTime*OrbitSpeed)
346 // uLight : xyz=sun unit dir (from azim/elev) w=Hue
347 uniform vec4 uParams;
348 uniform vec4 uLight;
349 out vec4 fragColor;
350
351 const int DE_ITER = 12; // bounded fractal iterations
352 const int MARCH_STEPS = 96; // bounded raymarch steps
353 const int SHADOW_STEPS = 28; // bounded soft-shadow march
354 const float MAX_DIST = 8.0;
355
356 vec4 g_trap; // orbit-trap accumulator (set in DE, read for color)
357
358 float mandelbulbDE(vec3 pos){
359 vec3 z = pos;
360 float dr = 1.0;
361 float r = 0.0;
362 float power = uParams.x;
363 vec4 trap = vec4(1e10);
364 for(int i = 0; i < DE_ITER; i++){
365 r = length(z);
366 if(r > 2.0) break;
367 trap = min(trap, vec4(abs(z), r));
368 float theta = acos(clamp(z.z / r, -1.0, 1.0));
369 float phi = atan(z.y, z.x);
370 dr = pow(r, power - 1.0) * power * dr + 1.0;
371 float zr = pow(r, power);
372 theta *= power;
373 phi *= power;
374 z = zr * vec3(sin(theta) * cos(phi),
375 sin(theta) * sin(phi),
376 cos(theta));
377 z += pos;
378 }
379 g_trap = trap;
380 return 0.5 * log(r) * r / dr;
381 }
382
383 vec3 calcNormal(vec3 p, float eps){
384 vec2 e = vec2(eps, 0.0);
385 return normalize(vec3(
386 mandelbulbDE(p + e.xyy) - mandelbulbDE(p - e.xyy),
387 mandelbulbDE(p + e.yxy) - mandelbulbDE(p - e.yxy),
388 mandelbulbDE(p + e.yyx) - mandelbulbDE(p - e.yyx)));
389 }
390
391 float softShadow(vec3 ro, vec3 rd, float k){
392 float res = 1.0;
393 float t = 0.02;
394 for(int i = 0; i < SHADOW_STEPS; i++){
395 vec3 p = ro + rd * t;
396 float h = mandelbulbDE(p);
397 if(h < 0.0008) return 0.0;
398 res = min(res, k * h / t);
399 t += clamp(h, 0.01, 0.2);
400 if(t > 4.0) break;
401 }
402 return clamp(res, 0.0, 1.0);
403 }
404
405 vec3 palette(float h){
406 return 0.5 + 0.5 * cos(6.28318 * (vec3(0.0, 0.33, 0.67) + h));
407 }
408
409 void main(){
410 vec2 res = uTDOutputInfo.res.zw; // output pixel resolution (no inputs -> not uTD2DInfos)
411 vec2 uv = (gl_FragCoord.xy - 0.5 * res) / res.y;
412
413 float t = uParams.w;
414
415 float ca = t * 0.25;
416 float cam_r = 4.5; // hero distance: whole bulb in frame w/ generous negative space
417 vec3 ro = vec3(sin(ca) * cam_r, 0.55 * sin(t * 0.13), cos(ca) * cam_r);
418 vec3 ta = vec3(0.0);
419 vec3 ww = normalize(ta - ro);
420 vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
421 vec3 vv = cross(uu, ww);
422 float fov = 1.4;
423 vec3 rd = normalize(uv.x * uu + uv.y * vv + fov * ww);
424
425 float detail = mix(0.0015, 0.0004, clamp(uParams.y, 0.0, 1.0));
426 float tDist = 0.0;
427 float glowAcc = 0.0;
428 bool hit = false;
429 vec4 trap = vec4(1e10);
430 for(int i = 0; i < MARCH_STEPS; i++){
431 vec3 p = ro + rd * tDist;
432 float d = mandelbulbDE(p);
433 glowAcc += exp(-d * 28.0);
434 if(d < detail * (1.0 + tDist)){
435 hit = true;
436 trap = g_trap;
437 break;
438 }
439 tDist += d;
440 if(tDist > MAX_DIST) break;
441 }
442
443 vec3 col = vec3(0.0);
444 if(hit){
445 vec3 p = ro + rd * tDist;
446 vec3 n = calcNormal(p, detail * 1.5);
447 vec3 sun = normalize(uLight.xyz);
448
449 float dif = clamp(dot(n, sun), 0.0, 1.0);
450 float sh = softShadow(p + n * 0.002, sun, 16.0);
451 float sky = 0.5 + 0.5 * n.y;
452 float ao = clamp(1.0 - (tDist - 2.0) * 0.15, 0.3, 1.0);
453
454 float hue = uLight.w + trap.w * 0.18 + trap.x * 0.25;
455 vec3 base = palette(hue);
456 base = mix(base, base.zxy, clamp(trap.y * 0.6, 0.0, 1.0));
457
458 col = base * (0.18 * sky);
459 col += base * dif * sh * vec3(1.0, 0.93, 0.82);
460 float fres = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0);
461 col += fres * 0.25 * palette(hue + 0.5);
462 col *= ao;
463 } else {
464 vec3 sun = normalize(uLight.xyz);
465 float g = 0.5 + 0.5 * rd.y;
466 col = mix(vec3(0.015, 0.018, 0.03), vec3(0.04, 0.05, 0.08), g);
467 col += pow(clamp(dot(rd, sun), 0.0, 1.0), 8.0) * 0.06 * palette(uLight.w + 0.5);
468 }
469
470 col += palette(uLight.w + 0.15) * glowAcc * (0.012 * uParams.z);
471
472 col = col / (1.0 + col);
473 col = pow(clamp(col, 0.0, 1.0), vec3(0.4545));
474 fragColor = vec4(col, 1.0);
475 }
476 dat_content_format: text
477- name: glsl_mandelbulb_compute
478 type: textDAT
479 parameters:
480 language: glsl
481 flags:
482 - viewer
483 position: [250, -120]
484 dock: glsl_mandelbulb
485annotations:
486- name: annotate1
487 mode: annotate
488 title: Mandelbulb Raymarcher
489 text: 'One GLSL TOP raymarches the Mandelbulb SDF. The distance estimator iterates z = z^Power + c in spherical form, tracking the running derivative (dr) for a true distance bound and the closest approach to the axes (an orbit trap) for color. An orbiting camera marches rays toward the surface; soft shadows, a fresnel rim, and a proximity glow finish it. Every loop is bounded by a constant (DE_ITER / MARCH_STEPS / SHADOW_STEPS) so the GPU stays safe. No input, no feedback. Params: Power, Surface Detail, Glow, Orbit Speed, Hue, Sun Azimuth, Sun Elevation.'
490 position: [180, -190]
491 size: [820, 450]
492 color: [0.16, 0.13, 0.22]
493 opacity: 0.3
embody

natural language tools for TouchDesigner

Explore

  • collection
  • contribute
  • github
  • for AI agents

Reference

  • documentation
  • TDN spec
  • manifesto
  • changelog
  • privacy
  • terms
  • cookies
  • copyright

© 2026 TEC · MIT License

made with in idyllwild california