Visualizing the Gonio­pho­to­me­ter Problem in Three.js

Jerry Yin
Dec. 25, 2015
Last Updated: Dec. 23, 2016

Suppose you have a series of points distributed about a sphere — like sensors in a goniophotometer, for example — and you want a good idea of how much area each point is expected to cover. The exact numbers are unimportant: say you’re more interested in a visualization.

We start out with exactly 100 points distributed as evenly as possible about a sphere. Here’s the JSON file of all the 100 points, if you want to play around with it yourself. (I generated the points using Stack Overflow user Fnord’s Python code, which uses Fibonacci spheres.)

Drag any of the demos to rotate.

We can use the following matrix to rotate a vector 10 degrees around the $z$-axis, which draws a circle when applied repeatedly:

\[R = \left[\begin{array}{ccc} \cos(10^\circ) & -\sin(10^\circ) & 0\\\, \sin(10^\circ) & \cos(10^\circ) & 0\\\, 0 & 0 & 1 \end{array}\right]\]

We can use the above matrix as long as we perform a change of basis first, and then do a change of basis after applying $R$.

Let the pivot be the vector

\[ \vec{p}:=\left[\begin{array}{c} a\\\, b\\\, c \end{array}\right]. \]

We can create a change-of-basis matrix $M$ as

\[ M:=\left[\begin{array}{ccc} -\frac{b}{\sqrt{a^{2}+b^{2}}} & ~ & a\\\, \tfrac{a}{\sqrt{a^{2}+b^{2}}} & \vec{c} & b\\\, 0 & & c \end{array}\right] \]

where $\vec{c}$ denotes a unit vector orthogonal to both columns 1 and 3 of $M$, which can be obtained with either the cross product or the Gram-Schmidt process.

Because $M$ is an orthogonal matrix, its inverse is equal to its transpose, so a vector $\vec{v}$ rotated around $\vec{p}$ becomes the vector $MRM^T\vec{v}$.

In three.js, we can use the following code to rotate a vector around a rotationPivot.

var rotationPivot = POINTS[57],
    vector = generateSkewVector(rotationPivot);

var Mcol1 = new THREE.Vector3(-rotationPivot.y, rotationPivot.x, 0).normalize(),
    Mcol2 = new THREE.Vector3().crossVectors(
      new THREE.Vector3(-rotationPivot.y, rotationPivot.x, 0),
      rotationPivot
    ).normalize(),
    matrixM = new THREE.Matrix3().set(
      Mcol1.x, Mcol2.x, rotationPivot.x,
      Mcol1.y, Mcol2.y, rotationPivot.y,
      Mcol1.z, Mcol2.z, rotationPivot.z
    ),
    matrixMinv = matrixM.clone().transpose();

this.rotateOnce = function() {
  // Apply matrix transformations
  vector.applyMatrix3(matrixMinv);
  vector.applyMatrix3(matrixR);
  vector.applyMatrix3(matrixM);

  // Move the arrows and create a point at the new position
  demo2.scene.remove(demo2.scene.getObjectByName("arrow"));

  var arrowRotated = new THREE.ArrowHelper(vector, ORIGIN, 1, CYAN),
      point = new THREE.Mesh(POINT_GEOM, CYAN_MATL);
  arrowRotated.name = "arrow";
  point.name = "point";
  point.position.set(vector.x, vector.y, vector.z);

  demo2.scene.add( arrowRotated, point );
};

If we call rotateOnce() 35 times, we will have drawn a circle. We can call it an additional time to return the arrow back to where it began.

Replay Animation

We can create a vector at an angle THETA from each pivot with the following code:

function generateSkewVector(vector) {
  var x = vector.x,
      y = vector.y,
      z = vector.z,
      // Convert to spherical
      r = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)),
      theta = Math.atan2(y, x),
      phi = Math.acos(z / r) + THETA;

  if (phi > Math.PI) {
    // Check if we moved into the next quadrant
    phi = Math.PI - (phi - Math.PI);
    theta += Math.PI;
  }

  // Convert back to Cartesian
  x = r * Math.cos(theta) * Math.sin(phi);
  y = r * Math.sin(theta) * Math.sin(phi);
  z = r * Math.cos(phi);

  return new THREE.Vector3(x, y, z);
}

Then we can reuse much of the rotateOnce() code, generating and storing a change of basis matrix for every pivot. Then we can rotate each generated skew vector around each pivot repeatedly, drawing a circle around each point — this can be roughly interpreted as the area each point should “cover.”

Replay Animation

Thank you for reading.