Visualizing the Goniophotometer Problem in Three.js
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.)
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.
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.”
Thank you for reading.