The Math Behind Lissajous 3D: Frequencies, Phases, and Parametric Surfaces

Creating Breathtaking 3D Lissajous Figures with Python and WebGLLissajous figures — the elegant curves produced by combining perpendicular simple harmonic motions — have enchanted artists, scientists, and hobbyists for generations. When extended into three dimensions, these forms become luminous ribbons and knots that can illustrate resonance, frequency ratios, and phase relationships while also serving as compelling generative art. This article shows how to create striking 3D Lissajous figures using Python to generate parametric data and WebGL to render interactive, high-performance visuals in the browser. You’ll get mathematical background, Python code to produce point clouds, tips for exporting the data, and a WebGL (Three.js) implementation that adds lighting, materials, animation, and UI controls.


Why 3D Lissajous figures?

  • Intuition and aesthetics: 3D Lissajous figures make multidimensional harmonic relationships visible. Small changes to frequency ratios or phase shifts produce dramatically different topologies, from simple loops to complex knot-like structures.
  • Interactivity: Rotating, zooming, and animating these shapes helps students and makers understand harmonics and parametric motion.
  • Performance and portability: Using Python for data generation and WebGL for rendering lets you leverage scientific libraries for math and an efficient GPU pipeline for visualization.

Math: parametric definition and parameters

A 3D Lissajous figure is a parametric curve defined by three sinusoidal components with (usually) different frequencies and phases:

x(t) = A_x * sin(a * t + δ_x)
y(t) = A_y * sin(b * t + δ_y)
z(t) = A_z * sin(c * t + δ_z)

Where:

  • A_x, A_y, A_z are amplitudes (scales along each axis).
  • a, b, c are angular frequencies (often integers).
  • δ_x, δ_y, δ_z are phase offsets.
  • t is the parameter, typically in [0, 2π·L] where L controls how many cycles are drawn.

Key behaviors:

  • When the frequency ratios a:b:c are rational, the curve is closed and periodic; when irrational, it densely fills a region.
  • Phase offsets control orientation and knotting; varying them can produce rotations and shifts of lobes.
  • Using different amplitudes stretches the figure along axes, creating flattened or elongated shapes.

Generate point data with Python

Below is a Python script that generates a dense point cloud for a 3D Lissajous curve and writes JSON suitable for loading into a WebGL viewer. It uses numpy for numeric work and optionally saves an indexed line set for efficient rendering.

# lissajous3d_export.py import numpy as np import json from pathlib import Path def generate_lissajous(ax=1.0, ay=1.0, az=1.0,                        a=3, b=4, c=5,                        dx=0.0, dy=np.pi/2, dz=np.pi/4,                        samples=2000, cycles=2.0):     t = np.linspace(0, 2*np.pi*cycles, samples)     x = ax * np.sin(a * t + dx)     y = ay * np.sin(b * t + dy)     z = az * np.sin(c * t + dz)     points = np.vstack([x, y, z]).T.astype(float).tolist()     return points def save_json(points, out_path='lissajous3d.json'):     data = {'points': points}     Path(out_path).write_text(json.dumps(data))     print(f'Saved {len(points)} points to {out_path}') if __name__ == '__main__':     pts = generate_lissajous(ax=1.0, ay=1.0, az=1.0,                              a=5, b=6, c=7,                              dx=0.0, dy=np.pi/3, dz=np.pi/6,                              samples=4000, cycles=3.0)     save_json(pts, 'lissajous3d.json') 

Notes:

  • Increase samples for smoother curves; 4–8k points is usually sufficient for line rendering.
  • You can store color or per-point radii in the JSON for richer rendering effects.

Exporting richer geometry: tubes and ribbons

Rendering a raw polyline looks simple but adding thickness (tube geometry) or a ribbon gives better depth cues and lighting. You can either:

  • Generate a tube mesh in Python (e.g., by computing Frenet frames and extruding a circle along the curve) and export as glTF/OBJ; or
  • Send the centerline points to the client and build the tube in WebGL using shader/geometry code (more flexible and usually faster).

A simple approach is to export centerline points and compute a triangle strip on the GPU.


Interactive rendering with WebGL and Three.js

Three.js provides an approachable WebGL abstraction. Below is a minimal (but feature-rich) example that loads the JSON points and renders a shaded tube with animation controls. Save this as index.html and serve it from a local HTTP server.

<!-- index.html --> <!doctype html> <html> <head>   <meta charset="utf-8" />   <title>3D Lissajous</title>   <style>body{margin:0;overflow:hidden} canvas{display:block}</style> </head> <body> <script type="module"> import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js'; import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js'; import { TubeGeometry } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/geometries/TubeGeometry.js'; (async function(){   const res = await fetch('lissajous3d.json');   const data = await res.json();   const points = data.points.map(p => new THREE.Vector3(p[0], p[1], p[2]));   const scene = new THREE.Scene();   const camera = new THREE.PerspectiveCamera(45, innerWidth/innerHeight, 0.01, 100);   camera.position.set(3,3,6);   const renderer = new THREE.WebGLRenderer({antialias:true});   renderer.setSize(innerWidth, innerHeight);   document.body.appendChild(renderer.domElement);   const controls = new OrbitControls(camera, renderer.domElement);   controls.enableDamping = true;   // Create Curve class from points   class PointsCurve extends THREE.Curve {     constructor(pts){ super(); this.pts = pts; }     getPoint(t){ const i = Math.floor(t*(this.pts.length-1)); return this.pts[i].clone(); }   }   const curve = new PointsCurve(points);   const tubeGeom = new TubeGeometry(curve, points.length, 0.03, 12, true);   const mat = new THREE.MeshStandardMaterial({ color: 0x66ccff, metalness:0.2, roughness:0.3 });   const mesh = new THREE.Mesh(tubeGeom, mat);   scene.add(mesh);   const light = new THREE.DirectionalLight(0xffffff, 0.9); light.position.set(5,10,7); scene.add(light);   scene.add(new THREE.AmbientLight(0x404040, 0.6));   function animate(t){     requestAnimationFrame(animate);     mesh.rotation.y += 0.002;     controls.update();     renderer.render(scene, camera);   }   animate(); })(); </script> </body> </html> 

Tips:

  • TubeGeometry automatically computes frames; for tighter control, compute Frenet frames in JavaScript.
  • Use MeshStandardMaterial with lights for realistic shading. Add environment maps for reflective sheen.

Performance tips

  • Instanced rendering: For multiple simultaneous curves, use GPU instancing.
  • Level of detail: Reduce segments for distant views or use dynamic resampling.
  • Shaders: Offload per-vertex displacement or colorization to GLSL for smooth, cheap animation (time-based phase shifts computed in vertex shader).
  • Buffer geometry: Use BufferGeometry and typed arrays (Float32Array) when passing large point sets.

Creative variations

  • Animated phases: increment δ_x,y,z over time to produce morphing shapes.
  • Color by frequency: map local curvature or velocity magnitude to color or emissive intensity.
  • Particle trails: spawn particles that follow the curve to highlight motion.
  • Multiple harmonics: superpose additional sinusoids to create more complex or “fractal” Lissajous shapes.
  • Physical simulation: use the curve as an attractor path for cloth, ribbons, or soft-body particles.

Example: animate phases in GLSL (concept)

Compute vertex positions on the GPU by sending base parameters (a,b,c, amplitudes, phase offsets) and evaluating sinusoids per-vertex with a parameter t. This lets you animate without regenerating geometry on CPU.

Pseudo-steps:

  1. Pass an attribute u in [0,1] per vertex representing t.
  2. In vertex shader compute t’ = u * cycles * 2π and x,y,z = A*sin(f*t’ + δ + time*ω).
  3. Output transformed position; fragment shader handles coloring.

Putting it all together: workflow

  1. Use Python to prototype frequency/phase combos and export JSON or glTF.
  2. Load centerline in the browser, generate tube/ribbon geometry via Three.js or custom shaders.
  3. Add UI (dat.GUI or Tweakpane) for live parameter tweaking: amplitudes, frequencies, phases, tube radius, color, and animation speed.
  4. Add sharing/export: capture frames to PNG, or export glTF for 3D printing or reuse.

Final notes

3D Lissajous figures are where math and art meet: a small set of parameters yields a huge variety of forms. Using Python for generation and WebGL for rendering gives a practical, performant pipeline for exploration and presentation. Experiment with non-integer and near-resonant frequency ratios and phase sweeps to discover surprising, knot-like structures — and consider layering multiple curves with different materials for striking compositions.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *