Open thygrrr opened 9 months ago
The window is actually a bit larger. Changing the range in the reproduction.gd
to 41 gives 2 full rotations, where the last 90° are ok again (and the first 270° are ok, too)
Rotation (% of 2pi)
0% (0, 0, 0, 1) == (0, 0, 0, 1)
5% (0, 0.156434, 0, 0.987688) == (0, 0.156434, 0, 0.987688)
10% (0, 0.309017, 0, 0.951056) == (0, 0.309017, 0, 0.951057)
15% (0, 0.453991, 0, 0.891007) == (0, 0.453991, 0, 0.891007)
20% (0, 0.587785, 0, 0.809017) == (0, 0.587785, 0, 0.809017)
25% (0, 0.707107, 0, 0.707107) == (0, 0.707107, 0, 0.707107)
30% (0, 0.809017, 0, 0.587785) == (0, 0.809017, 0, 0.587785)
35% (0, 0.891007, 0, 0.45399) == (0, 0.891007, 0, 0.453991)
40% (0, 0.951057, 0, 0.309017) == (0, 0.951057, 0, 0.309017)
45% (0, 0.987688, 0, 0.156434) == (0, 0.987689, 0, 0.156434)
50% (0, 1, 0, -0) == (0, 1, 0, -0)
55% (0, 0.987688, 0, -0.156435) == (0, 0.987689, 0, -0.156435)
60% (0, 0.951056, 0, -0.309017) == (0, 0.951057, 0, -0.309017)
65% (0, 0.891006, 0, -0.453991) == (0, 0.891007, 0, -0.453991)
70% (0, -0.809017, 0, 0.587785) != (0, 0.809017, 0, -0.587786) <<<<< ERROR
75% (0, -0.707107, 0, 0.707107) != (0, 0.707107, 0, -0.707107) <<<<< ERROR
80% (0, -0.587785, 0, 0.809017) != (0, 0.587785, 0, -0.809017) <<<<< ERROR
85% (0, -0.45399, 0, 0.891007) != (0, 0.45399, 0, -0.891007) <<<<< ERROR
90% (0, -0.309017, 0, 0.951057) != (0, 0.309017, 0, -0.951057) <<<<< ERROR
95% (0, -0.156434, 0, 0.987688) != (0, 0.156434, 0, -0.987689) <<<<< ERROR
100% (0, 0, 0, 1) != (0, -0, 0, -1) <<<<< ERROR
105% (0, 0.156435, 0, 0.987688) != (0, -0.156435, 0, -0.987689) <<<<< ERROR
110% (0, 0.309017, 0, 0.951056) != (0, -0.309017, 0, -0.951057) <<<<< ERROR
115% (0, 0.453991, 0, 0.891006) != (0, -0.453991, 0, -0.891007) <<<<< ERROR
120% (0, 0.587785, 0, 0.809017) != (0, -0.587786, 0, -0.809017) <<<<< ERROR
125% (0, 0.707107, 0, 0.707107) != (0, -0.707107, 0, -0.707107) <<<<< ERROR
130% (0, 0.809017, 0, 0.587785) != (0, -0.809018, 0, -0.587785) <<<<< ERROR
135% (0, 0.891007, 0, 0.45399) != (0, -0.891007, 0, -0.453991) <<<<< ERROR
140% (0, 0.951057, 0, 0.309017) != (0, -0.951057, 0, -0.309017) <<<<< ERROR
145% (0, 0.987688, 0, 0.156434) != (0, -0.987689, 0, -0.156434) <<<<< ERROR
150% (0, 1, 0, -0) != (0, -1.000001, 0, 0) <<<<< ERROR
155% (0, 0.987688, 0, -0.156435) != (0, -0.987689, 0, 0.156435) <<<<< ERROR
160% (0, 0.951056, 0, -0.309017) != (0, -0.951057, 0, 0.309017) <<<<< ERROR
165% (0, 0.891006, 0, -0.453991) != (0, -0.891007, 0, 0.453991) <<<<< ERROR
170% (0, -0.809017, 0, 0.587785) == (0, -0.809017, 0, 0.587786)
175% (0, -0.707107, 0, 0.707107) == (0, -0.707107, 0, 0.707107)
180% (0, -0.587785, 0, 0.809017) == (0, -0.587785, 0, 0.809018)
185% (0, -0.45399, 0, 0.891007) == (0, -0.453991, 0, 0.891007)
190% (0, -0.309017, 0, 0.951057) == (0, -0.309017, 0, 0.951057)
195% (0, -0.156434, 0, 0.987688) == (0, -0.156434, 0, 0.987689)
200% (0, 0, 0, 1) == (0, 0, 0, 1.000001)
My quick guess is that the problem you are seeing is Node3D
only stores the rotation as a basis, and you are conceptually thinking it is storing it as a quaternion. Afaik any rotation has 2 possible quaternion versions, so depending on the conversion code, maybe sometimes you will get flips if you try to retrieve a quat from this stored basis.
One solution if you wish to interpolate quaternion manually may be to manage the quaternion entirely in user code and update it every tick / frame, and not rely on getting it from the basis stored in the Node3D
.
This is my understanding, too, I was implementing various interpolators and realized that quaternion interpolation causes a reverse turn after 270° around the respective axis.
I don't think it should operate this way; in particular, setting the quaternion property should carry with it a reasonable expectation of getting the same quaternion back afterwards.
It's also notable that most other engines also encode their transforms in a basis, and don't show this behaviour when working with quaternions.
When creating camera interpolation with some inertia, you usually end up with an angular velocity in angle-axis representation. https://theorangeduck.com/page/spring-roll-call#quaternionspring (not a big fan of slerping euler angles - it's just not the same thing when doing XR or 6DOF stuff like flight sims or space games)
A workaround on user-side is to manually account for double cover.
Appending this line to the end of the loop in the demo script will reformulate our q in a way that it works with what get_quaternion returns, even if that call resulted in "flipping" the rotation. This is a deceptively expensive operation internally, as getting the quaternion isn't just a read.
q *= 1 if q.dot(quaternion) > 0 else -1
full code:
extends Node3D
func _ready() -> void:
print("Rotation (% of 2pi)")
var q = quaternion
for i in range(41): # Pretend an iterative rotation process that climbs up to 720° in 18° steps
var percent = str(i*5)+"%"
if quaternion.is_equal_approx(q):
prints(percent, quaternion, "==", q)
else:
prints(percent, quaternion, "!=", q, "<<<<< ERROR")
#assert(false, "Check output! Quaternions are not the same. Comparing the signs, notice that one is the complementary rotation. This doesn't work well for interpolation, and is mathematically incorrect.")
q *= Quaternion.from_euler(Vector3(0, PI/10.0, 0))
quaternion = q
q *= 1 if q.dot(quaternion) > 0 else -1
Output:
Vulkan API 1.3.242 - Forward+ - Using Vulkan Device #0: NVIDIA - NVIDIA GeForce RTX 3080
Rotation (% of 2pi)
0% (0, 0, 0, 1) == (0, 0, 0, 1)
5% (0, 0.156435, 0, 0.987688) == (0, 0.156434, 0, 0.987688)
10% (0, 0.309017, 0, 0.951057) == (0, 0.309017, 0, 0.951057)
15% (0, 0.453991, 0, 0.891007) == (0, 0.453991, 0, 0.891006)
20% (0, 0.587785, 0, 0.809017) == (0, 0.587785, 0, 0.809017)
25% (0, 0.707107, 0, 0.707107) == (0, 0.707107, 0, 0.707107)
30% (0, 0.809017, 0, 0.587785) == (0, 0.809017, 0, 0.587785)
35% (0, 0.891007, 0, 0.45399) == (0, 0.891007, 0, 0.45399)
40% (0, 0.951057, 0, 0.309017) == (0, 0.951057, 0, 0.309017)
45% (0, 0.987688, 0, 0.156434) == (0, 0.987688, 0, 0.156434)
50% (0, 1, 0, -0) == (0, 1, 0, -0)
55% (0, 0.987688, 0, -0.156435) == (0, 0.987688, 0, -0.156435)
60% (0, 0.951056, 0, -0.309017) == (0, 0.951057, 0, -0.309017)
65% (0, 0.891006, 0, -0.453991) == (0, 0.891006, 0, -0.453991)
70% (0, -0.809017, 0, 0.587785) == (0, -0.809017, 0, 0.587785)
75% (0, -0.707107, 0, 0.707107) == (0, -0.707107, 0, 0.707107)
80% (0, -0.587785, 0, 0.809017) == (0, -0.587785, 0, 0.809017)
85% (0, -0.45399, 0, 0.891007) == (0, -0.45399, 0, 0.891007)
90% (0, -0.309017, 0, 0.951057) == (0, -0.309017, 0, 0.951057)
95% (0, -0.156434, 0, 0.987688) == (0, -0.156434, 0, 0.987688)
100% (0, 0, 0, 1) == (0, 0, 0, 1)
105% (0, 0.156435, 0, 0.987688) == (0, 0.156435, 0, 0.987688)
110% (0, 0.309017, 0, 0.951056) == (0, 0.309017, 0, 0.951056)
115% (0, 0.453991, 0, 0.891006) == (0, 0.453991, 0, 0.891007)
120% (0, 0.587785, 0, 0.809017) == (0, 0.587785, 0, 0.809017)
125% (0, 0.707107, 0, 0.707107) == (0, 0.707107, 0, 0.707107)
130% (0, 0.809017, 0, 0.587785) == (0, 0.809017, 0, 0.587785)
135% (0, 0.891007, 0, 0.45399) == (0, 0.891007, 0, 0.45399)
140% (0, 0.951057, 0, 0.309017) == (0, 0.951057, 0, 0.309017)
145% (0, 0.987688, 0, 0.156434) == (0, 0.987688, 0, 0.156434)
150% (0, 1, 0, -0) == (0, 1, 0, -0)
155% (0, 0.987688, 0, -0.156435) == (0, 0.987688, 0, -0.156435)
160% (0, 0.951057, 0, -0.309017) == (0, 0.951056, 0, -0.309017)
165% (0, 0.891006, 0, -0.453991) == (0, 0.891006, 0, -0.453991)
170% (0, -0.809017, 0, 0.587785) == (0, -0.809017, 0, 0.587785)
175% (0, -0.707107, 0, 0.707107) == (0, -0.707107, 0, 0.707107)
180% (0, -0.587785, 0, 0.809017) == (0, -0.587785, 0, 0.809017)
185% (0, -0.45399, 0, 0.891007) == (0, -0.45399, 0, 0.891007)
190% (0, -0.309017, 0, 0.951057) == (0, -0.309017, 0, 0.951057)
195% (0, -0.156434, 0, 0.987688) == (0, -0.156434, 0, 0.987688)
200% (0, 0, 0, 1) == (0, 0, 0, 1)
--- Debugging process stopped ---
Godot version
4.2.beta, 4.1.2.stable
System information
Windows 11
Issue description
When incrementally updating a Node3D's quaternion property, either by *= or direct assignment, an erroneous operation in set_quaternion or get_quaternion will cause the value to flip to the complementary rotation after around 3/4ths of Pi.
This is incorrect and makes interpolation between quaternions over time error-prone (e.g. when using an interpolator that also tracks or limits angular velocity)
Steps to reproduce
reproduction.gd
: we're actually assigning the correct quaternion, but the rotation is corrupted by the conversion by the basis operation in set_quaternion or perhaps get_quaternion.You'll see something like:
Minimal reproduction project
quaternion-wrap-bug.zip