Trackball
A trackball is an input device found in arcade cabinets, laptops, certain styles of mice, and some game controllers. It consists of a rotating sphere that a user spins in order to move objects like a centipede, mouse cursor, or claw. The RollerController built by Philips in the 1990s includes a trackball:

If we a physical trackball, we can devise a very natural input system for a 3D renderer: as the user rotates the trackball, the 3D model rotates in the exact same way. However, we don't need a physical trackball. We can pretend there's a virtual and invisible one filling the renderer's viewport, like this:
When we click on the renderer, we establish a vector from the trackball's center to its surface, just as if we were putting a finger on a physical trackball. As we drag, we form a new vector from the center. The rotation effected by the drag aligns the starting vector with the current vector.
A virtual trackball abstraction is handy to have in a graphics library. Let's examine its state and behaviors so that we can build our own.
State
The trackball needs several pieces of state in order to determine the user's intended rotation:
-
The mouse's position on the trackball when the first down event occurs. We'll give this position the name
mouseSphere0
. -
A matrix representing the concatenation of all previously completed rotations, which we'll call
previousRotater
. A rotation is completed only on the pointer's up event. -
A matrix representing the rotation due to any pointer dragging currently in progress and any previously completed rotations. We'll call this matrix
rotater
. - The width and height of the viewport, measured in pixels.
The first three of these pieces of state must be modified on mouse events. The fourth must be updated only when the viewport resizes.
Create lib/trackball.ts
and add in this class.
Behaviors
The trackball must support several behaviors to turn mouse events into a rotation matrix.
Constructor
Three of the instance variables will be in initialized by helper methods called on events, but the two matrices do need to be initialized. The previousRotater
will accumulate up the rotation, so it needs a prior value. The rotater
will be freely accessed by client renderers. It must therefore always have a value. Both start off as the identity transformation.
Set Viewport
The virtual trackball is overlaid across the viewport. When the mouse is clicked, its pixel space coordinates are converted into the normalized coordinates used by the trackball, which requires knowledge of the viewport dimensions. The renderer must thereform inform the trackball whenever the viewport changes size by calling the setViewport
method.
The trackball itself doesn't listen for any input events. That job is deferred to the renderer. This keeps windowing API code out of the class and makes it more reusable.
Pixel Coordinates to Sphere Coordinates
When the user clicks in the viewport, the trackball must figure out where the click is on the surface of the unit sphere filling the viewport. Turning a 2D mouse position in pixel space into a 3D position on the surface of the unit sphere is something we will have to do several times, which makes it a good candidate for a utility method that we'll name pixelToSphere
.
The first step of this method is to turn the mouse position from pixel coordinates to normalized coordinates, which we've already seen how to do. The normalized coordinates give us the xy-position of the mouse on the trackball's unit sphere, but we must also figure out the z-coordinate. Since all points on the unit sphere are 1 unit away from the origin, this sphere equation must be true:
We have the x- and y-coordinates, so we solve for z:
The user may click in the corners of the viewport where the trackball doesn't reach. In such cases, \(z^2\) will be negative and the square root will include an imaginary component. To avoid complex numbers, we clamp the mouse's location to the edge of the trackball, where z is 0.
The vector that is returned from pixelsToSphere
reaches from the origin to the point on the sphere where the mouse is.
Start
When the user first clicks down, the trackball must store the mouse's sphere coordinates mouseSphere0
for later use. We define a method that accepts a mouse's position in pixel space and updates this state.
A renderer must call this method on a pointer's down event. No rotation occurs yet, as the mouse has not moved.
Drag
Things get exciting when the mouse drags away after the initial down event. We put this excitement in a method named drag
. Just like start
, it receives the current mouse position in pixels. The first step is to locate its position on the sphere. Then we want a rotation that makes the vector from the original down event align with this new vector.
The remaining steps build on the ideas we see in the trackball renderer we saw earlier, repeated here for reference:
The Matrix4.rotateAround
method is useful here, but it needs an angle in degrees and an axis about which to rotate. How do we get the angle between the two magenta vectors? The dot product gives us the cosine of the angle, so we take the inverse cosine to get the angle. How do we get the yellow axis about which to rotate? It must be perpendicular to both vectors, so we take their cross product.
If the two vectors are parallel or anti-parallel, the cross product will be the zero vector. Rotating around the zero vector produces invalid vectors. We must safeguard against these degenerate angles. The angle between parallel vectors is 0, and the dot product is 1. The angle between anti-parallel vectors is 180, and the dot product is -1. Therefore, to avoid a bad axis, we only rotate when the dot product's absolute value is strictly less than 1.
We separate the incomplete rotation driven by the current drag from the rotations that have already been completed. This allows us to cancel the current rotation and revert back to the previous rotation.
End
When an up event occurs, the rotation is complete. A renderer calls the end
method to commit the current rotation to the accumulation of completed rotations.
Cancel
Should we wish to cancel the rotation in progress, we include a behavior that resets the rotation to only those rotations that were previously completed:
Some renderers respond to the Esc key by canceling a rotation in progress.