javascriptalgorithm3d

Drawing/Rendering 3D objects with epicycles and fourier transformations [Animation]


First Note: They wont let me embed images until i have more reputation points (sorry), but all the links are images posted on imgur! :) thanks

I have replicated a method to animate any single path (1 closed path) using fourier transforms. This creates an animation of epicylces (rotating circles) which rotate around each other, and follow the imputed points, tracing the path as a continuous loop/function.

I would like to adopt this system to 3D. the two methods i can think of to achieve this is to use a Spherical Coordinate system (two complex planes) or 3 Epicycles --> one for each axis (x,y,z) with their individual parametric equations. This is probably the best way to start!!

2 Cycles, One for X and one for Y: img

Picture: One Cycle --> Complex Numbers --> For X and Y img

Fourier Transformation Background!!!:

• Eulers formula allows us to decompose each point in the complex plane into an angle (the argument to the exponential function) and an amplitude (Cn coefficients)

• In this sense, there is a connection to imaging each term in the infinite series above as representing a point on a circle with radius cn, offset by 2πnt/T radians

• The image below shows how a sum of complex numbers in terms of phases/amplitudes can be visualized as a set of concatenated cirlces in the complex plane. Each red line is a vector representing a term in the sequence of sums: cne2πi(nT)t

• Adding the summands corresponds to simply concatenating each of these red vectors in complex space:

Animated Rotating Circles: img

Circles to Animated Drawings:

• If you have a line drawing in 2D (x-y) space, you can describe this path mathematically as a parametric function. (two separate single variable functions, both in terms of an auxiliary variable (T in this case):

enter image description here

• For example, below is a simple line drawing of a horse, and a parametric path through the black pixels in image, and that path then seperated into its X and Y components:

Horse Path

• At this point, we need to calculate the Fourier approximations of these two paths, and use coefficients from this approximation to determine the phase and amplitudes of the circles needed for the final visualization.

Python Code: The python code used for this example can be found here on guithub

I have successful animated this process in 2D, but i would like to adopt this to 3D.

The Following Code Represents Animations in 2D --> something I already have working:

[Using JavaScript & P5.js library]

The Fourier Algorithm (fourier.js):

//  a + bi
class Complex {
  constructor(a, b) {
    this.re = a;
    this.im = b;
  }

  add(c) {
    this.re += c.re;
    this.im += c.im;
  }

  mult(c) {
    const re = this.re * c.re - this.im * c.im;
    const im = this.re * c.im + this.im * c.re;
    return new Complex(re, im);
  }
}



function dft(x) {
  const X = [];
  const Values = [];
  const N = x.length;
  for (let k = 0; k < N; k++) {
    let sum = new Complex(0, 0);
    for (let n = 0; n < N; n++) {
      const phi = (TWO_PI * k * n) / N;
      const c = new Complex(cos(phi), -sin(phi));
      sum.add(x[n].mult(c));
    }
    sum.re = sum.re / N;
    sum.im = sum.im / N;

    let freq = k;
    let amp = sqrt(sum.re * sum.re + sum.im * sum.im);
    let phase = atan2(sum.im, sum.re);
    X[k] = { re: sum.re, im: sum.im, freq, amp, phase };
Values[k] = {phase};
  console.log(Values[k]);

  }
  return X;
}

The Sketch Function/ Animations (Sketch.js):

let x = [];
let fourierX;
let time = 0;
let path = [];

function setup() {
  createCanvas(800, 600);
  const skip = 1;
  for (let i = 0; i < drawing.length; i += skip) {
    const c = new Complex(drawing[i].x, drawing[i].y);
    x.push(c);
  }
  fourierX = dft(x);
  fourierX.sort((a, b) => b.amp - a.amp);
}

function epicycles(x, y, rotation, fourier) {
  for (let i = 0; i < fourier.length; i++) {
    let prevx = x;
    let prevy = y;
    let freq = fourier[i].freq;
    let radius = fourier[i].amp;
    let phase = fourier[i].phase;
    x += radius * cos(freq * time + phase + rotation);
    y += radius * sin(freq * time + phase + rotation);

    stroke(255, 100);
    noFill();
    ellipse(prevx, prevy, radius * 2);
    stroke(255);
    line(prevx, prevy, x, y);
  }
  return createVector(x, y);
}

function draw() {
  background(0);

  let v = epicycles(width / 2, height / 2, 0, fourierX);
  path.unshift(v);

  beginShape();
  noFill();
  for (let i = 0; i < path.length; i++) {
    vertex(path[i].x, path[i].y);
  }
  endShape();

  const dt = TWO_PI / fourierX.length;
  time += dt;

And Most Importantly! THE PATH / COORDINATES: (this one is a triangle)

let drawing = [

  { y:  -8.001009734    , x:    -50 },
  { y:  -7.680969345    , x:    -49 },
  { y:  -7.360928956    , x:    -48 },
  { y:  -7.040888566    , x:    -47 },
  { y:  -6.720848177    , x:    -46 },
  { y:  -6.400807788    , x:    -45 },
  { y:  -6.080767398    , x:    -44 },
  { y:  -5.760727009    , x:    -43 },
  { y:  -5.440686619    , x:    -42 },
  { y:  -5.12064623 , x:    -41 },
  { y:  -4.800605841    , x:    -40 },
...
...

  { y:  -8.001009734    , x:    -47 },
  { y:  -8.001009734    , x:    -48 },
  { y:  -8.001009734    , x:    -49 },


];


Solution

  • This answer is in response to: "Do you think [three.js] can replicate what i have in 2D but in 3D? with the rotating circles and stuff?"

    Am not sure whether you're looking to learn 3D modeling from scratch (ie, creating your own library of vector routines, homogeneous coordinate transformations, rendering perspective, etc) or whether you're simply looking to produce a final product. In the case of the latter, three.js is a powerful graphics library built on webGL that in my estimation is simple enough for a beginner to dabble with, but has a lot of depth to produce very sophisticated 3D effects. (Peruse the examples at https://threejs.org/examples/ and you'll see for yourself.)

    I happen to be working a three.js project of my own, and whipped up a quick example of epicyclic circles as a warm up exercise. This involved pulling pieces and parts from the following references...

    The result is a simple scene with one circle running around the other, permitting mouse controls to orbit around the scene, viewing it from different angles and distances.

    Note that I've used basic trigonometry within the animate function to position the epicyclic circle around the center circle, and fudged the rate of rotation for the circles (rather than doing the precise math), but there's probably a better "three.js"-way of doing this via matrices or built in functions. Given that you obviously have a strong math background, I don't think you'll have any issues with translating your 2D model of multi-epicyclic circles using basic trigonometry when porting to 3D.

    Hope this helps in your decision making process on how to proceed with a 3D version of your program.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>Epicyclic Circles</title>
        <style>
          html, body { height: 100%; margin: 0; }
          canvas { display: block; width: 100%; height: 100%; }
        </style>
      </head>
      <body>
        <!-- Pin to a specific version so future changes don't break your page -->
        <script src="https://unpkg.com/three@0.126.1/build/three.min.js"></script>
        <script src="https://unpkg.com/three@0.126.1/examples/js/controls/OrbitControls.js"></script>
    
        <script>
          // Scene, camera, renderer
          const scene = new THREE.Scene();
          scene.background = new THREE.Color(0xf0f0f0);
    
          const camera = new THREE.PerspectiveCamera(
            75,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
          );
          scene.add(camera);
          camera.position.z = 50;
    
          const renderer = new THREE.WebGLRenderer({ antialias: true });
          renderer.setPixelRatio(window.devicePixelRatio);
          renderer.setSize(window.innerWidth, window.innerHeight);
          document.body.appendChild(renderer.domElement);
    
          // Lights
          const light = new THREE.PointLight(0xffffff, 0.9);
          camera.add(light);
          const ambient = new THREE.AmbientLight(0xffffff, 0.3);
          scene.add(ambient);
    
          // Controls
          const controls = new THREE.OrbitControls(camera, renderer.domElement);
          controls.enableDamping = true;
          controls.dampingFactor = 0.25;
          controls.screenSpacePanning = false;
          controls.minDistance = 0;
          controls.maxDistance = 500;
    
          // Geometry settings
          const extrudeSettings = {
            depth: 2,
            bevelEnabled: true,
            bevelSegments: 2,
            steps: 2,
            bevelSize: 0.25,
            bevelThickness: 0.25
          };
    
          // First ring
          const arcShape1 = new THREE.Shape();
          arcShape1.moveTo(0, 0);
          arcShape1.absarc(0, 0, 15, 0, Math.PI * 2, false);
          const holePath1 = new THREE.Path();
          holePath1.moveTo(0, 10);
          holePath1.absarc(0, 10, 2, 0, Math.PI * 2, true);
          arcShape1.holes.push(holePath1);
    
          const geometry1 = new THREE.ExtrudeGeometry(arcShape1, extrudeSettings);
          const mesh1 = new THREE.Mesh(
            geometry1,
            new THREE.MeshPhongMaterial({ color: 0x804000 })
          );
          scene.add(mesh1);
    
          // Second ring
          const arcShape2 = new THREE.Shape();
          arcShape2.moveTo(0, 0);
          arcShape2.absarc(0, 0, 15, 0, Math.PI * 2, false);
          const holePath2 = new THREE.Path();
          holePath2.moveTo(0, 10);
          holePath2.absarc(0, 10, 2, 0, Math.PI * 2, true);
          arcShape2.holes.push(holePath2);
    
          const geometry2 = new THREE.ExtrudeGeometry(arcShape2, extrudeSettings);
          const mesh2 = new THREE.Mesh(
            geometry2,
            new THREE.MeshPhongMaterial({ color: 0x00ff00 })
          );
          scene.add(mesh2);
    
          // Epicyclic motion state
          let mesh2AxisRadius = 30;
          let mesh2AxisAngle = 0;
    
          // Resize handler
          window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
          });
    
          // Animate
          function animate() {
            requestAnimationFrame(animate);
    
            mesh1.rotation.z -= 0.02;
    
            mesh2.rotation.z += 0.02;
            mesh2AxisAngle += 0.01;
            mesh2.position.set(
              mesh2AxisRadius * Math.cos(mesh2AxisAngle),
              mesh2AxisRadius * Math.sin(mesh2AxisAngle),
              0
            );
    
            controls.update();
            renderer.render(scene, camera);
          }
    
          animate();
        </script>
      </body>
    </html>