Trackball

How to 3D

Chapter 8: Interaction

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 the user spins in order to move a cursor or redirect a centipede. The RollerController built by Philips in the 1990s includes a trackball:

A trackball controller for the Philips CD-i media console. Image courtesy of Evan Amos.

If we have hardware with 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 the trackball rotates, so does the object. Figuring out the rotation to apply requires some vector math. 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 cross product of these two vectors is the axis of rotation and is shown in yellow. The angle between the two pink vectors is the number of degrees to rotate. From these we build a rotation matrix 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 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 that turn mouse events into a rotation matrix.

Constructor

Three of the instance variables will be initialized by helper methods, but the two matrices need to be initialized in the constructor. The previousRotater matrix stores just the completed rotations, and rotater stores the completed rotations and the current rotation. 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—probably in the resize event listener—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:

$$\begin{array}{rcl} x^2 + y^2 + z^2 &=& 1 \end{array}$$

We have the x- and y-coordinates, so we solve for z:

$$\begin{aligned} z^2 &= 1-x^2-y^2 \\ z &= \sqrt {1-x^2-y^2} \end{aligned}$$

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 pixelToSphere 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 in 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, as the mouse has not yet 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 itself. 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.

The virtual trackball is a nice addition to our graphics library. We could use it to write our own 3D modeling program or model viewer.

← Untransforming the MouseRaycasting →