KhronosGroup / glTF

glTF – Runtime 3D Asset Delivery
Other
7.16k stars 1.14k forks source link

rotation animation : How to rotate about local axes of a shape #2145

Open prajwalshelar opened 2 years ago

prajwalshelar commented 2 years ago

Hi I have created a glTF exporter with animation export. I am trying to write rotation animation data for rotation of components/shapes along their own(local) axis instead of global rotation axis. But I always get rotation about the global axes. For this I am trying to find a way to write quaternion for axis which is offset from the global coordinate systems origin so that I can set this axis as the axis of rotation for a shape. For eg. axis which passes through (0,0,25) and parallel to the X-axis. How do I represent this axis as a quaternion? Is it possible to do so or is there some other way to do it? Thank you.

javagl commented 2 years ago

The rotation will always be "local" so to speak. When you have a node like this

{
    "nodes": [
        {
            "mesh": 0,
            "rotation": [ 0.0, 1.0, 0.0, 0.0 ]
        }
    ]
}

then this rotation will take place in the local coordinate system of the node.

For eg. axis which passes through (0,0,25) and parallel to the X-axis. How do I represent this axis as a quaternion?

It is not possible to describe the center of the rotation as a quaternion. The quaternion only stores the rotation itself (for example, the rotation around the x-axis, by a certain angle).

The center of the rotation ((0,0,25) in your case) has to be described wiht a translation.

You can combine multiple transforms in a node (translation, rotation and scale), but maybe it's easier to describe this with two different nodes here. Imagine you have a glTF asset with the following structure:

- root
    - node with rotation about 45 degrees around the x-axis
        - mesh that is attached to the node

then this mesh will be rotated around the x-axis, at the origin.

In contrast to that, when you have a glTF with this structure:

- root
    - node with translation (0,0,25)
        - node with rotation about 45 degrees around the x-axis
            - mesh that is attached to the node

then the object will first be rotated around the x-axis, in its local coordinate system, and then be translated by (0,0,25), as shown on the left side of this ancient image:

If the asset was structured like this

- root
    - node with rotation about 45 degrees around the x-axis
        - node with translation (0,0,25)
            - mesh that is attached to the node

then the object would first be translated, and then it would be rotated, as shown on the right side of the image.

prajwalshelar commented 2 years ago

@javagl Thanks for the reply. I am interested in setting the rotation axis for rotation animation of the node and not for the local transformations at the node. For that I am writing the quaternions and time data in the buffer along with JSON objects viz. accessors, bufferViews and animations. As you mentioned, the transformations applied at the node are applied locally, but for animations I am not getting the same result. In the attached glTF model, I expect the wheel to rotate about its own X-axis but instead it rotates about global X-axis. Wheel_Assembly_GLTF.zip. Any suggestions on this?

javagl commented 2 years ago

The given model contains

...
    {
      "name": "Wheel",
      "children": [ 8 ]
    },
    {
      "translation": [ 15.8159142, -22.7570438, 105.64357 ],
      "rotation": [ -0.663252592, 0.663252592, -0.245144859, -0.245144859 ],
      "children": [ 0 ]
    },
...

and looks like this:

GltfAnimationOrder01

Moving the translation up into the parent, as in

...
    {
      "name": "Wheel",
      "translation": [ 15.8159142, -22.7570438, 105.64357 ],
      "children": [ 8 ]
    },
    {
      "rotation": [ -0.663252592, 0.663252592, -0.245144859, -0.245144859 ],
      "children": [ 0 ]
    },
...

yields this:

GltfAnimationOrder02

prajwalshelar commented 2 years ago

@javagl Thanks for the reply. I do get expected result as above. I am just confused that why just move translation part to parent and not both translation and rotation and when we take both of them to parent, again we get wrong result.

bghgary commented 2 years ago

See this: https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#transformations

To compose the local transformation matrix, TRS properties MUST be converted to matrices and postmultiplied in the T R S order; first the scale is applied to the vertices, then the rotation, and then the translation.

prajwalshelar commented 2 years ago

Thanks for sharing @bghgary

prajwalshelar commented 1 year ago

Hi @javagl , You commented: The center of the rotation ((0,0,25) in your case) has to be described with a translation. But how do I set the axis/pivot for rotation animations such as in the attached example animation videos, I have rotated an object along three different axes (which are different from object's 'local' axes). I intend to export such animations to glTF. How can this be done? Thanks, Prajwal. glTf_rotation_anim_issue.zip

javagl commented 1 year ago

The first video looks "right" for me. The others look wrong, but I don't know exactly what they should show. (Also, the attached glTF does not contain animations at all...)

The center of the rotation ((0,0,25) in your case) has to be described with a translation.

But how do I set the axis/pivot for rotation animations such as in the attached

My statement (that the center of the rotation has to be described with a translation) only refers to the pivot point. From the videos, it looks like you also want to rotate around different axes.

The axis of rotation is encoded in the qaternion itself. For example, consider the AnimatedTriangle Sample Model. It contains a rotation animation, where the quaternions describe a rotation around the z-Axis. I just created an AnimatedTriangle2.gltf (see attachment below). There I changed the axis of rotation, to rotate around the y-axis. The differences in the rotation accessor data are shown here:

glTF Rotations

The asset also contains an additional node, with a translation: 0,0,0. You can change this translation to see how this affects the rotation. Right now, it rotates around an "edge" of the triangle. When you set a translation of 1,0,0, then it rotates around an axis that is outside of the triangle.


Just to better understand the context: The generator of your asset is OpenCascade - are you trying to implement glTF export for OpenCascade?


The AnimatedTriangle2.gltf - just save this as a .glTF file:

{
  "scene": 0,
  "scenes": [
    {
      "nodes": [0]
    }
  ],

  "nodes": [
    {
      "rotation": [0.0, 0.0, 0.0, 1.0],
      "children": [1]
    },
    {
      "translation": [0.0, 0.0, 0.0],
      "mesh": 0
    }
  ],

  "meshes": [
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 1
          },
          "indices": 0
        }
      ]
    }
  ],

  "animations": [
    {
      "samplers": [
        {
          "input": 2,
          "interpolation": "LINEAR",
          "output": 3
        }
      ],
      "channels": [
        {
          "sampler": 0,
          "target": {
            "node": 0,
            "path": "rotation"
          }
        }
      ]
    }
  ],

  "buffers": [
    {
      "uri": "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
      "byteLength": 44
    },

    {
      "uri": "data:application/gltf-buffer;base64,AAAAAAAAgD8AAABAAABAQAAAgEAAAAAAAAAAAAAAAAAAAIA/AAAAAPT9ND8AAAAA9P00PwAAAAAAAIA/AAAAAAAAAAAAAAAA9P00PwAAAAD0/TS/AAAAAAAAAAAAAAAAAACAPw==",
      "byteLength": 100
    }
  ],
  "bufferViews": [
    {
      "buffer": 0,
      "byteOffset": 0,
      "byteLength": 6,
      "target": 34963
    },
    {
      "buffer": 0,
      "byteOffset": 8,
      "byteLength": 36,
      "target": 34962
    },
    {
      "buffer": 1,
      "byteOffset": 0,
      "byteLength": 100
    }
  ],
  "accessors": [
    {
      "bufferView": 0,
      "byteOffset": 0,
      "componentType": 5123,
      "count": 3,
      "type": "SCALAR",
      "max": [2],
      "min": [0]
    },
    {
      "bufferView": 1,
      "byteOffset": 0,
      "componentType": 5126,
      "count": 3,
      "type": "VEC3",
      "max": [1.0, 1.0, 0.0],
      "min": [0.0, 0.0, 0.0]
    },
    {
      "bufferView": 2,
      "byteOffset": 0,
      "componentType": 5126,
      "count": 5,
      "type": "SCALAR",
      "max": [4.0],
      "min": [0.0]
    },
    {
      "bufferView": 2,
      "byteOffset": 20,
      "componentType": 5126,
      "count": 5,
      "type": "VEC4",
      "max": [0.0, 1.0, 0.0, 1.0],
      "min": [0.0, 0.0, 0.0, -0.707]
    }
  ],

  "asset": {
    "version": "2.0"
  }
}
prajwalshelar commented 1 year ago

Hi @javagl , Thanks for the reply.

"The first video looks "right" for me. The others look wrong, but I don't know exactly what they should show." Here I was just trying to show that I want to rotate the object about any custom axis (in this case the selected edges of that door). Also what do you mean by "right"/"wrong" animation?

I understand that we can change the rotation axis by adding a translation node, but it also means that it will move the object with respect to others objects in the model. Is that correct?

"Just to better understand the context: The generator of your asset is OpenCascade - are you trying to implement glTF export for OpenCascade?" Yes, I am trying to export glTF animations by extending OpenCascade glTF exporter. I am able to export rotation animations but they always rotate around the local axes. In my application I am able to select axis of rotations from the "edges" of the object and rotate an object about that axis. I am trying to export that to glTF. Now as the rotation has to be added as a quaternion, I am confused on how exactly do I get the "position of the axis"/"center of rotation" into glTF models.

javagl commented 1 year ago

It's difficult to describe all that verbally. And I hope that the following attempt to describe it (a bit more) visually is not too confusing.

The exact behavior only depends on the structure of the nodes, and which of them has a translation or rotation. I created an example glTF asset (see the snippet at the end of this comment). The asset has the following node structure:

Root node
-> 'Global' translation,
-> The node where the rotation is animated
-> 'Local' translation
-> The node that holds the mesh

(The node names in the glTF asset are chosen to describe this structure as well)

The asset shows the rotated object in dark blue. This is just a unit square in (0,0,0)-(1,1,0). The asset also shows a unit square in transparent red, that is attached to the root node - this is used to show an "untransformed" object, and highlight the differences in the following images.

The node that contains the rotation is always the same. The rotation animation itself is also always the same. It is always a rotation around the y-axis, as described with the rotation animation quaternions.

And to emphasize that: The quaternion describes the axis and angle of the rotation. It does not describe "where" the rotation is happening. (So it does not include information about a "pivot point" or so).

The differences in the following examples are only accomplished by setting the translation of the "global translation" node and the "local translation" node.

I created four different cases, with different "global translation" and "local translation" values:


Case 0:

In this case, there is no translation. The rotation is happening about the y-axis, at the origin. One could describe that as a rotation around the line through (0,0,0)-(0,1,0).

(Here, this is also the left edge of the square).

AnimationA0


Case 1:

The "local" translation moves the square to the left, by -0.5. The rotation is still happening around the y-axis, at the origin. But the square is moved relative to that. So it is no longer rotating about its left edge, but about its "center".

AnimationA1


Case 2:

The "global" translation moves the rotated square to the right, by 1.5. The rotation is now no longer happening at the origin. It is rotating around the line through (1.5,0,0)-(1.5,1,0).

(There is no "local" translation. So this rotation still appears to be about "the left edge" of the square. One could say that in the coordinate system of the square, the rotation is still "around the origin", but globally, it is happening "somewhere else" - namely, shifted right by 1.5)

AnimationA2


Case 3:

  'Global' translation [1.5, 0.0, 0.0]
  'Local' translation: [-0.5, 0.0, 0.0]

The "global" transform moves the rotated square to the right, by 1.5. The "local" transform moves the square to the left, by 0.5. So the rotation is happening around the line through (1.5,0,0)-(1.5,1,0), which now is the "center line" of the square.

AnimationA3


The glTF asset. This is a complete, standalone (embedded) glTF asset that can just be saved as a .gltf file. It can serve as a "template" for the cases described above. The nodes have their name set to indicate which node is which, referring to the descriptions here. You may want to play with the translation values of these nodes, and observe the effects for the different configurations by dragging-and-dropping the glTF file into https://gltf-viewer.donmccurdy.com/ or https://sandbox.babylonjs.com/ , or - when you are using VSCode - previewing the glTF model directly in the editor by installing https://marketplace.visualstudio.com/items?itemName=cesium.gltf-vscode

{
  "scene": 0,
  "scenes": [
    {
      "nodes": [0]
    }
  ],

  "nodes": [
    {
      "name": "Root node",
      "children": [1],
      "mesh": 1
    },
    {
      "name": "'Global' translation: The node that translates the rotating mesh (including its 'pivot')",
      "translation": [0.0, 0.0, 0.0],
      "children": [2]
    },
    {
      "name": "The node where the rotation is animated",
      "rotation": [0.0, 0.0, 0.0, 1.0],
      "children": [3]
    },
    {
      "name": "'Local' translation: The node that determines the position of the 'pivot', relative to the mesh",
      "translation": [0.0, 0.0, 0.0],
      "children": [4]
    },
    {
      "name": "The node that holds the mesh",
      "mesh": 0
    }
  ],

  "materials": [
    {
      "doubleSided": true,
      "pbrMetallicRoughness": {
        "baseColorFactor": [0, 0, 1, 1],
        "metallicFactor": 0,
        "roughnessFactor": 1
      }
    },
    {
      "doubleSided": true,
      "alphaMode": "BLEND",
      "pbrMetallicRoughness": {
        "baseColorFactor": [1.0, 0.0, 0.0, 0.5],
        "metallicFactor": 0,
        "roughnessFactor": 1
      }
    }
  ],
  "meshes": [
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 1
          },
          "indices": 0,
          "material": 0
        }
      ]
    },
    {
      "primitives": [
        {
          "attributes": {
            "POSITION": 1
          },
          "indices": 0,
          "material": 1
        }
      ]
    }
  ],

  "animations": [
    {
      "samplers": [
        {
          "input": 2,
          "interpolation": "LINEAR",
          "output": 3
        }
      ],
      "channels": [
        {
          "sampler": 0,
          "target": {
            "node": 1,
            "path": "rotation"
          }
        }
      ]
    }
  ],

  "buffers": [
    {
      "uri": "data:application/octet-stream;base64,AAABAAIAAQADAAIAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAA",
      "byteLength": 60
    },
    {
      "uri": "data:application/gltf-buffer;base64,AAAAAAAAgD8AAABAAABAQAAAgEAAAAAAAAAAAAAAAAAAAIA/AAAAAPT9ND8AAAAA9P00PwAAAAAAAIA/AAAAAAAAAAAAAAAA9P00PwAAAAD0/TS/AAAAAAAAAAAAAAAAAACAPw==",
      "byteLength": 100
    }
  ],
  "bufferViews": [
    {
      "buffer": 0,
      "byteOffset": 0,
      "byteLength": 12,
      "target": 34963
    },
    {
      "buffer": 0,
      "byteOffset": 12,
      "byteLength": 48,
      "target": 34962
    },
    {
      "buffer": 1,
      "byteOffset": 0,
      "byteLength": 100
    }
  ],
  "accessors": [
    {
      "bufferView": 0,
      "byteOffset": 0,
      "componentType": 5123,
      "count": 6,
      "type": "SCALAR",
      "max": [3],
      "min": [0]
    },
    {
      "bufferView": 1,
      "byteOffset": 0,
      "componentType": 5126,
      "count": 4,
      "type": "VEC3",
      "max": [1.0, 1.0, 0.0],
      "min": [0.0, 0.0, 0.0]
    },
    {
      "bufferView": 2,
      "byteOffset": 0,
      "componentType": 5126,
      "count": 5,
      "type": "SCALAR",
      "max": [4.0],
      "min": [0.0]
    },
    {
      "bufferView": 2,
      "byteOffset": 20,
      "componentType": 5126,
      "count": 5,
      "type": "VEC4",
      "max": [0.0, 1.0, 0.0, 1.0],
      "min": [0.0, 0.0, 0.0, -0.707]
    }
  ],

  "asset": {
    "version": "2.0"
  }
}