Specimen / raymarching-sdf

Mandelbulb March

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.

advanced 5 operators

Node graph preview

Mandelbulb March graph

inert preview
Show raw TDN YAML

Paste in TouchDesigner with Embody running.

format: tdn
version: '2.0'
build: null
generator: Embody/6.0.26
td_build: 099.2025.32820
source_file: Embody-6.26.toe
exported_at: '2026-06-13T16:55:40Z'
network_path: /specimen_lab/mandelbulb_march
options:
  include_dat_content: true
  include_storage: true
type_defaults:
  textDAT:
    size: [130, 90]
    color: [0.67, 0.67, 0.67]
type: baseCOMP
custom_pars:
  Mandelbulb:
  - name: Power
    style: Float
    startSection: true
    default: 8
    min: 2.0
    max: 12.0
    clampMin: true
    clampMax: true
    normMin: 2.0
    normMax: 12.0
    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.
  - name: Detail
    style: Float
    label: Surface Detail
    default: 0.6
    clampMin: true
    clampMax: true
    help: Raymarch hit threshold, remapped to a 0.0015->0.0004 epsilon. Higher resolves finer fractal crenellation at more cost near grazing rays.
  - name: Glow
    style: Float
    default: 1
    max: 3.0
    clampMin: true
    clampMax: true
    normMax: 3.0
    help: Proximity/iteration glow added along every ray. Close-but-miss rays accumulate an exponential halo. 0 = matte; 3 = ethereal bloom.
  - name: Orbitspeed
    style: Float
    label: Orbit Speed
    startSection: true
    default: 1
    max: 3.0
    clampMin: true
    clampMax: true
    normMax: 3.0
    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.
  - name: Hue
    style: Float
    default: 0.55
    clampMin: true
    clampMax: true
    help: Base hue offset of the orbit-trap cosine palette. Rotates the whole color identity (surface, glow, rim, background sun) through the spectrum.
  - name: Sunazim
    style: Float
    label: Sun Azimuth
    startSection: true
    default: 35
    min: -180.0
    max: 180.0
    clampMin: true
    clampMax: true
    normMin: -180.0
    normMax: 180.0
    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.
  - name: Sunelev
    style: Float
    label: Sun Elevation
    default: 45
    min: -10.0
    max: 90.0
    clampMin: true
    clampMax: true
    normMin: -10.0
    normMax: 90.0
    help: Elevation (degrees) of the key light. High = top-down soft light with short shadows; low = dramatic raking light with long soft shadows.
color: [0.67, 0.67, 0.67]
operators:
- name: out1
  type: outTOP
  parameters:
    label: =me.name
  flags:
  - viewer
  position: [800, 0]
  size: [130, 90]
  color: [0.67, 0.67, 0.67]
  inputs:
  - glsl_mandelbulb
- name: annotate1
  type: annotateCOMP
  parameters:
    order: =me.digits or 0
    layerzone: =0 if hasattr(me, 'EncloseOPs') and me.EncloseOPs else 1
  sequences:
    ext:
    - object: op.TDAnnotate.mod.AnnotateExt.AnnotateExt(me)
      promote: true
  custom_pars:
    Text:
    - name: Titletext
      style: Str
      label: Title Text
      startSection: true
      default: Annotate
      help: Text in the title bar.
      value: Mandelbulb Raymarcher
    - name: Titleheight
      style: Int
      label: Title Height
      default: 30
      clampMin: true
      normMin: 5.0
      normMax: 100.0
      help: Height of the title bar. Title font height adjusts automatically to fill.
    - name: Titlealign
      style: Menu
      label: Title Align
      default: left
      menuNames:
      - left
      - center
      - right
      menuLabels:
      - Left
      - Center
      - Right
      help: Alignment of title text.
    - name: Bodytext
      style: Str
      label: Body Text
      startSection: true
      help: Text in the body area. Use an expression for newlines etc.
      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.'
    - name: Bodyfontsize
      style: Int
      label: Body Font Size
      default: 10
      clampMin: true
      normMin: 8.0
      normMax: 100.0
      help: Size of text in the body area.
    - name: Bodylimitwidth
      style: Toggle
      label: Limit Body Text Width
      help: Limit the width of the text area.
    - name: Bodymaxwidth
      style: Int
      label: Max Body Text Width
      default: 1000
      clampMin: true
      normMin: 100.0
      normMax: 2000.0
      help: Width limit that will cause wraparound or cut-off. Measured in Panel units.
    Settings:
    - name: Mode
      style: Menu
      default: comment
      menuNames:
      - comment
      - networkbox
      - annotate
      menuLabels:
      - Comment
      - Network Box
      - Annotate
      help: 'Switch between Comment, Network Box, and Annotate Modes. '
      value: annotate
    - name: Smartquote
      style: Toggle
      label: Smart Quote
      startSection: true
      help: Converts quotes, ellipsis, and dashes to more typographically nice unicode versions.
    - name: Bodywordwrap
      style: Toggle
      label: Body Word Wrap
      default: true
      help: Wrap body text when it extends past right bound.
    - name: Backcolorr
      style: RGBA
      label: Back Color
      startSection: true
      default: 1
      help: Background color base.
      values: [0.16, 0.13, 0.22]
    - name: Backcoloralpha
      style: Float
      label: Back Color Alpha
      default: 1
      clampMin: true
      clampMax: true
      help: Back color alpha.
    - name: Opacity
      style: Float
      label: Annotate Opacity
      default: 1
      clampMin: true
      clampMax: true
      help: Opacity of the entire Annotate.
      value: 0.3
    OP Viewer:
    - name: Opviewerdisplay
      style: Toggle
      label: Viewer Display
      help: Turn the visibility of the viewer specified in the OP parameter below on or off.
    - name: Opviewer
      style: OP
      label: OP
      help: The operator whose viewer is displayed in the Annotate.
    - name: Opviewerinteractive
      style: Toggle
      label: OP Viewer Interactive
      help: Allow interaction with the OP viewer.
    - name: Opvieweroversize
      style: Menu
      label: Size/Aspect Override
      startSection: true
      default: 'False'
      menuNames:
      - natural
      - specify
      - autofit
      menuLabels:
      - Natural
      - Specify
      - Auto-Fit
      help: Use the Size/Aspect Override to control viewer's size in the background.
      value: natural
    - name: Opviewersize
      style: WH
      label: Size/Aspect
      default: 800
      clampMin: true
      normMin: 1.0
      normMax: 1000.0
      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.
    - name: Opviewerscale
      style: Float
      label: Scale
      startSection: true
      default: 1
      normMax: 2.0
      help: Scale the viewer by this factor.
    - name: Opviewerjustifyx
      style: Float
      label: Justify X
      normMin: -1.0
      help: Move the border of the viewer towards left edge of Annotate when negative or towards right edge when positive.
    - name: Opviewerjustifyy
      style: Float
      label: Justify Y
      normMin: -1.0
    - name: Opviewerfillbodytitle
      style: Toggle
      label: Cover Body and Title
      help: When True, allow viewer to display in the Annotate title area as well as body.
    - name: Opviewerzoom
      style: Float
      label: OP Viewer Zoom
      startSection: true
      default: 1
      min: 0.001
      clampMin: true
      normMin: 1.0
      normMax: 5.0
      help: Zoom the viewer by this scale factor without increasing the size of its display area in the Annotate.
    - name: Opvieweroffsetx
      style: XYZW
      label: OP Viewer Offset
      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.
    - name: Opviewerfillalpha
      style: Float
      label: Fill Alpha
      startSection: true
      default: 1
      clampMin: true
      clampMax: true
      help: Alpha value of the background area in the OP Viewer.
    About:
    - name: Version
      style: Str
      startSection: true
      default: '1.0'
      readOnly: true
      help: Annotate COMP default setup version.
    - name: Help
      style: Pulse
      help: Click to open help page.
  flags:
  - display
  position: [180, -190]
  size: [820, 450]
  color: [0.45, 0.45, 0.45]
  palette_clone: true
- name: glsl_mandelbulb
  type: glslTOP
  parameters:
    pixeldat: glsl_mandelbulb_pixel
    computedat: glsl_mandelbulb_compute
    outputresolution: custom
    resolutionw: 768
    resolutionh: 768
    format: rgba16float
  sequences:
    vec:
    - name: uParams
      valuex: =parent().par.Power.eval()
      valuey: =parent().par.Detail.eval()
      valuez: =parent().par.Glow.eval()
      valuew: =absTime.seconds*parent().par.Orbitspeed.eval()
    - name: uLight
      valuex: =math.cos(math.radians(parent().par.Sunelev.eval()))*math.sin(math.radians(parent().par.Sunazim.eval()))
      valuey: =math.sin(math.radians(parent().par.Sunelev.eval()))
      valuez: =math.cos(math.radians(parent().par.Sunelev.eval()))*math.cos(math.radians(parent().par.Sunazim.eval()))
      valuew: =parent().par.Hue.eval()
  position: [400, 0]
  size: [130, 90]
  color: [0.67, 0.67, 0.67]
- name: glsl_mandelbulb_info
  type: infoDAT
  parameters:
    op: glsl_mandelbulb
  flags:
  - viewer
  position: [400, -120]
  size: [130, 90]
  color: [0.67, 0.67, 0.67]
  dock: glsl_mandelbulb
  dat_read_only: true
- name: glsl_mandelbulb_pixel
  type: textDAT
  parameters:
    extension: frag
  position: [550, -120]
  dock: glsl_mandelbulb
  dat_content: |
    // Mandelbulb March -- raymarched 3D Mandelbulb, orbit-trap color, soft light.
    // uParams: x=Power  y=Detail  z=Glow  w=time(=absTime*OrbitSpeed)
    // uLight : xyz=sun unit dir (from azim/elev)  w=Hue
    uniform vec4 uParams;
    uniform vec4 uLight;
    out vec4 fragColor;

    const int   DE_ITER     = 12;   // bounded fractal iterations
    const int   MARCH_STEPS = 96;   // bounded raymarch steps
    const int   SHADOW_STEPS = 28;  // bounded soft-shadow march
    const float MAX_DIST = 8.0;

    vec4 g_trap;   // orbit-trap accumulator (set in DE, read for color)

    float mandelbulbDE(vec3 pos){
        vec3 z = pos;
        float dr = 1.0;
        float r  = 0.0;
        float power = uParams.x;
        vec4  trap = vec4(1e10);
        for(int i = 0; i < DE_ITER; i++){
            r = length(z);
            if(r > 2.0) break;
            trap = min(trap, vec4(abs(z), r));
            float theta = acos(clamp(z.z / r, -1.0, 1.0));
            float phi   = atan(z.y, z.x);
            dr = pow(r, power - 1.0) * power * dr + 1.0;
            float zr = pow(r, power);
            theta *= power;
            phi   *= power;
            z = zr * vec3(sin(theta) * cos(phi),
                          sin(theta) * sin(phi),
                          cos(theta));
            z += pos;
        }
        g_trap = trap;
        return 0.5 * log(r) * r / dr;
    }

    vec3 calcNormal(vec3 p, float eps){
        vec2 e = vec2(eps, 0.0);
        return normalize(vec3(
            mandelbulbDE(p + e.xyy) - mandelbulbDE(p - e.xyy),
            mandelbulbDE(p + e.yxy) - mandelbulbDE(p - e.yxy),
            mandelbulbDE(p + e.yyx) - mandelbulbDE(p - e.yyx)));
    }

    float softShadow(vec3 ro, vec3 rd, float k){
        float res = 1.0;
        float t = 0.02;
        for(int i = 0; i < SHADOW_STEPS; i++){
            vec3 p = ro + rd * t;
            float h = mandelbulbDE(p);
            if(h < 0.0008) return 0.0;
            res = min(res, k * h / t);
            t += clamp(h, 0.01, 0.2);
            if(t > 4.0) break;
        }
        return clamp(res, 0.0, 1.0);
    }

    vec3 palette(float h){
        return 0.5 + 0.5 * cos(6.28318 * (vec3(0.0, 0.33, 0.67) + h));
    }

    void main(){
        vec2 res = uTDOutputInfo.res.zw;          // output pixel resolution (no inputs -> not uTD2DInfos)
        vec2 uv  = (gl_FragCoord.xy - 0.5 * res) / res.y;

        float t = uParams.w;

        float ca = t * 0.25;
        float cam_r = 4.5;                         // hero distance: whole bulb in frame w/ generous negative space
        vec3 ro = vec3(sin(ca) * cam_r, 0.55 * sin(t * 0.13), cos(ca) * cam_r);
        vec3 ta = vec3(0.0);
        vec3 ww = normalize(ta - ro);
        vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
        vec3 vv = cross(uu, ww);
        float fov = 1.4;
        vec3 rd = normalize(uv.x * uu + uv.y * vv + fov * ww);

        float detail = mix(0.0015, 0.0004, clamp(uParams.y, 0.0, 1.0));
        float tDist = 0.0;
        float glowAcc = 0.0;
        bool  hit = false;
        vec4  trap = vec4(1e10);
        for(int i = 0; i < MARCH_STEPS; i++){
            vec3 p = ro + rd * tDist;
            float d = mandelbulbDE(p);
            glowAcc += exp(-d * 28.0);
            if(d < detail * (1.0 + tDist)){
                hit  = true;
                trap = g_trap;
                break;
            }
            tDist += d;
            if(tDist > MAX_DIST) break;
        }

        vec3 col = vec3(0.0);
        if(hit){
            vec3 p = ro + rd * tDist;
            vec3 n = calcNormal(p, detail * 1.5);
            vec3 sun = normalize(uLight.xyz);

            float dif = clamp(dot(n, sun), 0.0, 1.0);
            float sh  = softShadow(p + n * 0.002, sun, 16.0);
            float sky = 0.5 + 0.5 * n.y;
            float ao = clamp(1.0 - (tDist - 2.0) * 0.15, 0.3, 1.0);

            float hue = uLight.w + trap.w * 0.18 + trap.x * 0.25;
            vec3 base = palette(hue);
            base = mix(base, base.zxy, clamp(trap.y * 0.6, 0.0, 1.0));

            col  = base * (0.18 * sky);
            col += base * dif * sh * vec3(1.0, 0.93, 0.82);
            float fres = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0);
            col += fres * 0.25 * palette(hue + 0.5);
            col *= ao;
        } else {
            vec3 sun = normalize(uLight.xyz);
            float g = 0.5 + 0.5 * rd.y;
            col = mix(vec3(0.015, 0.018, 0.03), vec3(0.04, 0.05, 0.08), g);
            col += pow(clamp(dot(rd, sun), 0.0, 1.0), 8.0) * 0.06 * palette(uLight.w + 0.5);
        }

        col += palette(uLight.w + 0.15) * glowAcc * (0.012 * uParams.z);

        col = col / (1.0 + col);
        col = pow(clamp(col, 0.0, 1.0), vec3(0.4545));
        fragColor = vec4(col, 1.0);
    }
  dat_content_format: text
- name: glsl_mandelbulb_compute
  type: textDAT
  parameters:
    language: glsl
  flags:
  - viewer
  position: [250, -120]
  dock: glsl_mandelbulb
annotations:
- name: annotate1
  mode: annotate
  title: Mandelbulb Raymarcher
  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.'
  position: [180, -190]
  size: [820, 450]
  color: [0.16, 0.13, 0.22]
  opacity: 0.3