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 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:

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

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 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:

$$\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 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.

← Untransforming the MouseRaycasting →