project:
harmono×graph
description:

Rediscovering the inherent possibilities arising from the limitations encountered in early computer generated and aided graphics, inspired by the motion graphics approaches of John Whitney Snr. A computational arts research and theory project.

title:
Defining a test bed
timestamp:
March 16, 2021
excerpt:
In order to try out some of the ideas researched in this project, it's useful to define a standard testbed which we can apply these too.
heroImage:
content:

Introduction

In order to try out some of the techniques, and limitations as part of this research - it's going to be useful to have a test bed that we can try the ideas out on. This post describes that test bed.

If we go back to something like the film Lapis, we can see that it was created by placing images, and lights onto rotating discs which were then controlled by the gun computer, stepping through various positions as each frame is captured.

This rotational motion - not unlike the motion captured in a child's spirograph should provide us with some opportunities to create a basic system into which we can look at various effects, and adding them in real time. As part of this exercise however, we want to be able to step outside of the restrictions that this physical approach would have had - and make use of the virtual environment that we will create.

We can achieve this in quite a straightforward way with two small additions to the model of rotating objects in a flat plane - namely

  1. Allow rotations in three dimensions
  2. Create object trees which allow for objects to be chained together to create more complex interactions

This essay will define how we will build our test bed, and some thoughts on how we might use it.

Test Bed Definition

The test bad will consist of a single node based structure, where the first node is a rotor attached the 3D space origin, and from that rotor which describes movement around an axis in 3D space further nodes can be attached, either subsequent rotors or drawable nodes.

Rotors

A rotor can be thought of as a ring located in a 3D space, spinning in 1 or more dimensions which has one or more presentation objects attached to it at points around that ring.

We need to define and use the following attributes for this object

  1. It's location in 3D space
  2. It's rotation in each of three dimensions - these can be
  • Absolute - defined as the arc in radians translated in a unit of time - for ease of computation we can define this as per frame
  • Relative - a multiplier to a defined reference, for example 0.75x - which be 0.75 x X rotation rate. We can of course build chains of these, and it will certainly be interesting to define these in terms of rotors within the chain of objects.
  1. It's child nodes, which could be additional rotors, or drawable objects, and where these are attached to the rotor - defined as a polar co-ordinate (⍴ - radial,θ -angular)

Drawable Nodes

Drawable nodes represent something which can de drawn a point in space, and are attached to a rotor using polar co-ordinates.

There are many possibilities for drawable nodes, the following will initially be considered

Primitive Shape / Solid

A shape or solid such as circle, or cube - it should have basic material attributes such as color. We can see in the following example a single rotor with three coloured spheres placed equidistant on the ring, which is then rotated through three dimensions about it's axis.

animation && source:
/*

  File: singleRotor.jsx
  Kind: ThreeJS canvas
  Description: Example of a single rotor in action, with dat gui controls

*/

import React, { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { OrbitControls } from '@react-three/drei';

const Rotor = ({ radius, size }) => {
  const rotor = useRef();
  const rotation = [0,0,0];

  useFrame(() => {
    if (rotor.current) {
      rotor.current.rotation.x += 0.0321;
      rotor.current.rotation.y += 0.00201;
      rotor.current.rotation.z += 0.0797;
    }
  });

  const ringRadius = radius + (size/2);
  const theta = 2 * Math.PI / 3;


  return (
    <group ref={rotor} rotation={rotation}>
      <mesh>
        <torusGeometry args={[ringRadius, 1, 8, 32]} />
        <meshBasicMaterial color="#999999" wireframe />
      </mesh>
      <mesh position={[ringRadius,0,0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#FF00FF" wireframe />
      </mesh>
      <mesh position={[Math.cos( theta) * ringRadius, Math.sin( theta) * ringRadius, 0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#00FFFF" wireframe />
      </mesh>
      <mesh position={[Math.cos( 2*theta) * ringRadius, Math.sin( 2*theta) * ringRadius, 0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#FFFF00" wireframe />
      </mesh>
    </group>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={10} />
      <spotLight
        castShadow
        intensity={0.2}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

export default function SingleRotor() {
  return (
    <Canvas shadowMap style={{ backgroundColor: '#000021' }} orthographic camera={{ zoom: 5, position: [10, 20, 50] }} >
      <OrbitControls />
      <Lights />
      <axesHelper args={[75]}/>
      <Rotor radius={50} size={3} />
    </Canvas>
  );
}
single rotor example
source:with.lasers

We can see if we remove the axis, and the display of the ring, how even at this straightforward level we begin to capture an interesting range of movement from each of the spheres.

animation && source:
/*

  File: singleRotor.jsx
  Kind: ThreeJS canvas
  Description: Example of a single rotor in action, with dat gui controls

*/

// import * as THREE from 'three';
import React, { useRef } from 'react';
import { Canvas, extend, useFrame, useThree } from 'react-three-fiber';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

extend({ OrbitControls });

const CameraControls = () => {
  // Get a reference to the Three.js Camera, and the canvas html element.
  // We need these to setup the OrbitControls component.
  // https://threejs.org/docs/#examples/en/controls/OrbitControls
  const {
    camera,
    gl: { domElement },
  } = useThree();
  // Ref to the controls, so that we can update them on every frame using useFrame
  const controls = useRef();
  useFrame((state) => controls.current.update());
  return <orbitControls ref={controls} args={[camera, domElement]} />;
};

const Rotor = ({ radius, size }) => {
  const rotor = useRef();
  const rotation = [0,0,0];

  useFrame(() => {
    if (rotor.current) {
      rotor.current.rotation.x += 0.0321;
      rotor.current.rotation.y += 0.00201;
      rotor.current.rotation.z += 0.0797;
    }
  });

  const ringRadius = radius + (size/2);
  const theta = 2 * Math.PI / 3;


  return (
    <group ref={rotor} rotation={rotation}>
      <mesh visible={false}>
        <torusGeometry args={[ringRadius, 1, 8, 32]} />
        <meshBasicMaterial color="#999999" wireframe />
      </mesh>
      <mesh position={[ringRadius,0,0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#FF00FF" wireframe />
      </mesh>
      <mesh position={[Math.cos( theta) * ringRadius, Math.sin( theta) * ringRadius, 0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#00FFFF" wireframe />
      </mesh>
      <mesh position={[Math.cos( 2*theta) * ringRadius, Math.sin( 2*theta) * ringRadius, 0]}>
        <sphereGeometry args={[size, 32, 32]}/>
        <meshBasicMaterial color="#FFFF00" wireframe />
      </mesh>
    </group>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={10} />
      <spotLight
        castShadow
        intensity={0.2}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

export default function SingleRotor() {
  return (
    <Canvas shadowMap style={{ backgroundColor: '#000021' }} orthographic camera={{ zoom: 5, position: [10, 20, 50] }} >
      <CameraControls />
      <Lights />
      <Rotor radius={50} size={3} />
    </Canvas>
  );
}
single rotor example
source:with.lasers

Nesting Rotors

As mentioned above, we can create a node like structure where we nest rotors creating more complex rotational systems - depending on how we set these up - they start to have some affinity with Fourier Series and it's ability draw complex shapes by tracing a point in 2D space. We can see in the following example what happens when we take three rings, and connect them in a hierarchy, maintaining three spheres attached to each rotor.

animation && source:
/*

  File: nestedRotor.jsx
  Kind: ThreeJS canvas
  Description: Example of a nested rotors

*/

import * as THREE from 'three';
import React, { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { OrbitControls } from '@react-three/drei';

const Rotors = ({ radius1, size1, radius2, size2, radius3, size3, trail, trailInterval }) => {
  const rotor1 = useRef();
  const rotor2 = useRef();
  const rotor3 = useRef();

  const rotation = [0, 0, 0];

  useFrame(() => {
    if (rotor1.current) {
      rotor1.current.rotation.x += 0.0321  / 6;
      rotor1.current.rotation.y += 0.00201  / 5.3;
      rotor1.current.rotation.z += 0.0797   / 4.9;
    }
    if (rotor2.current) {
      rotor2.current.rotation.x += 0.000321;
      rotor2.current.rotation.y += 0.0201;
      rotor2.current.rotation.z -= 0.0597;
    }
    if (rotor3.current) {
      rotor3.current.rotation.x += 0.10321 / 3;
      rotor3.current.rotation.y += 0.00099;
      rotor3.current.rotation.z -= 0.00006;
    }


    
  });

  const ringRadius = radius1 + size1 / 2;
  const theta = (2 * Math.PI) / 3;

  const ringRadius2 = radius2 + size2 / 2;
  const ringRadius3 = radius3 + size3 / 2;

  return (
    <group>
      <group ref={rotor1} rotation={rotation}>
        <mesh visible={true}>
          <torusGeometry args={[ringRadius, 0.5, 8, 32]} />
          <meshBasicMaterial color="#999999" wireframe side={THREE.DoubleSide} />
        </mesh>
        <mesh  position={[ringRadius, 0, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#8800FF" wireframe side={THREE.DoubleSide}/>
        </mesh>
        <mesh  position={[Math.cos(theta) * ringRadius, Math.sin(theta) * ringRadius, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#00FF88" wireframe side={THREE.DoubleSide}/>
        </mesh>
        <mesh position={[Math.cos(2 * theta) * ringRadius, Math.sin(2 * theta) * ringRadius, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#FF8800" wireframe side={THREE.DoubleSide}/>
        </mesh>
        <group ref={rotor2} rotation={rotation} position={[ringRadius, 0, 0]}>
          <mesh visible={true}>
          <torusGeometry args={[ringRadius2, 0.5, 8, 32]} />
            <meshBasicMaterial color="#999999" wireframe side={THREE.DoubleSide} />
          </mesh>
          <mesh  position={[ringRadius2, 0, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#0000FF" wireframe side={THREE.DoubleSide}/>
          </mesh>
          <mesh  position={[Math.cos(theta) * ringRadius2, Math.sin(theta) * ringRadius2, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#00FF00" wireframe />
          </mesh>
          <mesh  position={[Math.cos(2 * theta) * ringRadius2, Math.sin(2 * theta) * ringRadius2, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#FF0000" wireframe side={THREE.DoubleSide}/>
          </mesh>
          <group ref={rotor3} rotation={rotation} position={[ringRadius2, 0, 0]}>
            <mesh visible={true}>
            <torusGeometry args={[ringRadius3, 0.5, 8, 32]} />
              <meshBasicMaterial color="#999999" wireframe side={THREE.DoubleSide} />
            </mesh>
            <mesh  position={[ringRadius3, 0, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#FF00FF" wireframe side={THREE.DoubleSide}/>
            </mesh>
            <mesh position={[Math.cos(theta) * ringRadius3, Math.sin(theta) * ringRadius3, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#00FFFF" wireframe side={THREE.DoubleSide}/>
            </mesh>
            <mesh  position={[Math.cos(2 * theta) * ringRadius3, Math.sin(2 * theta) * ringRadius3, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#FFFF00" wireframe side={THREE.DoubleSide}/>
            </mesh>
          </group>
        </group>
      </group>
    </group>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={10} />
      <spotLight
        castShadow
        intensity={0.2}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

const NestedRotor = () => {
  return (
    <Canvas
      style={{ backgroundColor: '#000021' }}
      orthographic
      camera={{ zoom: 3, position: [10, 20, 50] }}
    >
      <OrbitControls />
      <Lights />
      <Rotors radius1={50} size1={2} radius2={30} size2={2} radius3={40} size3={2} trail={15} trailInterval={7} />
      <axesHelper args={[75]} />
    </Canvas>
  );
}

export default NestedRotor;
nested rotor example
source:with.lasers

Again by removing the display of the rings, we can begin to see the possibilities offered from a more complex rotational system - in this case we have enhanced the effect by creating a trail of spheres that track each sphere as it moves through space - this begins to elude to the mechanics of things like capturing motion blur occurring due to movement of the sphere whilst the 'shutter' is open. In this particular example we have also made use of the Unreal Bloom fragment shader to add a neon like effect - and we already start to see steps towards a complete and interesting visual effect.

animation && source:
/*

  File: singleRotor.jsx
  Kind: ThreeJS canvas
  Description: Example of a single rotor in action, with dat gui controls

*/

import * as THREE from 'three';
import React, { useRef, } from 'react';
import { Canvas, extend, useFrame } from 'react-three-fiber';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { OrbitControls, Effects } from '@react-three/drei';


extend({ UnrealBloomPass });

const Rotors = ({ radius1, size1, radius2, size2, radius3, size3, trail, trailInterval }) => {
  const rotor1 = useRef();
  const rotor2 = useRef();
  const rotor3 = useRef();

  const ball0 = useRef();
  const ball1 = useRef();
  const ball2 = useRef();

  const ball3 = useRef();
  const ball4 = useRef();
  const ball5 = useRef();

  const ball6 = useRef();
  const ball7 = useRef();
  const ball8 = useRef();

  const rotation = [0, 0, 0];


  const trail0 = useRef();
  const trail1 = useRef();
  const trail2 = useRef();

  const trail3 = useRef();
  const trail4 = useRef();
  const trail5 = useRef();

  const trail6 = useRef();
  const trail7 = useRef();
  const trail8 = useRef();

  const trails = [
    {
      id: 0,
      sourceRef: ball0,
      targetRef: trail0,
      pos: []
    },
    {
      id: 1,
      sourceRef: ball1,
      targetRef: trail1,
      pos: []
    },
    {
      id: 2,
      sourceRef: ball2,
      targetRef: trail2,
      pos: []
    },
    {
      id: 3,
      sourceRef: ball3,
      targetRef: trail3,
      pos: []
    },
    {
      id: 4,
      sourceRef: ball4,
      targetRef: trail4,
      pos: []
    },
    {
      id: 5,
      sourceRef: ball5,
      targetRef: trail5,
      pos: []
    },
    {
      id: 6,
      sourceRef: ball6,
      targetRef: trail6,
      pos: []
    },
    {
      id: 7,
      sourceRef: ball7,
      targetRef: trail7,
      pos: []
    },
    {
      id: 8,
      sourceRef: ball8,
      targetRef: trail8,
      pos: []
    },
  ]


  const tempObject = new THREE.Object3D();
  const trailPosLength = trail * trailInterval


  const origin = new THREE.Vector3(0,0,0);

  useFrame(() => {
    if (rotor1.current) {
      rotor1.current.rotation.x += (0.0321 + (Math.random() * 0.1)) / 6;
      rotor1.current.rotation.y += (0.00201 + (Math.random() * 0.1)) / 5.3;
      rotor1.current.rotation.z += (0.0797 + (Math.random() * 0.1))  / 4.9;
    }
    if (rotor2.current) {
      rotor2.current.rotation.x += 0.000321;
      rotor2.current.rotation.y += 0.0201;
      rotor2.current.rotation.z -= 0.0597;
    }
    if (rotor3.current) {
      rotor3.current.rotation.x += 0.10321;
      rotor3.current.rotation.y += 0.00099;
      rotor3.current.rotation.z -= 0.00006;
    }

    for (let i=0; i<trails.length; i++) {
      if (trails[i].sourceRef.current) {
        const worldPos = trails[i].sourceRef.current.getWorldPosition(origin);
        trails[i].pos.unshift(new THREE.Vector3(worldPos.x, worldPos.y, worldPos.z)); //record current position
        if (trails[i].pos.length > trailPosLength) {
          trails[i].pos.splice(trailPosLength);    //push last value off the end
        }  
      }
      for (let j=0; j<trail; j++) {
        const j_ = ((j+1) * trailInterval);
        let scale = 0;
        if (trails[i].pos.length > j_) {
          scale = Math.max((1 - (j * 0.09)),0.0); 
          tempObject.position.set(trails[i].pos[j_].x, trails[i].pos[j_].y, trails[i].pos[j_].z);
        }
        tempObject.scale.set(scale, scale, scale);
        tempObject.updateMatrix();
        trails[i].targetRef.current.setMatrixAt(j, tempObject.matrix);
      }
      trails[i].targetRef.current.instanceMatrix.needsUpdate = true;
    }
  });

  const ringRadius = radius1 + size1 / 2;
  const theta = (2 * Math.PI) / 3;

  const ringRadius2 = radius2 + size2 / 2;
  const ringRadius3 = radius3 + size3 / 2;

  return (
    <group>
      <group ref={rotor1} rotation={rotation}>
        <mesh visible={false}>
          <torusGeometry args={[radius1, size1 / 4, 3, 32]} />
          <meshBasicMaterial color="#999999" wireframe />
        </mesh>
        <mesh ref={trails[6].sourceRef} position={[ringRadius, 0, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#8800FF" wireframe />
        </mesh>
        <mesh ref={trails[7].sourceRef} position={[Math.cos(theta) * ringRadius, Math.sin(theta) * ringRadius, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#00FF88" wireframe />
        </mesh>
        <mesh ref={trails[8].sourceRef} position={[Math.cos(2 * theta) * ringRadius, Math.sin(2 * theta) * ringRadius, 0]}>
          <sphereGeometry args={[size2, 32, 32]} />
          <meshBasicMaterial color="#FF8800" wireframe />
        </mesh>
        <group ref={rotor2} rotation={rotation} position={[ringRadius, 0, 0]}>
          <mesh visible={false}>
            <torusGeometry args={[radius2, size2 / 4, 3, 32]} />
            <meshBasicMaterial color="#999999" wireframe />
          </mesh>
          <mesh ref={trails[3].sourceRef} position={[ringRadius2, 0, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#0000FF" wireframe />
          </mesh>
          <mesh ref={trails[4].sourceRef} position={[Math.cos(theta) * ringRadius2, Math.sin(theta) * ringRadius2, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#00FF00" wireframe />
          </mesh>
          <mesh ref={trails[5].sourceRef} position={[Math.cos(2 * theta) * ringRadius2, Math.sin(2 * theta) * ringRadius2, 0]}>
            <sphereGeometry args={[size2, 32, 32]} />
            <meshBasicMaterial color="#FF0000" wireframe />
          </mesh>
          <group ref={rotor3} rotation={rotation} position={[ringRadius2, 0, 0]}>
            <mesh visible={false}>
              <torusGeometry args={[radius3, size3 / 4, 3, 32]} />
              <meshBasicMaterial color="#999999" wireframe />
            </mesh>
            <mesh ref={trails[0].sourceRef} position={[ringRadius3, 0, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#FF00FF" wireframe />
            </mesh>
            <mesh ref={trails[1].sourceRef} position={[Math.cos(theta) * ringRadius3, Math.sin(theta) * ringRadius3, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#00FFFF" wireframe />
            </mesh>
            <mesh ref={trails[2].sourceRef} position={[Math.cos(2 * theta) * ringRadius3, Math.sin(2 * theta) * ringRadius3, 0]}>
              <sphereGeometry args={[size3, 32, 32]} />
              <meshBasicMaterial color="#FFFF00" wireframe />
            </mesh>
          </group>
        </group>
      </group>
      <instancedMesh ref={trails[0].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#FF00FF" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[1].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#00FFFF" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[2].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#FFFF00" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[3].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#0000FF" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[4].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#00FF00" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[5].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#FF0000" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[6].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#8800FF" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[7].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#00FF88" wireframe/>
      </instancedMesh>
      <instancedMesh ref={trails[8].targetRef} args={[null, null, trail]}>
        <sphereGeometry attach="geometry" args={[size3, 32, 32]} />
        <meshBasicMaterial color="#FF8800" wireframe/>
      </instancedMesh>
    </group>
  );
};


function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={10} />
      <spotLight
        castShadow
        intensity={0.2}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}


const SphericalTrails = () => {
  
  return (
    <Canvas
      shadowMap
      style={{ backgroundColor: '#000021' }}
      camera={{ position: [10, 20, 140], fov: 80 }}
      data-id="sphericalTrails"
    >
      <OrbitControls />
      <Lights />
      {/* <Axes length={75} /> */}
      <Rotors radius1={50} size1={2} radius2={30} size2={2} radius3={40} size3={2} trail={12} trailInterval={4} />
      <Effects>
        <unrealBloomPass attachArray="passes" args={[undefined, 2.1, 0.1, 0.1]} />
      </Effects>


    </Canvas>
  );
}

export default SphericalTrails;
simple spherical trails example
source:with.lasers

We can further see the nature of how complex paths are created by such as system if we create trails based on something like threejss tubeGeometry where we can use the positions the sphere's have passed though to create a path that generates that tube geometry from.

animation && source:
/*

  File: tube.jsx
  Kind: ThreeJS canvas
  Description: Example of a single rotor in action, with dat gui controls

*/

import React, { useRef } from 'react';
import PropTypes from 'prop-types';

import * as THREE from 'three';
import { Canvas, extend, useFrame } from 'react-three-fiber';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { OrbitControls, Effects } from '@react-three/drei';

extend({ RenderPass, UnrealBloomPass });

// Rotor as component
const Rotor = ({ position, rotation, spin, size, children, visible }) => {
  const rotor = useRef();

  useFrame(() => {
    rotor.current.rotation.x += (spin[0] / 3);
    rotor.current.rotation.y += (spin[1] / 3);
    rotor.current.rotation.z += (spin[2] / 2);
  });

  return (
    <group ref={rotor} rotation={rotation} position={position}>
      <mesh visible={false}>
        <torusGeometry args={[size, 0.5, 4, 32]} />
        <meshBasicMaterial color={0x666666} wireframe />
      </mesh>
      {children}
    </group>
  );
};

const Positions = {};

const Ball = ({ id, position, size, color, visible }) => {

  const ball = useRef();

  //const p = new THREE.Vector3();

  useFrame(() => {
    // console.log(Positions[id],ball.current);
    if (ball.current && Positions[id]) {
      const p = new THREE.Vector3();
      ball.current.getWorldPosition(p);
      Positions[id].unshift(p);
      Positions[id].splice(-1,1);
    }
  });

  return (
    <>
      <mesh ref={ball} position={position} visible={visible}>
        <sphereGeometry args={[size, 32, 32]} />
        <meshBasicMaterial color={color} wireframe/>
      </mesh>
    </>
  );
};

const Trail = ({ id, size, color, length, radial, segments }) => {
  
  const trail = useRef();

  Positions[id] = Array.from({length:length}, () =>  new THREE.Vector3());

  let d = 0;

  //Initial curve and tube
  const curve = new THREE.CatmullRomCurve3(Positions[id]);
  const tubeGeometry = new THREE.TubeGeometry(curve, length * segments, size, radial);

  const indices = tubeGeometry.getIndex();
  const vertices = tubeGeometry.getAttribute('position');

  //Update curve and tube
  useFrame(()=>{
    //new tube from updated points, then transfer vertices to current geometry
    const curve = new THREE.CatmullRomCurve3(Positions[id]);
    const tube = new THREE.TubeGeometry(curve, length * segments, size, radial);
    trail.current.geometry.setAttribute('position', tube.getAttribute('position'));
    trail.current.geometry.attributes.position.needsUpdate = true;
    trail.current.visible = (d > length);
    d++;
  });



  return (
    <mesh ref={trail}>
      <bufferGeometry attach="geometry">
        <bufferAttribute attach="index" array={indices.array} count={indices.array.length} itemSize={1} />
        <bufferAttribute
          attachObject={['attributes', 'position']}
          array={vertices.array}
          count={vertices.array.length / 3}
          itemSize={3}
        />
      </bufferGeometry>
      <meshBasicMaterial color={color} wireframe />
    </mesh>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={10} />
      <spotLight
        castShadow
        intensity={0.2}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

const TubeTrails = ({ running }) => {

  return (
    <Canvas shadowMap style={{ backgroundColor: '#000021' }} camera={{ position: [10, 20, 80], fov: 80 }}>
      {/* <axesHelper args={[75]} /> */}
      <OrbitControls />
      <Lights />
      <Rotor id="r1" position={[0, 20, 0]} rotation={[0, 0, 0]} spin={[0.000123, 0.00067, 0.01]} size={50} visible>
        <Rotor id="r2" position={[0, 50, 0]} rotation={[0, 0, 0]} spin={[0.01, -0.0001, 0.0775]} size={40} visible>
          <Rotor id="r3" position={[40, 0, 0]} rotation={[0, 0, 0]} spin={[-0.021, 0.075, 0.1275]} size={30} visible>
            <Rotor
              id="r4"
              position={[Math.cos(60) * 30, Math.sin(60) * 30, 0]}
              rotation={[0, 0, 0]}
              spin={[-0.001, 0.175, 0.5275]}
              size={20}
              visible
            >
              <Rotor
                id="r5"
                position={[20, 0, 0]}
                rotation={[0, 0, 0]}
                spin={[-0.021, 0.075, 0.1275]}
                size={15}
                visible
              >
                <Ball
                  id="r5b1"
                  position={[Math.cos(42) * 15, Math.sin(42) * 15, 0]}
                  size={1}
                  color={0x0089FE}
                  visible
                />
                <Ball
                  id="r5b2"
                  position={[Math.cos(137) * 15, Math.sin(137) * 15, 0]}
                  size={1}
                  color={0xF08918}
                  visible
                />
                <Ball
                  id="r5b3"
                  position={[Math.cos(221) * 15, Math.sin(221) * 15, 0]}
                  size={1}
                  color={0xFF190A}
                  visible
                />
                <Rotor id="r3" position={[Math.cos(221) * 15, Math.sin(221) * 15, 0]} rotation={[0, 0, 0]} spin={[-0.00021, 0.000075, 1.4275]} size={5} visible>
                <Ball
                  id="r6b1"
                  position={[5, 0, 0]}
                  size={1.5}
                  color={0x00FE01}
                  visible
                />
                </Rotor>
              </Rotor>
            </Rotor>
          </Rotor>
        </Rotor>
      </Rotor>
      <group>
        <Trail id="r5b1" size={1} length={50} segments={5} radial={6} color={0x0089FE} />
        <Trail id="r5b2" size={1} length={50} segments={5} radial={6} color={0xF08918} />
        <Trail id="r5b3" size={1} length={50} segments={5} radial={6} color={0xFF190A} />
        <Trail id="r6b1" size={1.5} length={80} segments={5} radial={6} color={0x00FE01} />
      </group>
      <Effects>
        <unrealBloomPass attachArray="passes" args={[undefined, 0.6, 0.3, 0]} />
      </Effects>
    </Canvas>
  );
};

TubeTrails.propTypes = {
  running: PropTypes.bool,
};

TubeTrails.defaultProps = {
  running: true,
};

export default TubeTrails;
simple tube trails example
source:with.lasers

Image / Texture

James Whiteny created the images for his film Lapis by "meticulously painting the patterns of pin-point dots on paper cards"1 we can recreate this effect by making use of textures attached to objects, attached to the rotors. In the following example we have attached a single plane to nested rotor, painted it with a texture, and set it to always look at the camera so it remains relatively flat to the viewer. This combination is less interesting at first glance, but will offer interesting possibilties once effects like motion blur, or slit scan are added.

animation && source:
/*

  File: rotorWithPlane.jsx
  Kind: ThreeJS canvas
  Description: Example of a nested rotors, with a plane attached to the final rotor.

*/

import React, { useRef, Suspense } from 'react';
import PropTypes from 'prop-types';

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

import { Canvas, extend, useFrame, useThree, useLoader } from 'react-three-fiber';
import { TextureLoader } from 'three/src/loaders/TextureLoader.js';

extend({ OrbitControls });

const CameraControls = () => {
  // Get a reference to the Three.js Camera, and the canvas html element.
  // We need these to setup the OrbitControls component.
  // https://threejs.org/docs/#examples/en/controls/OrbitControls
  const {
    camera,
    gl: { domElement },
  } = useThree();
  // Ref to the controls, so that we can update them on every frame using useFrame
  const controls = useRef();
  useFrame((state) => controls.current.update());
  return <orbitControls ref={controls} args={[camera, domElement]} />;
};

// Rotor as component
const Rotor = ({ position, rotation, spin, size, children, visible }) => {
  const rotor = useRef();

  useFrame(() => {
    rotor.current.rotation.x += spin[0] / 3;
    rotor.current.rotation.y += spin[1] / 3;
    rotor.current.rotation.z += spin[2] / 2;
  });

  return (
    <group ref={rotor} rotation={rotation} position={position}>
      <mesh visible={visible}>
        <torusGeometry args={[size, 0.5, 4, 32]} />
        <meshBasicMaterial color={0x666666} wireframe />
      </mesh>
      {children}
    </group>
  );
};


const TexturePlane = ({ position, rotation, color, isBillboard }) => {
  const texture = useLoader(TextureLoader, '/threejs/textures/PlaneTexture.png');

  const plane = useRef();

  useFrame(({ camera }) => {
    //ensure the plane is always pointing towards the camera
    plane.current.lookAt(camera.position);
    //plane.current.quaternion.copy( camera.quaternion );
  });

  return (
    <mesh ref={plane} position={position} rotation={rotation}>
      <planeBufferGeometry attach="geometry" args={[60, 60, 1, 1]} />
      <meshStandardMaterial map={texture} attach="material" side={THREE.DoubleSide} />
    </mesh>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={1} />
      <spotLight
        castShadow
        intensity={0.1}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

const TubeTrails = ({ running }) => {
  return (
    <Canvas shadowMap style={{ backgroundColor: '#000021' }} orthographic camera={{ position: [10, 20, 80], fov: 80 }}>
      {/* <axesHelper args={[75]} /> */}
      <CameraControls />
      <Lights />
      <Rotor id="r1" position={[0, 20, 0]} rotation={[0, 0, 0]} spin={[0.000123, 0.00067, 0.01]} size={50} visible>
        <Rotor id="r2" position={[0, 50, 0]} rotation={[0, 0, 0]} spin={[0.01, -0.0001, 0.0775]} size={40} visible>
          <Rotor id="r3" position={[40, 0, 0]} rotation={[0, 0, 0]} spin={[-0.021, 0.075, 0.1275]} size={30} visible>
            <Suspense fallback={null}>
              <TexturePlane position={[Math.cos(0) * 30, Math.sin(0) * 30, 0]} rotation={[90, 0, 0]} color={0x0089fe} />
            </Suspense>
          </Rotor>
        </Rotor>
      </Rotor>
    </Canvas>
  );
};

TubeTrails.propTypes = {
  running: PropTypes.bool,
};

TubeTrails.defaultProps = {
  running: true,
};

export default TubeTrails;
nested rotors, with a single plane
source:with.lasers

There are other possibilities we could look at along these lines - we could for example create planes with holes in, and use those in conjunction with light and shadow, or just as a way of revealing other objects.

Animated Billboard

A simple 2d grid of objects which can be further animated - for example a grid of 4 x 4 spheres where visibility, colour etc can be changed. This idea derives from some initial thoughts on creating a physical harmonograph - and would have been achieved by use of something like a 8x8 LED matrix - and a simple example follows, using threes instanced object.

animation && source:
/*

  File: rotorWithAnimatedObject.jsx
  Kind: ThreeJS canvas
  Description: Example of a nested rotors, with a an instanced object of spheres in a grid which are animated randomly

*/

import React, { useRef } from 'react';
// import PropTypes from 'prop-types';

import * as THREE from 'three';

import { Canvas, extend, useFrame } from 'react-three-fiber';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { OrbitControls, Effects } from '@react-three/drei';


extend({ UnrealBloomPass });

// Rotor as component
const Rotor = ({ position, rotation, spin, size, children, visible }) => {
  const rotor = useRef();

  useFrame(() => {
    rotor.current.rotation.x += spin[0] / 3;
    rotor.current.rotation.y += spin[1] / 3;
    rotor.current.rotation.z += spin[2] / 2;
  });

  return (
    <group ref={rotor} rotation={rotation} position={position}>
      <mesh visible={false}>
        <torusGeometry args={[size, 0.5, 4, 32]} />
        <meshBasicMaterial color={0x666666} wireframe />
      </mesh>
      {children}
    </group>
  );
};

const SphereGrid = ({ position, rotation, number, size, spacing, color }) => {
  const sgRef = useRef();
  const n = number * number;
  let first = true;
  const tempObject = new THREE.Object3D();
  const tempColor = new THREE.Color();
  const rot = (Math.random() * 0.178) + 0.012

  const sphereState = Array.from({length: n}).fill().map(()=>true);

  //Animation - and set position on first pass
  useFrame(() => {

    if (first) {
      for (let i=0; i<n; i++) {
        const x = i / number;
        const y = i % number;
        tempObject.position.set(x * spacing, y * spacing, 0);
        //tempObject.scale.set(1);
        tempObject.updateMatrix();
        sgRef.current.setMatrixAt(i, tempObject.matrix);
      }
      sgRef.current.instanceMatrix.needsUpdate = true;
      first = false;
    }

     sgRef.current.rotation.z += rot;

      const i = Math.floor(Math.random() * n);
      sphereState[i] = !sphereState[i];
      const tcolor = sphereState[i] ? color : 0x000021
      tempColor.setHex(tcolor);
      sgRef.current.setColorAt(i, tempColor);
      sgRef.current.instanceColor.needsUpdate = true;



  });

  return (
    <instancedMesh ref={sgRef} args={[null, null, n]} rotation={rotation} position={position}>
      <sphereGeometry attach="geometry" args={[size, 32, 32]} />
      <meshBasicMaterial color={color} wireframe />
    </instancedMesh>
  );
};

function Lights() {
  return (
    <group>
      <pointLight intensity={0.3} />
      <ambientLight intensity={1} />
      <spotLight
        castShadow
        intensity={0.1}
        angle={Math.PI / 7}
        position={[150, 150, 250]}
        penumbra={1}
        shadow-mapSize-width={2048}
        shadow-mapSize-height={2048}
      />
    </group>
  );
}

const AnimatedRotor = () => {
  return (
    <Canvas shadowMap style={{ backgroundColor: '#000021' }} camera={{ position: [10, 20, 80], fov: 80 }}>
      {/* <axesHelper args={[75]} /> */}
      <OrbitControls />
      <Lights />
      <Rotor id="r1" position={[0, 20, 0]} rotation={[0, 0, 0]} spin={[0.000123, 0.00067, 0.01]} size={50} visible>
        <SphereGrid position={[Math.cos(0) * 50, Math.sin(0) * 50, 0]} rotation={[90, 0, 0]} color={0xFF190A} number={7} size={3} spacing={10} />
        <Rotor id="r2" position={[0, 50, 0]} rotation={[0, 0, 0]} spin={[0.01, -0.0001, 0.0775]} size={40} visible>
        <SphereGrid position={[Math.cos(30) * 40, Math.sin(30) * 40, 0]} rotation={[90, 0, 0]} color={0x00FE01} number={3} size={5} spacing={15} />
          <Rotor id="r3" position={[40, 0, 0]} rotation={[0, 0, 0]} spin={[-0.021, 0.075, 0.1275]} size={30} visible>
            <SphereGrid position={[Math.cos(0) * 30, Math.sin(0) * 30, 0]} rotation={[90, 0, 0]} color={0x0089fe} number={4} size={2} spacing={8} />
          </Rotor>
        </Rotor>
      </Rotor>
      <Effects>
        <unrealBloomPass attachArray="passes" args={[undefined, 2.1, 0.1, 0.1]} />
      </Effects>
    </Canvas>
    
  );
};

export default AnimatedRotor;
nested rotors, with a animated object
source:with.lasers

  1. "William Moritz on James Whitney's Yantra and Lapis" source