godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 76 forks source link

Add a method to perform quaternion swing-twist decomposition #8906

Open Remi123 opened 8 months ago

Remi123 commented 8 months ago

Describe the project you are working on

3D platformer

Describe the problem or limitation you are having in your project

Godot provide a good interface to interact with Quaternion, However it can be difficult to retrieve the rotation around an arbitrary axis from a Quaternion. Fortunately, there exists an algorithm to do exactly that : The Swing-Twist Decomposition.

This separate the quaternion into two sub-quaternion, the swing and the twist. While the name aren't exactly descriptive, it use the twist to describe the rotation around the axis, and the swing as "the rest" of the rotation.

You can use this for a number of use case. I personally use it in my character code to align the velocity to the floor normal, and also extract the hips rotation to place it on a new root bone in case the animation have hips as the root bone.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Add two functions to Quaternion : func get_swing(axis:Vector3)->Quaternion and func get_twist(axis:Vector3)->Quaterion.

Most of the implementation uses reference parameter to decompose the quaternion into two, using the same function. While great and very slightly more performant if you need the swing, the twist is the main part of the function and should, in my opinion, be its own function.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The code below is what I use, but those are static function, so the change will be added to the quaternion interface and be called like myquaterion.get_twist(Vector3.UP)

static func get_twist(q : Quaternion, axis : Vector3) -> Quaternion:
        # Assert that the quaternion and the axis is normalized
    var p :Vector3 = Vector3(q.x,q.y,q.z).dot(axis) * axis
    var twist :Quaternion= Quaternion(p.x, p.y, p.z,q.w).normalized()
    return twist
static func get_swing(q : Quaternion, axis : Vector3) -> Quaternion:
    var twist := get_twist(q,axis)
    var swing := -(q.inverse() * twist) 
    return swing

If this enhancement will not be used often, can it be worked around with a few lines of script?

It's a very simple addition to the engine.

Is there a reason why this should be core and not an add-on in the asset library?

It's too simple to not add to core.

Remi123 commented 8 months ago

We can also rename get_twist to get_rotation_around(axis:Vector3), but the swing doesn't have a good name that I can think off, since it's mostly the rest of the rotation from the twist.

WojtekPachowiak commented 2 months ago

What kind of twist-swing decomposition is this? twist-after-swing or swing-after-twist?

static func get_swing(q : Quaternion, axis : Vector3) -> Quaternion:
    var twist := get_twist(q,axis)
    var swing := -(q.inverse() * twist) 
    return swing

This code is weird. Why the minus sign? Also, the quaternion multiplication seems to be wrong. In the case of swing-after-twist decomposition we have:

q = swing * twist

therefore

swing = q * twist.inverted()
Remi123 commented 2 months ago

@WojtekPachowiak The calculation presented in the proposal isn't erroneous, as it give the correct swing and twist. but it wasn't constant when multiplying them back together since quaternion have two valid representation. It wasn't possible to do something like this :

var v := Vector(...)
var q := Quaternion(...)
var swing := q.get_swing(v)
var twist := q.get_twist(v)

assert(swing * twist == q)

I've fixed this in the PR at https://github.com/godotengine/godot/pull/87285