Yes, referring to your second example.
In it, after a 90 degree rotation around y, the red dot travels up the side of the sphere when you move the mouse upward.
What I want is, with the same setup, that the red dot stays in its place and the point of the sphere in the center of the sketch, moves upward. This is no longer achievable by rotateX(), because a rotation matrix has already been applied.
I should note that I actually switched to p5js because it allows me to rotate around a specified axis, instead of just x, y and z.
Thank you for your effort, but that only gets me to the point of where I quit in p5.
I tried my best to understand some of the maths in the medium.com article, but I still can’t figure out how to calculate the “screen axis” that is needed to turn the rotated sphere as intended.
actually i think there is view problem,
while for a full 3D view the mouse interpretation like
mouseX, 0, width, -PI ,PI
is optimal, it looks bad.
a much more natural view i feel with
mouseX, 0, width, -PI/2, PI/2
anyhow the whole concept with this mouse requires the thinking
that NULL is when the mouse points at the CENTER of the canvas,
( a other approach would be to use delta values…)
to repair the processing y axis points down,
can use a (-1)*…
as we play here 3D view, what is that without a ZOOM
use mouseWheel.
add i try to separate the
view code
axis code
user circles code
float x=0,y=0,zmag = 1;
float Y = -1; // to invert processing y axis
void setup() {
size(500, 500, P3D);
noSmooth();
noFill();
//ortho();
strokeWeight(2);
println("use: mouse X Y, wheel for zoom");
}
void draw() {
background(200, 200, 0);
my_Camera();
my_Drawing();
}
void my_Drawing() {
axis();
spheres();
}
void axis() {
stroke(200, 0, 0);
line(0, 0, 0, height/4, 0, 0);
sphereAt(height/4, 0, 0, 6);
stroke(0, 200, 0);
line(0, 0, 0, 0, Y*height/4, 0);
sphereAt(0, Y*height/4, 0, 6);
stroke(0, 0, 200);
line(0, 0, 0, 0,0,height/4);
sphereAt(0,0,height/4, 6);
}
void spheres() {
push();
stroke(0, 200, 0);
ellipse(0, 0, height/2, height/2);
rotateX(HALF_PI);
stroke(200, 0, 0);
ellipse(0, 0, height/2, height/2);
rotateY(HALF_PI);
stroke(0, 0, 200);
ellipse(0, 0, height/2, height/2);
pop();
}
void my_Camera() { // limit from +- PI to +- PI/2 like more natural view??
x = map(mouseX, 0, width, -PI/2, PI/2);
y = Y*map(mouseY, 0, height, -PI/2, PI/2); // invert processing Y
translate(width/2, height/2);
scale(zmag);
rotateY(x);
rotateX(y);
// here the view is axis correct if mouse is middle/center of canvas!!!
// right hand rule: thumb X RIGHT (RED), point finger Y UP(GREEN), middle finger Z FRONT (BLUE)( points to you )
}
void sphereAt(float x, float y, float z, float diam ) {
push();
translate(x, y, z);
sphere(diam);
pop();
}
void mouseWheel(MouseEvent event) {
float e = event.getCount(); //println(e);
zmag += e*0.01;
}
I very much appreciate all these attempts to help me, but none of these examples behave the way I tried to describe.
The rotation commands to achieve this cannot be as simple as rotateY(x) and rotateX(y), because the second rotation is executed in an already rotated system, and therefore do not rotate around the axes “of my monitor” that never change.
In your example, compare the behaviour of
rotateY(x);
rotateX(y);
to
rotateY(x);
rotate(y, cos(x), 0, sin(x));
my problem is achieving the effect rotate(y, cos(x), 0, sin(x)) has, but for both axes.
What is to be seen in your gif is the behaviour of my original post, although with the axes flipped.
Thank you for adding grid and lines, they might help describe my problem:
This is exactly what I meant:
This is what I am trying to get rid of:
As you can see, the sphere rotates around the yellow x-axis.
What it should rotate around is the not yet drawn stationary “x-axis” that lies on the grid.
Just as the y-rotation in your sketch always happens around a not drawn y-axis on the grid.
This is not an assignment and I am not trying to bait you into doing it for me
This is a random sketch idea I had (further problems here lol P5js: 3D Typography Problems/Bugs?) where something I thought would be trivial apparently turned out to be kind of tricky and I’m almost certain there’s some clever math involved in solving this.
I may have captured and posted the wrong animated GIF; I have a dozen or so animations in that sketch to select from various rotations, signs, etc.
I really got into this as a personal exercise!
Is this it?
The corners of the box move vertically along the y-axis along with mouse movement.
I just tweaked your original code.
float y = map(mouseX, 0, width, PI/4, -PI/4);
float x = map(mouseY, 0, height, -PI/4, PI/4);
translate(width/2, height/2);
rotateX(x);
rotateY(-y);
@raetrakt – @glv’s solution is absolutely correct for a fixed-axis camera.
However, it seems that you may want your secondary rotation to be the plane of intersection created by your primary rotation. One approach to do this is a quaternion-based camera with relative motion input. For that please take a look at the p5.easyCam library, based on peasyCam.
For example, check out the Quick Start Ortho demo:
Note that, regardless, some of the basic feedback above still applies. If you want things to “always move right” etc. that cannot be true if a point rotates beyond the silhouette of the sphere – so your ranges (-PI, PI) are incorrect. Your inputs should range from -PI/4 to PI/4.
The other way is to actually translate to your plane of intersection, rotate, and draw. Here is an example of how that works:
These are great solutions… but am I missing something here? Seems like there’s ambiguity between (1.) making a ‘look at’ function and (2.) rotating the sphere from a given orientation to a new orientation based on the mouse (assuming a fixed camera and and a centered sphere)? From @raetrakt 's initial description, I thought I’d want the ability to spin the sphere all the way around? There also seems to be some muddying of the waters as to whether we have the option of rotating the camera rather than the sphere (in which case, try peasycam)… Anyway, here’s my go at it in Java using quaternions.
First, a quaternion class:
// Should be normalized at all times when used to represent a rotation
// in 3D space.
class Quaternion {
float x = 0.0;
float y = 0.0;
float z = 0.0;
float w = 1.0;
Quaternion() {
}
Quaternion(Quaternion q) {
set(q);
}
Quaternion(float x, float y, float z, float w) {
set(x, y, z, w);
}
Quaternion div(float scalar) {
return mult(1.0 / scalar);
}
Quaternion mult(float scalar) {
x *= scalar;
y *= scalar;
z *= scalar;
w *= scalar;
return this;
}
Quaternion normalize() {
float m = magSq();
if (m != 0.0 && m != 1.0) {
div(sqrt(m));
}
return this;
}
Quaternion mult(Quaternion b) {
// Caveat: quaternion multiplication is not commutative.
float tx = x;
float ty = y;
float tz = z;
x = x * b.w + w * b.x + y * b.z - z * b.y;
y = y * b.w + w * b.y + z * b.x - tx * b.z;
z = z * b.w + w * b.z + tx * b.y - ty * b.x;
w = w * b.w - tx * b.x - ty * b.y - tz * b.z;
return this;
}
Quaternion set(float angle, PVector axis) {
// Assumes that the axis is of unit length, i.e.,
// has a magnitude of 1.
float halfangle = 0.5 * angle;
float sinhalf = sin(halfangle);
x = axis.x * sinhalf;
y = axis.y * sinhalf;
z = axis.z * sinhalf;
w = cos(halfangle);
return this;
}
Quaternion set(Quaternion q) {
return set(q.x, q.y, q.z, q.w);
}
Quaternion set(float x, float y, float z, float w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
return this;
}
float magSq() {
// The quaternion's dot product with itself, i.e., dot(q, q).
return x * x + y * y + z * z + w * w;
}
PMatrix3D toMatrix(PMatrix3D out) {
float x2 = x + x;
float y2 = y + y;
float z2 = z + z;
float xsq2 = x * x2;
float ysq2 = y * y2;
float zsq2 = z * z2;
float xy2 = x * y2;
float xz2 = x * z2;
float yz2 = y * z2;
float wx2 = w * x2;
float wy2 = w * y2;
float wz2 = w * z2;
out.set(
1.0 - ysq2 - zsq2, xy2 - wz2, xz2 + wy2, 0.0,
xy2 + wz2, 1.0 - xsq2 - zsq2, yz2 - wx2, 0.0,
xz2 - wy2, yz2 + wx2, 1.0 - xsq2 - ysq2, 0.0,
0.0, 0.0, 0.0, 1.0);
return out;
}
Quaternion ease(Quaternion b, float t) {
// This is a simplistic form of easing. For situations
// where you need torque minimization, do not use this.
// Use spherical linear interpolation instead.
if (t <= 0.0) {
return normalize();
}
if (t>=1.0) {
return set(b).normalize();
}
float u = t * t * (3.0 - 2.0 * t);
float v = 1.0 - u;
x = v * x + u * b.x;
y = v * y + u * b.y;
z = v * z + u * b.z;
w = v * w + u * b.w;
return normalize();
}
}
Then the main class:
float rotSpeed = 1.25;
float easeSpeed = 0.125;
PVector ref = new PVector(0.0, 0.0, 1.0);
PVector axis = new PVector();
PVector mouse = new PVector();
PMatrix3D sphereMat = new PMatrix3D();
Quaternion rot = new Quaternion();
Quaternion rotMouse = new Quaternion();
Quaternion rotWorld = new Quaternion();
Quaternion identity = new Quaternion(0.0, 0.0, 0.0, 1.0);
PShape sphere;
void setup() {
size(512, 512, P3D);
smooth(8);
// Set the camera to the center of the screen.
ortho();
camera(
0.0, 0.0, -height * sqrt(3.0) * 0.5,
0.0, 0.0, 0.0,
0.0, 1.0, 0.0);
// Create a sphere PShape of unit size.
sphereDetail(32, 16);
sphere = createShape(SPHERE, 0.5);
//sphere = createShape(BOX, 0.5);
// Just in case the sphere has unnecessary children...
// Might not be necessary.
sphere = sphere.getTessellation();
// Let Processing style tags dictate how the shape looks in draw.
sphere.disableStyle();
}
void draw() {
float sphereScale = min(width, height) * 0.75;
float strokeWeight = 0.0025 / sphereScale;
if (mousePressed) {
// If the mouse is pressed, set a mouse vector
// to coordinates in the range [-1, 1].
// Invert the y-axis.
float wInv = 1.0 / width;
float hInv = 1.0 / height;
mouse.set(
-1.0 + 2.0 * mouseX * wInv,
1.0 - 2.0 * mouseY * hInv, 0.0);
// Capture the mouse's magnitude so that
// more dramatic mouse movements will increase
// the speed of rotation.
float mag = mouse.mag();
if (mag > 0.0) {
// If the mouse is not in the center, then create
// an angle of rotation.
float ang = rotSpeed * mag;
// Cross a reference vector against the mouse vector.
PVector.cross(ref, mouse, axis);
// Normalize the axis.
axis.normalize();
rotMouse.set(ang, axis);
}
} else {
// When the mouse is not pressed, decelerate the
// rotation to the identity rotation.
rotMouse.ease(identity, easeSpeed);
}
// The rotation created by the mouse axis is multiplied with
// the sphere's current rotation.
rotWorld.set(rotMouse).mult(rot);
// The rotation is eased toward the target rotation.
rot.ease(rotWorld, easeSpeed);
// Since Processing doesn't support quaternions, we need to convert.
rot.toMatrix(sphereMat);
// The resultant matrix is only a rotation matrix, so we scale
// up to the size we want.
sphereMat.scale(sphereScale);
// Apply the matrix to the shape.
sphere.resetMatrix();
sphere.applyMatrix(sphereMat);
// Draw.
directionalLight(
255.0, 255.0, 255.0,
0.0, 0.6, 0.8);
background(255.0);
strokeWeight(strokeWeight);
stroke(#202020);
fill(#fff7d5);
shape(sphere);
// Update the information for reference.
surface.setTitle(String.format("%.1f, %.2f, %.2f",
frameRate, mouse.x, mouse.y));
}
@raetrakt Nice challenge. I had encountered this problem before but had not put words to it. I’m surprised there’s not an easier way to “apply a rotation to an already rotated 3d object”.
@behreajj 's solution is the only one that solves it, the way I see it (thanks!). It would be nice to have a simplified version (without the easing and things like that) for easier analysis. I might give it a go in plain JavaScript if I have some spare time.
Edit: Another bit I find difficult to parse is the PMatrix3D. Would it be possible to simplify the final rotation values to a 3 axis rotation that can be applied to rotate(XYZ) or in an environment where PMatrix3D does not exist (plain JavaScript)?
Glad you found the example helpful! You can get around the differences between 3D in Java-based Processing and p5.js by converting the Quaternion to an axis angle then providing the results to rotate. A p5 web editor example is here.
To axis angle:
class Quaternion {
// ...
toAxisAngle() {
let wAcos = 0.0;
// Values supplied to acos should be in [-1, 1].
if (this._w <= -1.0) {
wAcos = TAU;
} else if (this._w >= 1.0) {
wAcos = 0.0;
} else {
wAcos = 2.0 * acos(this._w);
}
const wAsin = TAU - wAcos;
if (wAsin === 0.0) {
return {
angle: wAcos,
axis: createVector(0.0, 0.0, 1.0)
};
}
const sInv = 1.0 / wAsin;
const x = this._x * sInv;
const y = this._y * sInv;
const z = this._z * sInv;
// Find magnitude-squared.
const mSq = x * x + y * y + z * z;
if (mSq === 0.0) {
return {
angle: wAcos,
axis: createVector(0.0, 0.0, 1.0)
};
} else if (mSq === 1.0) {
return {
angle: wAcos,
axis: createVector(x, y, z)
};
}
// Normalize.
const mInv = 1.0 / sqrt(mSq);
return {
angle: wAcos,
axis: createVector(
x * mInv,
y * mInv,
z * mInv)
};
}
}
You could also retrieve the axis and angle from a quaternion separately, but the formulas I’ve tried for independent getAxis and getAngle functions have not provided accurate re-conversion to a quaternion.
The ability to ease between quaternions is one of its noteworthy features. The easing function above, smooth step, is a variant of lerp. Since quaternions which serve to represent rotations are usually of unit magnitude, and easing may change a quaternion’s magnitude, the quaternion is normalized after easing. This itself is a simplification: a substitute for spherical linear interpolation, slerp. Any time you want to remove an easing function, you can set the current rotation to the target rotation.
To be honest, 3D rotations are better handled by other JS libraries, such as Three.js. Lastly, to add to the list of resources that cover the maths and concepts better than I can, 3Blue1Brown has a great video on quaternions.
I am doing something similar inside After Effects, so plain JavaScript is the only resource I have. I had watched 3Blue1Brown’s videos before, but I could still not fully get my head around quaternions.
Whoops, sorry, I had missed what you said before - about plain Javascript. To return to your earlier question
Would it be possible to simplify the final rotation values to a 3 axis rotation that can be applied to rotate(XYZ) or in an environment where PMatrix3D does not exist (plain JavaScript)?
You can convert Euler angles to quaternions and back again, provided that you inform the conversion function of the desired order (XYZ, ZYX, etc.). (Even if you can’t use Three.js, you can reference its source code for how to define these conversions here and here.)
You can also define rotateX, rotateY and rotateZ for a quaternion. By calling such functions in a sequence you will in effect use a quaternion like Euler angles.
But AFAIK any use of Euler angles – or usage of a matrix or quaternion like Euler angles – would result in gimbal lock.
My advice is to search the After Effects scripting documentation to see if it offers you any other conversions. In my experience, the equivalents to a PMatrix3D are typically named some variant of AffineTransform, Transform3D, Mat4, Matrix4x4, etc., etc. They frequently wrap a 2D (4 rows x 4 columns) or 1D array (16 elements long) and/or can be converted to and from such an array.