Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

is there a way of rotating a model around a point instead of around itself? #1783

Open
hamza-hajji opened this issue Jan 22, 2025 · 9 comments
Labels
feature-suggestion Suggestion for how to extend xeokit

Comments

@hamza-hajji
Copy link
Contributor

Is your feature request related to a problem? Please describe.
I want a way to rotate an object based on a different origin, not the default one. If I load a model , I can specify its origin in the load params

let model = xktLoader.load({
  id: "mymodel",
  src: "./models/whatever.xkt",
  saoEnabled: true,
  edges: true,
  dtxEnabled: true,
  origin: [1, 0, 1],
});

If I rotate it like this, it's correctly rotated according to that local origin, but I wish there was a way that while rotating using model.rotation = [x, y, z] we also provide an global origin for that rotation

Context

I want to rotate a group of individual models around the center point which is the average of all their positions, so I need to set each of their global origins as that center

Describe the solution you'd like
This could be a function called like this

const newRotation = [0, 90, 0];
// global center
const center = [0, 0, 0];
model.rotateAroundPoint(newRotation, center)

This would rotate a model around the point (0, 0, 0) in the world, not the origin (0, 0, 0) we provide when loading the model

Alternative solutions I tried
I tried to group the models under one Node, like with Meshes but that apparenly only works with Meshes, I wonder if it's possible?

Thanks for consideration

@hamza-hajji hamza-hajji added the feature-suggestion Suggestion for how to extend xeokit label Jan 22, 2025
@MichalDybizbanskiCreoox
Copy link
Collaborator

Hi @hamza-hajji ,

This is an interesting problem that can be solved with a following approach:
Aside from the position and rotation properties, the SceneModel also exposes a matrix property.
They are related in a way that a change in either of the first two affects the third one, and vice versa.
The matrix property represents a transformation matrix of a SceneModel, and assigning to it can be used to perform any kind of 3D transformation.

A 3D transformation matrix that represents the case you described can be created as follows:

  1. Translate the coordinate system so that the pivot point moves to its origin.
  2. Rotate the coordinate system (around its origin).
  3. Translate the pivot point back to its position.

There are many libraries that abstract such operations, and xeokit SDK exposes such a library as well, as the math package.
An eulerRotationAroundPoint function that returns a transformation matrix could be implemented as this:

function eulerRotationAroundPoint(eulerRotation, center) {
    // Matrix that translates `center` to origin.
    const t1  = math.translationMat4v(math.subVec3([0,0,0], center, math.vec3()), math.identityMat4());
    // Quaternion and matrix representing the rotation
    const q2 = math.eulerToQuaternion(eulerRotation, "XYZ", math.vec4());
    const r2 = math.quaternionToMat4(q2, math.identityMat4());
    // Matrix that translates `center` back to its place.
    const t3  = math.translationMat4v(center, math.identityMat4());
    // Combine the transformations (order is important)
    const m = math.mat4();
    math.mulMat4(t3, math.mulMat4(r2, t1, m), m);
    return m;
}

and then used as:

const newRotation = [0, 90, 0];
// global center
const center = [0, 0, 0];
model.matrix = eulerRotationAroundPoint(newRotation, center);

Hope this helps!
Michał

@hamza-hajji
Copy link
Contributor Author

@MichalDybizbanskiCreoox Thanks for your response, but for some reason after applying that algo, the model moves to position [0, 0, 0]

function eulerRotationAroundPoint(eulerRotation, center) {
    // Matrix that translates `center` to origin.
    const t1  = math.translationMat4v(math.subVec3([0,0,0], center, math.vec3()), math.identityMat4());
    // Quaternion and matrix representing the rotation
    const q2 = math.eulerToQuaternion(eulerRotation, "XYZ", math.vec4());
    const r2 = math.quaternionToMat4(q2, math.identityMat4());
    // Matrix that translates `center` back to its place.
    const t3  = math.translationMat4v(center, math.identityMat4());
    // Combine the transformations (order is important)
    const m = math.mat4();
    math.mulMat4(t3, math.mulMat4(r2, t1, m), m);
    return m;
}

function rotateAroundPoint(object, center, rotation) {
  object.matrix = eulerRotationAroundPoint(rotation, center);
}

window.rotateAroundPoint = rotateAroundPoint;

I call it then in the console
rotateAroundPoint(viewer.scene.models.libModel, [0, 0, 0], [0, 30, 0])

Image

@MichalDybizbanskiCreoox
Copy link
Collaborator

Hi @hamza-hajji ,
Yes, the reason the position is set to zero is because the matrix returned by the eulerRotationAroundPoint function doesn't contain a translation component, so after it's being assigned to object.matrix, the model's original translation is overwritten.
If the model is to be translated, then the rotation matrix needs to be multiplied by a translation matrix.
In your case that'd be

object.matrix = math.mulMat4(
    eulerRotationAroundPoint(rotation, center), 
    math.translationMat4v(position, math.identityMat4()), 
    math.mat4());

That represents a sequence of transformations:

  1. Translate an object by position from the coordinate system's origin
  2. Rotate the coordinate system around a center pivot point, by rotation euler angle.
    Please note that the original model's position, before the model is rotated, is also represented by the model's matrix property.

BTW if you wanted to rotate a model around a pivot while preserving existing transformation, you could achieve it by

object.matrix = math.mulMat4(
    eulerRotationAroundPoint(rotation, center), 
    object.matrix, 
    math.mat4());

@hamza-hajji
Copy link
Contributor Author

@MichalDybizbanskiCreoox thanks for assistance, that worked, but for some reason the object's rotation is not updated, is there a way in xeokit API to update the position/rotation from transform matrix?

Image

@MichalDybizbanskiCreoox
Copy link
Collaborator

Thank you for pointing this out @hamza-hajji
Rotation not being updated in this case is indeed a bug, great catch!
It's been fixed by the PR at #1787
It'll get merged to the master branch soon, and be included in a next release.

@hamza-hajji
Copy link
Contributor Author

hamza-hajji commented Jan 24, 2025

@MichalDybizbanskiCreoox I ran the code added in the commit after setting the matrix, it looks like they're in rad instead of degree. Do you have any idea what went wrong?

function rotateAroundPoint(object, center, rotation) {
  object.matrix = math.mulMat4(
    eulerRotationAroundPoint(rotation, center), 
    object.matrix, 
    math.mat4());
  
    math.quaternionToEuler(object._quaternion, "XYZ", object._rotation);
    console.log("results", object.rotation);
}

// running
rotateAroundPoint(viewer.scene.models.libModel, [-3.868927614592229, 49.849784528251845, -42.94546603505578], [0, 30, 0])
// logs 30 in radian
[-0, 0.523598775, -0]

@hamza-hajji
Copy link
Contributor Author

@MichalDybizbanskiCreoox I made it work locally by converting it to degress, but I'm afraid in the next release we'll have it in radians

function rotateAroundPoint(object, center, rotation) {
  object.matrix = math.mulMat4(
    eulerRotationAroundPoint(rotation, center),
    object.matrix,
    math.mat4()
  );

  const rot = math.vec3();
  math.quaternionToEuler(object._quaternion, "XYZ", rot);
  object.rotation = rot.map(angle => angle * math.RADTODEG);
}

@MichalDybizbanskiCreoox
Copy link
Collaborator

Hi @hamza-hajji , thank you for pointing this out.
We've rectified asymmetric semantics of math.quaternionToEuler and math.eulerToQuaternion to both interpret Euler angles as expressed in degrees.
The fix has been merged to the master branch through #1796, and will be available in the next release.

@MichalDybizbanskiCreoox
Copy link
Collaborator

Hi @hamza-hajji ,
The fix has just been released with v2.6.66
https://github.com/xeokit/xeokit-sdk/releases/tag/v2.6.66

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-suggestion Suggestion for how to extend xeokit
Projects
None yet
Development

No branches or pull requests

2 participants