Hi @paulstgeorge,

Looks like nice progress!

Odd thing, I was messing about with your code and @glv’s code and I ran into this issue: if I tried to fatten up the stroke weight of the gimbals to make them easier to see, the stroke would flicker and clip as the circles rotated. That lead me down a rabbit hole looking for code to make a torus for Processing Java. I suppose there are libraries which have 3D shapes, but I wound up reverse engineering some Unity script here.

In response to thise question:

The three rings should rotate together. My problem is how to rotate the origins of two of the rings relative to the first before I start rotating the group of three.

PShapes may be of interest for doing complicated stuff like this. The benefit is that you have methods that separate and clarify when transformations (like the initial rotations of each ring into orthogonal position) are established in `setup`

and when they are animated in `draw`

. You can also group together `PShape`

s with addChild so they can be rotated all together or individually.

Just to be clear, using sequences of code like `rotateX(1.0); rotateZ(3.0); rotateY(2.0);`

will give you gimbal lock (and `rotateZ`

is bugged out for `PShape`

s last I checked). Using the version of `rotate`

which takes four numbers – one angle and the x, y and z of an axis – will ease the problem.

```
rotate(radians, 1.0, 0.0, 0.0); // rotateX
rotate(radians, 0.0, 1.0, 0.0); // rotateY
rotate(radians, 0.0, 0.0, 1.0); // rotateZ
```

The axis needs to have a length or magnitude of 1. So `(1, 1, 0)`

becomes `(1 / sqrt(2), 1 /sqrt(2), 0)`

. `(1, 1, 1)`

becomes `(1 / sqrt(3), 1 / sqrt(3), 1 / sqrt(3))`

.

Ok, sorry about the big dump of code… but if I broke it apart it’d be harder to copy and paste into the Processing IDE.

```
// Group of all three gimbals
PShape gimbals;
// Each child gimbal
PShape gmblred;
PShape gmblgreen;
PShape gmblblue;
// Axis around which group rotates
PVector axis = new PVector();
// Record of whether or not a key is pressed
boolean xPressed;
boolean yPressed;
boolean zPressed;
float rotateSpeed = radians(0.5);
void setup() {
size(320, 320, P3D);
// Center camera at (0, 0, 0)
camera(
0.0, 0.0, height * 0.86602,
0.0, 0.0, 0.0,
0.0, 1.0, 0.0);
gimbals = createShape(GROUP);
// Set initial gimbals to orthogonal position
// set their fill color
// give them a name
gmblred = torus(120.0, 7.5, 32, 16);
gmblred.setFill(#ff0000);
gmblred.rotate(radians(90), 1.0, 0.0, 0.0);
gmblred.setName("Red Gimbal");
gmblgreen = torus(120.0, 7.5, 32, 16);
gmblgreen.setFill(#00ff00);
gmblgreen.setName("Green Gimbal");
gmblblue = torus(120.0, 7.5, 32, 16);
gmblblue.setFill(#0000ff);
gmblblue.rotate(radians(90), 0.0, 0.0, 1.0);
gmblblue.setName("Blue Gimbal");
// Add children shape to parent shape
gimbals.addChild(gmblred);
gimbals.addChild(gmblgreen);
gimbals.addChild(gmblblue);
}
void draw() {
bvec(xPressed, yPressed, zPressed, axis);
axis.normalize();
if (keyPressed) {
println(axis);
}
gimbals.rotate(rotateSpeed, axis.x, axis.y, axis.z);
background(#fff7d5);
shape(gimbals);
}
void keyPressed() {
if (key == 'x' || key == 'X') {
xPressed = true;
}
if (key == 'y' || key == 'Y') {
yPressed = true;
}
if (key == 'z' || key == 'Z') {
zPressed = true;
}
}
void keyReleased() {
if (key == 'x' || key == 'X') {
xPressed = false;
}
if (key == 'y' || key == 'Y') {
yPressed = false;
}
if (key == 'z' || key == 'Z') {
zPressed = false;
}
}
PVector bvec(boolean x, boolean y, boolean z) {
return bvec(x, y, z, (PVector)null);
}
PVector bvec(
boolean x,
boolean y,
boolean z,
PVector target) {
if (target == null) target = new PVector();
return target.set(
boolToFloat(x),
boolToFloat(y),
boolToFloat(z));
}
float boolToFloat(boolean bool) {
return bool ? 1.0 : 0.0;
}
// Reverse engineered from Bérenger.
// https://wiki.unity3d.com/index.php/ProceduralPrimitives#C.23_-_Torus
PShape torus(
float radius,
float thickness,
int sectors,
int tubeRes) {
int sectors1 = sectors + 1;
int tubeRes1 = tubeRes + 1;
float toU = 1.0 / sectors;
float toV = 1.0 / tubeRes;
float toAngle1 = TAU / sectors;
float toAngle2 = TAU / tubeRes;
// Calculate angle and the v coordinate in uvs.
float[] costs = new float[tubeRes1];
float[] sints = new float[tubeRes1];
float[] vs = new float[tubeRes1];
for (int side = 0; side < tubeRes1; ++side) {
int currSide = side % tubeRes;
float theta = currSide * toAngle2;
costs[side] = cos(theta);
sints[side] = sin(theta);
vs[side] = side * toV;
}
// Create mesh info arrays.
PVector[] coords = new PVector[sectors1 * tubeRes1];
PVector[] normals = new PVector[coords.length];
PVector[] texCoords = new PVector[coords.length];
for (int k = 0, seg = 0; seg < sectors1; ++seg) {
// Calculate theta.
float phi = (seg % sectors) * toAngle1;
float cosp = cos(phi);
float sinp = sin(phi);
// Calculate r1
float r1x = radius * cosp;
float r1y = 0.0f;
float r1z = radius * sinp;
// Calculate horizontal texture coordinate.
float u = seg * toU;
// Calculate quaternion from axis and angle.
// Assumes that the reference up from which
// the quaternion is created is ( 0.0, 1.0, 0.0 ) .
// For that reason qx and qz will be 0.0.
float halfAngle = 0.5 * -phi;
float qw = cos(halfAngle);
float qy = sin(halfAngle);
for (int side = 0; side < tubeRes1; ++side, ++k) {
// Texture coordinate is easy.
texCoords[k] = new PVector(u, vs[side], 0.0);
// Vector to be multiplied by the quaternion.
float mulx = sints[side];
float muly = costs[side];
// Multiply quaternion q and vector mul (part 1).
float iw = -qy * muly;
float ix = qw * mulx;
float iy = qw * muly;
float iz = -qy * mulx;
// Multiply quaternion q and vector mul (part 2).
float r2x = ix * qw + iz * qy;
float r2y = iy * qw - iw * qy;
float r2z = iz * qw - ix * qy;
normals[k] = new PVector(r2x, r2y, r2z);
// Add r1 to r2 to get coordinate.
coords[k] = new PVector(
r1x + r2x * thickness,
r1y + r2y * thickness,
r1z + r2z * thickness);
}
}
int triangles = coords.length * 2;
int idxLimit = (triangles * 3) - 6;
int[][][] faces = new int[triangles][3][3];
int idx = 0;
int fidx = 0;
for (int seg = 0; seg < sectors1; ++seg) {
int currentFac = seg * tubeRes1;
int nextFac = (seg + 1) * tubeRes1;
for (int side = 0; side < tubeRes; ++side) {
int current = side + currentFac;
int next = side + (seg < sectors ? nextFac : 0);
if (idx < idxLimit) {
int n1 = next + 1;
int c1 = current + 1;
faces[fidx++] = new int[][] {
{ current, current, current },
{ next, next, next },
{ n1, n1, n1 } };
faces[fidx++] = new int[][] {
{ current, current, current },
{ n1, n1, n1 },
{ c1, c1, c1 } };
idx += 6;
}
}
}
return shapeMaker(coords, texCoords, normals, faces);
}
PShape shapeMaker(
PVector[] coords,
PVector[] texCoords,
PVector[] normals,
int[][][] faces) {
PShape shape = createShape(GROUP);
// Loop through faces.
int flen0 = faces.length;
for (int i = 0; i < flen0; ++i) {
int[][] f = faces[i];
int flen1 = f.length;
// Create a new polygon.
PShape poly = createShape();
poly.setStroke(false);
poly.setFill(true);
poly.setTextureMode(NORMAL);
poly.beginShape(POLYGON);
// Loop through vertices in a face.
for (int j = 0; j < flen1; ++j) {
int[] data = f[j];
// Retrieve appropriate data from
// indices in the vertex.
int vIndex = data[0];
PVector v = coords[vIndex];
int vtIndex = data[1];
PVector vt = texCoords[vtIndex];
int vnIndex = data[2];
PVector vn = normals[vnIndex];
// Draw the vertex.
poly.fill(0xffffffff);
poly.normal(vn.x, vn.y, vn.z);
poly.vertex(
v.x, v.y, v.z,
vt.x, vt.y);
}
poly.endShape(CLOSE);
shape.addChild(poly);
}
// Tessellate to consolidate all children
// into one parent shape.
shape = shape.getTessellation();
shape.setTextureMode(NORMAL);
return shape;
}
```

Best,

Jeremy