Deprecated: Creation of dynamic property wpdb::$categories is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$post2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$link2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Return type of WP_Theme::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 554

Deprecated: Return type of WP_Theme::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 595

Deprecated: Return type of WP_Theme::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 535

Deprecated: Return type of WP_Theme::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 544

Deprecated: Return type of WP_REST_Request::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 960

Deprecated: Return type of WP_REST_Request::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 980

Deprecated: Return type of WP_REST_Request::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 992

Deprecated: Return type of WP_REST_Request::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 1003

Deprecated: Return type of WP_Block_List::current() should either be compatible with Iterator::current(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 151

Deprecated: Return type of WP_Block_List::next() should either be compatible with Iterator::next(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 175

Deprecated: Return type of WP_Block_List::key() should either be compatible with Iterator::key(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 164

Deprecated: Return type of WP_Block_List::valid() should either be compatible with Iterator::valid(): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 186

Deprecated: Return type of WP_Block_List::rewind() should either be compatible with Iterator::rewind(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 138

Deprecated: Return type of WP_Block_List::offsetExists($index) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 75

Deprecated: Return type of WP_Block_List::offsetGet($index) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 89

Deprecated: Return type of WP_Block_List::offsetSet($index, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 110

Deprecated: Return type of WP_Block_List::offsetUnset($index) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 127

Deprecated: Return type of WP_Block_List::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 199

Deprecated: Creation of dynamic property Automattic\Jetpack\Connection\Manager::$error_handler is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php on line 91

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Instagram_Gallery::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-instagram-gallery.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Mailchimp::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php on line 17

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 21

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 22

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php on line 20

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 24

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Subscribers::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php on line 15

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property WPCOM_Widget_Goodreads::$shelves is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/modules/widgets/goodreads.php on line 33

Deprecated: DateTime::__construct(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/twodee/blog/wp-includes/script-loader.php on line 348

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_classic_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 347

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 349

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block_side is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 350

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$panels_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 351

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$used_block_buttons is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 354

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198
» CS 488: Lecture 9 – Trackball

teaching machines

CS 488: Lecture 9 – Trackball

February 24, 2021 by . Filed under
Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383
graphics-3d, lectures, spring-2021.

Dear students:

Computer graphics is not just about rendering. The user must be able to interact with the scene in ways that feel natural. Today we investigate a way of letting the user spin around an object of focus using the mouse.

Rotation Around Axis

We’ve worked out how to rotate an object around the x-, y-, or z-axes by reducing the problem to two dimensions and using some trigonometric identities to rewrite the math into a form that can be expressed with a matrix multiplication. What if we want to rotate around an arbitrary axis, say $\begin{bmatrix} 1 & 1 & 1 \end{bmatrix}$? Complex rotations can be decomposed into this sequence of rotations around the standard axes:

  1. Rotate around the x-axis so that the axis of rotation lies in the xz plane.
  2. Rotate around the y-axis so that the axis of rotation aligns with the z axis.
  3. Rotate around the z-axis by the desired amount.
  4. Unrotate around the y-axis.
  5. Unrotate around the x-axis.

Figuring out the angles requires some projection and dot products. We’re not going to actually do that. But suppose we did. Then we could generate our matrix with this code:

function rotateAroundAxis(axis, degrees)
  angle1 = ...
  angle2 = ...
  return Matrix4.rotateX(-angle1) *
         Matrix4.rotateY(-angle2) *
         Matrix4.rotateZ(degrees) *
         Matrix4.rotateY(angle2) *
         Matrix4.rotateX(angle1)

We could multiply these matrices out and simplify to get a more direct representation. We’re not going to do that either. Our forebears have done that work for us. All told, the matrix that rotates $a$ radians about the normalized vector $\mathbf{v}$ is expressed as follows:

$$\begin{array}{rcl}s &=& \sin a \\c &=& \cos a \\R &=& \begin{bmatrix}(1-c) \cdot v_x \cdot v_x+c &(1-c) \cdot v_x \cdot v_y-s \cdot v_z &(1-c) \cdot v_x \cdot v_z+s \cdot v_y &0 \\(1-c) \cdot v_y \cdot v_x+s \cdot v_z &(1-c) \cdot v_y \cdot v_y+c &(1-c) \cdot v_y \cdot v_z-s \cdot v_x &0 \\(1-c) \cdot v_z \cdot v_x-s \cdot v_y &(1-c) \cdot v_z \cdot v_y+s \cdot v_x &(1-c) \cdot v_z \cdot v_z+c &0 \\0 & 0 & 0 & 1\end{bmatrix}\end{array}$$

Hang onto this matrix. We will need it in a bit.

Trackball

The trackball is an imaginary unit sphere centered in the viewport. When the user clicks, we pretend a finger was pressed to a trackball found in an arcade cabinet. As the mouse moves, the finger spins the ball. The object in the scene rotates in the same way as the trackball.

To turn a user’s mouse input into rotations, we’ll build an abstraction that encloses a few operations that will be tied to mouse click-and-drag events and the state we need to track the accumulating rotations.

State

We need the following instance variables in our abstraction:

Update Dimensions

To ensure that the virtual trackball always fills the viewport, we need to know its size. Our renderer needs to inform that trackball whenever the window changes size by calling this method:

function setViewport(width, height)
  this.dimensions[0] = width
  this.dimensions[1] = height

Pixel Coordinates to Sphere Coordinates

When the user clicks in the viewport, our first step is to figure out where the click is on the surface of the unit sphere. We’ll put the code to do this in a reusable utility method. The mouse coordinates are given to us in pixel or screen space. Let’s first move them into NDC space:

function pixelsToSphere(mousePixels)
  mouseNdc = mousePixels / this.dimensions * 2 - 1

This conversion gives us the x- and y-coordinates of the sphere where the mouse appears. We don’t have the z-coordinate, but we need it on order to figure out the rotation. How can we figure it out? We know this equation about points on the unit sphere, since all points on the sphere must be 1 unit away from the origin:

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

If we plug in our known x- and y-coordinates, we can solve for z:

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

Sometimes the user may click in the corners where the trackball doesn’t fit. In such cases, $z^2$ will be negative. We’ll avoid complex numbers by clamping the mouse’s location to the edge of the trackball, where z is 0. We arrive at code that looks like this:

function pixelsToSphere(mousePixels)
  mouseNdc = mousePixels / this.dimensions * 2 - 1
  zSquared = 1 - mouseNdc.x ^ 2 - mouseNdc.y ^ 2
  if zSquared > 0
    return Vector3(mouseNdc.x, mouseNdc.y, zSquared ^ 0.5)
  else
    return Vector3(mouseNdc.x, mouseNdc.y, 0).normalize()

Start

When the user first clicks down, there’s not much we can do as there’s no rotating action yet. However, we do want to register this click as our anchor point:

function start(mousePixels)
  this.mouseSphere0 = this.pixelsToSphere(mousePixels)

Drag

Things get exciting when the mouse drags away after the click. We turn the new mouse position into a second location on the unit sphere:

function drag(mousePixels, multiplier)
  mouseSphere = this.pixelsToSphere(mousePixels)

The parameter multiplier is included so that we can speed up the rotation.

Our two locations on the trackball represent two vectors. We want to figure out the math that will take the first vector and rotate it to align with the second vector. First, let’s figure out the angle between the two vectors:

function drag(mousePixels, multiplier)
  mouseSphere = this.pixelsToSphere(mousePixels)
  dot = this.mouseSphere0.dot(mouseSphere)

Only if the two vectors are not coincident or antiparallel are we able to figure out the axis around which we are rotating. If the dot product is not near 1 or -1, we can figure out the axis using the cross product:

function drag(mousePixels, multiplier)
  mouseSphere = this.pixelsToSphere(mousePixels)
  dot = this.mouseSphere0.dot(mouseSphere)
  if |dot| < 0.9999
    radians = acos(dot) * multiplier
    axis = this.mouseSphere0.cross(mouseSphere).normalize()

With the angle and axis known, we can call upon our matrix utility method to generate the transformation. In order to make the rotation “cancelable,”, we keep the current rotation separate from the previous rotations that have already been completed:

function drag(mousePixels, multiplier)
  mouseSphere = this.pixelsToSphere(mousePixels)
  dot = this.mouseSphere0.dot(mouseSphere)
  if |dot| < 0.9999
    radians = acos(dot) * multiplier
    axis = this.mouseSphere0.cross(mouseSphere).normalize()
    currentRotation = Matrix4.rotateAroundAxis(axis, radians * 180 / pi)
    this.rotation = currentRotation * this.previousRotation

End

When the mouse events finish, let’s commit the rotation and clean up the stale data:

function end()
  this.previousRotation = this.rotation
  this.mouseSphere0 = null

Cancel

Should we wish to cancel the rotation in progress, we restore the previous rotation:

function cancel()
  this.rotation = this.previousRotation
  this.mouseSphere0 = null

Renderer

Our renderer makes an instance of Trackball and maintains its state on each mouse event. In our mouse handlers below, we receive the event parameter. We only want the trackball rotation to work on clicks and drags of the left mouse button. JavaScript has several ways of asking which button is pressed, but each browser treats them differently. To avoid this inconsistent support, we use the button property in the onMouseDown handler and set a global flag. Note all that we must trigger a re-render on a drag event to reflect the trackball’s new rotation. The mouse coordinates comes to us from a coordinate space where the origin is at the top left corner of the window and the y-axis points down. We move into a coordinate system where the bottom left corner is the origin and the y-axis points up by taking the complement of mouse’s y-position.

let isLeftMouseDown = false;
let trackball;

function render() {
  // ...
  shaderProgram.setUniformMatrix4('modelToWorld', trackball.rotation);
  // ...
}

async function initialize() {
  trackball = new Trackball();

  // ...

  window.addEventListener('mousedown', onMouseDown);
  window.addEventListener('mousemove', onMouseDrag);
  window.addEventListener('mouseup', onMouseUp);
}

function onMouseDown(event) {
  if (event.button === 0) {
    isLeftMouseDown = true;
    const mousePixels = new Vector2(event.clientX, canvas.height - event.clientY);
    trackball.start(mousePixels);
  }
}

function onMouseDrag(event) {
  if (isLeftMouseDown) {
    const mousePixels = new Vector2(event.clientX, canvas.height - event.clientY);
    trackball.drag(mousePixels, 2);
    render();
  }
}

function onMouseUp(event) {
  if (isLeftMouseDown) {
    isLeftMouseDown = false;
    const mousePixels = new Vector2(event.clientX, canvas.height - event.clientY);
    trackball.end(mousePixels);
  }
}

function onSizeChanged() {
  // ...
  trackball.setViewport(canvas.width, canvas.height);
  // ...
}

TODO

Here’s your TODO list:

See you next time.

Sincerely,

P.S. It’s time for a haiku!

Goodbye, steering wheels
So long, brake and gas pedals
Trackballs, it’s your turn