About this technique →
rd-on-sphere/sketch.js
// RD on Sphere
//
// A Turing-like spotted/striped pattern wrapped on a 3D sphere,
// procedurally generated from 3D fBm noise rather than iterative simulation.
// Raymarches a unit sphere, samples fBm at the surface point, then thresholds
// into one of three pattern modes: Spots, Stripes, or Maze.

export const PARAMS = {
  pattern:   { value: "Spots",   options: ["Spots", "Stripes", "Maze"], label: "Pattern Type",    folder: "Structure" },
  scale:     { value: 8.0, min: 2, max: 30, step: 0.1,  label: "Pattern Scale",   folder: "Structure" },
  threshold: { value: 0.4, min: 0.1, max: 0.9, step: 0.01, label: "Threshold",   folder: "Structure" },
  speed:     { value: 0.1, min: 0, max: 1.5, step: 0.01, label: "Animation Speed", folder: "Behavior" },
  hueA:      { value: 180, min: 0, max: 360, step: 1, label: "Hue A",            folder: "Appearance" },
  hueB:      { value: 30,  min: 0, max: 360, step: 1, label: "Hue B",            folder: "Appearance" },
  bg:        { value: "#080a0f", type: "color", label: "Background",             folder: "Appearance" },
};

function hexToVec3(hex) {
  const h = hex.replace(/^#/, '');
  return [
    parseInt(h.slice(0, 2), 16) / 255,
    parseInt(h.slice(2, 4), 16) / 255,
    parseInt(h.slice(4, 6), 16) / 255,
  ];
}

export function shaderUniforms(params) {
  const map = { Spots: 0, Stripes: 1, Maze: 2 };
  return {
    u_scale:     { value: params.scale },
    u_threshold: { value: params.threshold },
    u_speed:     { value: params.speed },
    u_hueA:      { value: params.hueA },
    u_hueB:      { value: params.hueB },
    u_bg:        { value: hexToVec3(params.bg) },
    u_pattern:   { value: map[params.pattern] ?? 0 },
  };
}

export function shaderAnimate(uniforms, params) {
  const map = { Spots: 0, Stripes: 1, Maze: 2 };
  uniforms.u_scale.value     = params.scale;
  uniforms.u_threshold.value = params.threshold;
  uniforms.u_speed.value     = params.speed;
  uniforms.u_hueA.value      = params.hueA;
  uniforms.u_hueB.value      = params.hueB;
  uniforms.u_bg.value        = hexToVec3(params.bg);
  uniforms.u_pattern.value   = map[params.pattern] ?? 0;
}

export function fragmentShader() {
  return `
    uniform float u_time;
    uniform vec2  u_resolution;
    uniform float u_scale;
    uniform float u_threshold;
    uniform float u_speed;
    uniform float u_hueA;
    uniform float u_hueB;
    uniform vec3  u_bg;
    uniform int   u_pattern;

    // Hash for 3D gradient noise
    vec3 hash33(vec3 p) {
      p = vec3(
        dot(p, vec3(127.1, 311.7,  74.7)),
        dot(p, vec3(269.5, 183.3, 246.1)),
        dot(p, vec3(113.5, 271.9, 124.6))
      );
      return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
    }

    // Smooth Perlin-style 3D noise
    float noise3(vec3 p) {
      vec3 i = floor(p);
      vec3 f = fract(p);
      f = f * f * (3.0 - 2.0 * f);
      return mix(
        mix(
          mix(dot(hash33(i + vec3(0,0,0)), f - vec3(0,0,0)),
              dot(hash33(i + vec3(1,0,0)), f - vec3(1,0,0)), f.x),
          mix(dot(hash33(i + vec3(0,1,0)), f - vec3(0,1,0)),
              dot(hash33(i + vec3(1,1,0)), f - vec3(1,1,0)), f.x), f.y),
        mix(
          mix(dot(hash33(i + vec3(0,0,1)), f - vec3(0,0,1)),
              dot(hash33(i + vec3(1,0,1)), f - vec3(1,0,1)), f.x),
          mix(dot(hash33(i + vec3(0,1,1)), f - vec3(0,1,1)),
              dot(hash33(i + vec3(1,1,1)), f - vec3(1,1,1)), f.x), f.y),
        f.z
      );
    }

    // Fractional Brownian Motion — 4 octaves, result in [0,1]
    float fbm(vec3 p) {
      float v = 0.0, a = 0.5;
      for (int i = 0; i < 4; i++) {
        v += a * noise3(p);
        p *= 2.03;
        a *= 0.5;
      }
      return v + 0.5;
    }

    // Cosine-based color palette from two hue values
    vec3 hueColor(float h) {
      vec3 a = vec3(0.5);
      vec3 b = vec3(0.5);
      vec3 c = vec3(1.0);
      return a + b * cos(6.28318 * (c * 0.5 + vec3(h / 360.0)));
    }

    void main() {
      vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;

      // Camera ray — simple orthographic-style perspective
      vec3 ro = vec3(0.0, 0.0, -3.0);
      vec3 rd = normalize(vec3(uv, 1.5));

      // Analytic sphere intersection (unit sphere at origin)
      float b = dot(ro, rd);
      float c = dot(ro, ro) - 1.0;
      float disc = b * b - c;

      if (disc < 0.0) {
        gl_FragColor = vec4(u_bg, 1.0);
        return;
      }

      float t = -b - sqrt(disc);
      vec3 pos = ro + rd * t;
      vec3 nor = normalize(pos);

      // Sample fBm at surface point, animated by time
      vec3 sp = pos * u_scale + vec3(u_time * u_speed);
      float v = fbm(sp);

      // Pattern modes
      float pat = 0.0;
      if (u_pattern == 0) {
        // Spots: hard threshold
        pat = step(u_threshold, v);
      } else if (u_pattern == 1) {
        // Stripes: repeated banding
        pat = step(0.5, fract(v * 4.0));
      } else {
        // Maze: thin corridors at contour lines
        float band = abs(v - 0.5);
        pat = 1.0 - smoothstep(0.0, 0.06, band);
      }

      vec3 colA = hueColor(u_hueA);
      vec3 colB = hueColor(u_hueB);
      vec3 col = mix(colA, colB, pat);

      // Lambertian shading
      vec3 lightDir = normalize(vec3(0.6, 0.7, -0.5));
      float diff = max(dot(nor, lightDir), 0.0);
      col *= (0.3 + 0.7 * diff);

      // Specular highlight
      vec3 viewDir = normalize(-rd);
      vec3 halfDir = normalize(lightDir + viewDir);
      float spec = pow(max(dot(nor, halfDir), 0.0), 32.0);
      col += 0.15 * spec;

      // Subtle rim light
      float rim = 1.0 - max(dot(nor, viewDir), 0.0);
      col += 0.08 * rim * rim * mix(colA, colB, 0.5);

      // Gamma correction
      col = pow(clamp(col, 0.0, 1.0), vec3(1.0 / 2.2));

      gl_FragColor = vec4(col, 1.0);
    }
  `;
}