got trackball rotation for the rotation gizmo in &half-edge working now! lots of quaternion math today, which was fun!
there are a few things that made this an interesting case to implement:
- all my handles cache the initial state when you start dragging, and then only add a delta. this is useful to prevent the kind of drifting/precision loss you sometimes see in certain gizmos when you only ever apply a per-frame delta, which will accumulate error. unreal's rotation gizmo had this issue, for example
- in this case, unlike the per-axis rotation, I still do need a cumulative delta
given these constraints, there were lots of space transformation with quaternions I had to do which was kind fun to brush up on and finally grok more properly, so I wrote down some quaternion notes which might be useful to others too! my solution was basically:
- on start drag, cache the current world space quaternion rotation
w
- the mouse pixel delta is divided by the radius (in pixels) of the gizmo, which makes it so that the distance you move the cursor, also corresponds to the radians you want to rotate by, thanks to radians being 1:1 with arc length (yay radians :heart_cat: )
- the magnitude of this scaled delta is the angle, and the axis of rotation is a perpendicular vector to this direction, which is a simple (-y,x) or (y,-x) swizzle
- from this, we can construct a quaternion representing a per-frame delta rotation, as a camera space quaternion
- this is then accumulated in another camera space quaternion, by composition
delta = frameDelta * delta
, the order here ensuring delta is applied in the extrinsic space (swapping them would apply the rotation in the frameDelta's own intrinsic axes, which is, not what we want)
- then, in order to calculate the final rotation, we need to transform our camera space rotation
delta
, and apply it to a world space rotation. In other words, we want to transform the delta
rotation as an intrinsic rotation applied to the camera's rotation c
, but as an extrinsic (world space) rotation.
- I did a bunch of math today, and basically it ends up being
c * delta * c.conjugate()
, which, as applied to our initial cached world space starting orientation w
, we get
c * delta * c.conjugate() * w
, aaaand then it works!
thanks for coming to my ted talk :henlo: