8
votes

Update: Added a jsfiddle to illustrate:

JSFiddle

I have an object ( a cube ) that is placed in the scene. My goal is to be supplied 3 angles which represent an object's orientation in the real world. The angles will be measured against real-world X, Y and Z axis. What I am failing (miserably) is how to give an object these angles and then as the data changes, set the object to receive a new triplet of angles. What I seem to be finding is that when I set an initial set of angles, all is good, but when I set a new set of angles, they appear to be set relative to the local object space orientation as opposed to world space. Ive tried to understand some of the similar questions posed in this area but these appear to be about rotating an object around an axis as opposed to setting an object's angles explicitly against the world axis.

When a value of my x, y or z angle changes, I am currently calling:

cube.rotation.set(x, y , z, 'XYZ');

To illustrate further, here is a screen shot of my cube ... without changing X or Y, I rotate 90 about Z ... see the the two images of before and after

enter image description here

and

enter image description here

Notice how the rotation has occurred around the direction of the normal of the purple face and not around the world Z axis ...

I'm stumped :-(

2
Testing ... ... I thought it worked when I changed the code from cube.rotation.set(x, y , z, 'XYZ'); to cube.rotation.set(x, y , z, 'ZYX'); but it just moved the problem to a different axis.Kolban
Added a jsFiddle to illustrate.Kolban

2 Answers

8
votes

cube.rotation.set(), as you've already discovered, creates a rotation along the local axes of the object. During the first rotation, say, along X, the local X axis in world coordinates is exactly equal to the world X axis. After the rotation however, the local Y and Z axes in world coordinates are no longer equal to the world Y and Z axes. Therefore, subsequent rotations along those axes will no longer look like rotations along the world axes. If you change the order of rotations, again, only the first rotation will take place in local space that is incidentally identical to world space. All subsequent rotations will be calculated in local space that is no longer identical.

You want to rotate your object three times: once along the world X axis, once along the world Y axis and once along the world Z axis. But we now know that all rotations take place in local space. The key to do the rotations in world space is to ask the question: "What does this world axis look like in the local space of my object?" If you can take a world axis, express it as a vector in the object's local space, and then make a rotation along that vector, then you've got yourself a rotation along a world vector, regardless of the specific local space.

Let's integrate that idea into your fiddle:

var x = toRadians($("#rotateX").slider("option", "value"));
var y = toRadians($("#rotateY").slider("option", "value"));
var z = toRadians($("#rotateZ").slider("option", "value"));

var rotation = new THREE.Matrix4();
rotation.multiply(new THREE.Matrix4().makeRotationX(x));

var worldYAxis = new THREE.Vector3(0, 1, 0).applyMatrix4(new THREE.Matrix4().getInverse(rotation));
var rotationWorldY = new THREE.Matrix4().makeRotationAxis(worldYAxis, y);
rotation.multiply(rotationWorldY);

var worldZAxis = new THREE.Vector3(0, 0, 1).applyMatrix4(new THREE.Matrix4().getInverse(rotation));
var rotationWorldZ = new THREE.Matrix4().makeRotationAxis(worldZAxis, z);
rotation.multiply(rotationWorldZ);

cube.matrixAutoUpdate = false;
cube.matrix = rotation;

If you try this code, you'll notice that X rotations are along the world X axis when Y and Z rotations are zero, Y rotations are along the world Y axis when Z rotation is zero and Z rotations are always along the Z axis.

But what if, after setting a X, then a Y, then a Z rotation, we turn the X slider again? You'll notice that the rotation is no longer along the world X axis, but along the local X axis. The reason for this is simple, although the explanation why it's so may not be. With one word - rotations do not commute. You see, the rotations X(a) * Y(b) * Z(c) * X(d) is not in general equal to X(a + d) * Y(b) * Z(c). That is, if you rotate along world X by a, then along world Y by b, then along world Z by c, and then again along world X by d, then the result is not the same as the rotation that you would have gotten if you had rotated along world X by a + d right from the start. The reason for that is that "world X" at the start of the transformations and "world X" at the end of the transformations are two different vectors, whose values depend on all transformations done up to this point. Therefore, they represent different axes of rotation in local space.

I will try to anticipate the next question: "How can I make it so, that no matter how I move my sliders left and right, the object always rotates only along the world axes?" The answer to that build on what we already know about doing rotations around world axes - the world axis must be expressed as a vector in local space and the the rotation must take place around that vector. An implementation of the idea follows:

var prevX = 0, prevY = 0, prevZ = 0;
function doRotate() {
    var x = toRadians($("#rotateX").slider("option", "value"));
    var y = toRadians($("#rotateY").slider("option", "value"));
    var z = toRadians($("#rotateZ").slider("option", "value"));

    var inverse = new THREE.Matrix4().getInverse(cube.matrix);

    var rotation = cube.matrix;
    var worldXAxis = new THREE.Vector3(1, 0, 0).applyMatrix4(new THREE.Matrix4().getInverse(rotation));
    var rotationWorldX = new THREE.Matrix4().makeRotationAxis(worldXAxis, x - prevX);
    rotation.multiply(rotationWorldX);
    var worldYAxis = new THREE.Vector3(0, 1, 0).applyMatrix4(new THREE.Matrix4().getInverse(rotation));
    var rotationWorldY = new THREE.Matrix4().makeRotationAxis(worldYAxis, y - prevY);
    rotation.multiply(rotationWorldY);
    var worldZAxis = new THREE.Vector3(0, 0, 1).applyMatrix4(new THREE.Matrix4().getInverse(rotation));
    var rotationWorldZ = new THREE.Matrix4().makeRotationAxis(worldZAxis, z - prevZ);
    rotation.multiply(rotationWorldZ);

    prevX = x;
    prevY = y;
    prevZ = z;

    cube.matrixAutoUpdate = false;
    cube.matrix = rotation;
}

Fiddle: https://jsfiddle.net/2whv0e8o/13/

The above function is bound as the slide event handler of all sliders: "slide": doRotate. Now, if you try to rotate hither and thither, you'll notice that no matter the current orientation of the box, moving the slider always results in rotations along the world axes. There is one caveat though: the values of the sliders no longer represent actual angles of rotation around any particular axis. You'll notice that the doRotate function only applies a delta rotation along the world axes as they're currently expressed in local coordinates. We have achieved the goal of being able to apply incremental rotations that are always done along the world axes, but we have lost the meaning of the absolute values of the X, Y, and Z sliders. But if the values don't mean anything, what can possibly be used to save the object's current rotation as the rotation? In my experience, it's best to just store the object's transformation matrix wholesale. That is, instead of trying to save three particular rotation angles, plus a translation vector, plus maybe scaling parameters, just save the entire matrix as part of the object.

1
votes

You have to add a pivot point to the scene, this will be the center of the rotations. The cube mash has to be added to the pivot point and then you can rotate the pivot point.

I updated your jsFiddle here: https://jsfiddle.net/x5w31f33/

var cube = new THREE.Mesh( geometry, material );
cube.position.set(2, 0, 0);

var pivotPoint = new THREE.Object3D();
scene.add( pivotPoint );
pivotPoint.add(cube);

pivotPoint.rotation.set(x, y , z, 'XYZ');