xeokit / xeokit-sdk

Open source JavaScript SDK for viewing high-detail, full-precision 3D BIM and AEC models in the Web browser.
https://xeokit.io
Other
725 stars 287 forks source link

Euler angles in model rotation does not work as expected #1690

Open hamza-hajji opened 1 week ago

hamza-hajji commented 1 week ago

Describe the bug

Changing rotation coordinates does not work as expected, this visualization describes how euler angles work

https://danceswithcode.net/engineeringnotes/rotations_in_3d/demo3D/rotations_in_3d_tool.html

If I try the same thing in xeokit, I get very different results

To Reproduce Steps to reproduce the behavior:

  1. Go to an example model https://xeokit.github.io/xeokit-sdk/examples/buildings/#xkt_vbo_Duplex
  2. change the x and y coordinates
const model = viewer.scene.models.myModel
// [pitch, yaw, roll]
model.rotation = [30, 90, 0]

image

Notice how the x and y rotations are always local to the object's axes

Expected behavior

I expect the rotation to work like this visualization of euler angles (yaw being y axis, x being pitch)

image

I'm not sure how xeokit calculates rotation, if anyone can provide me with a way to make the object's rotation global instead of local that would be appreciated

Thank you

paireks commented 1 week ago

Hello @hamza-hajji,

first thing to remember is that Xeokit has a different coordinate system than the one in this website presented.

IMO in Xeokit it's like this (green one): image

second thing to remember is that Euler angle itself doesn't specify the order in which you should rotate. On this website they rotate in order ZYX, but respectively to their coordinate system.

I'd advise to check the rotations e.g. in Blender where you can specify the order of rotations for Euler angles. Here is some tutorial there that explains the differences: https://www.youtube.com/watch?v=mcDHDfK2pXs

This way you can make sure if everything looks ok.

paireks commented 1 week ago

Also please check this example, click on the question mark at the top right and manipulate rotations by sliders to see how it works in Xeokit: https://xeokit.github.io/xeokit-sdk/examples/lidar/#xkt_MAP_alignPointCloud

hamza-hajji commented 1 week ago

@paireks thanks for your response, it seems in the rotation setter, you use an XYZ order when setting the quaternion

// Mesh.js
set rotation(value) {
    this._rotation.set(value || [0, 0, 0]);
    math.eulerToQuaternion(this._rotation, "XYZ", this._quaternion);
    this._setLocalMatrixDirty();
    this._setAABBDirty();
    this.glRedraw();
    this.fire("rotation", this._rotation);
}

Is it possible to override the XYZ order in this code (either by doing computation on my side or throught modifying the library)? Do the rotation angles care about the order you pass to quaternion setter or it's just in xeokit that you do only one order?

Thanks for taking the time to respond 🙏

MichalDybizbanskiCreoox commented 1 week ago

Hi @hamza-hajji , there's an alternative way to control a Mesh's rotation through the Mesh::quaternion setter, as defined at https://github.com/xeokit/xeokit-sdk/blob/master/src/viewer/scene/mesh/Mesh.js#L481-L494

This way you can transform a local euler angle with a local order to a quaternion, and have it assigned to the Mesh, e.g.: model.quaternion = math.eulerToQuaternion([30, 90, 0], "ZYX");

Hope this helps!

hamza-hajji commented 1 week ago

@MichalDybizbanskiCreoox won't this line override my quaternion value?

https://github.com/xeokit/xeokit-sdk/blob/master/src/viewer/scene/mesh/Mesh.js#L490

math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);
MichalDybizbanskiCreoox commented 1 week ago

@hamza-hajji Both Mesh::quaternion and Mesh::rotation setters modify "private" Mesh::_quaternion and Mesh::_rotation properties, which are kept in sync. The difference is a type of an input value either of the setters take.

hamza-hajji commented 1 week ago

@MichalDybizbanskiCreoox excuse me but if I do

model.quaternion = math.eulerToQuaternion([30, 90, 0], "ZYX");

the quaternion setter will override the rotation value with a XYZ rotation based on my quaternion, so they will be out of sync, no?

math.quaternionToEuler(this._quaternion, "XYZ", this._rotation);

since math.quaternionToEuler() does set this._rotation

MichalDybizbanskiCreoox commented 6 days ago

@hamza-hajji right, if that's what you mean then it's correct - Mesh::rotation's value, which is defined in the "XYZ" order, might not be the same as your math.eulerToQuaternion's argument if applied with the "ZYX" order.

If you wanted to get an arbitrarily ordered euler angle value from a Mesh, you could use a symmetrical call:

const euler = math.quaternionToEuler(model.quaternion, "ZYX");

Please keep in mind however, that euler value might not be what was initially used as the math.eulerToQuaternion argument, because quaternionToEuler(eulerToQuaternion(E, O), O) does not necessarily equal E.

hamza-hajji commented 6 days ago

@hamza-hajji right, if that's what you mean then it's correct - Mesh::rotation's value, which is defined in the "XYZ" order, might not be the same as your math.eulerToQuaternion's argument if applied with the "ZYX" order.

If you wanted to get an arbitrarily ordered euler angle value from a Mesh, you could use a symmetrical call:

const euler = math.quaternionToEuler(model.quaternion, "ZYX");

Please keep in mind however, that euler value might not be what was initially used as the math.eulerToQuaternion argument, because quaternionToEuler(eulerToQuaternion(E, O), O) does not necessarily equal E.

@MichalDybizbanskiCreoox wouldn't it be simpler if instead of hardcofding the XYZ, there's a Mesh#eulerOrder that I can set like

model.eulerOrder = "ZYX";
model.quaternion = [x, y, z, w];

and in the rotation and quaternion setters you just use this.eulerOrder || "XYZ"

MichalDybizbanskiCreoox commented 6 days ago

The thing is, once both Euler and Quaternion setters are exposed in the object's interface, you can no longer rely on the Euler angle returned by the getter to be identical with the one provided to the setter.

Consider the code where E is an euler angle vector:

model.rotation = E;
model.quaternion = model.quaternion;

Now, even though logically the model's effective rotation didn't change, its rotation property (as provided by the getter) will likely be different than E.

In fact any quaternion property assignment necessarily modifies the rotation property, so the rotation should only be considered as an effective value, i.e. as an angle that will have a specific effect on an object, regardless of its particular value (as in - there can be many different Euler angles that lead to the same object's rotation).

Truth be told - quaternion property could be the only one exposed as an interface to set object's "rotation". The rotation property should be considered kinda as a convenience shortcut, in that is saves user a single math.eulerToQuaternion call, but not as a value store, which would likely be lossy even if we added the eulerOrder property (e.g. due to a limited floating-point precision if the values were obtained from some kind of a text-based interface).

If your viewer controls all places the object's rotation is being changed, you could "store" the input rotation value in some kind of and external object->rotation mapping, or as a custom property of the object being affected.