The problem is likely that you’re using Frotators at all.
If you are using quaternions for true “free” movement, you want to each frame do:
- Get “up” axis
- rotate by right-movement around “up” axis (this is viewer-relative left/right)
- Get “right” axis
- Rotate by down-movement around “right” axis (this is viewer-relative up/down)
- The quaternion you end up with is your new actor orientation
- re-normalize this quaternion
This will always rotate exactly the way you expect by looking at the camera ahead. However, you will accumulate roll compared to the world up axis unless you exactly back-trace your mouse path to look back forward again. This is how relative rotations work – Euler angles get around this by mapping mouse movement to world-absolute values and re-generating the rotation each frame.
If there is a “gravity” or “up” vector that you want to favor, you may need to add a separate kind of rotation that procedurally drives the derived quaternion rotation towards that up vector.