Vector3

How to 3D

Chapter 1: Points and Lines

Vector3

Three-dimensional space is the terminal to which graphics programs send their output. In order to situate objects at particular locations in this space, a program uses addresses. Physical spaces are addressed using streets and house numbers or latitudes and longitudes. Virtual spaces are frequently addressed using a Cartesian grid. A location on a Cartesian grid is a sequence of numbers describing how far the location is from some anchor point in the grid called the origin. These distances from the origin are called coordinates.

Cartesian coordinates are often written in vector form:

$$\begin{aligned} \text{origin} &= \begin{bmatrix}0 & 0 & 0\end{bmatrix} \\ \text{target} &= \begin{bmatrix}-5 & 8 & 2\end{bmatrix} \end{aligned}$$

Each individual number in the vector describes an offset along a single axis of the coordinate system. Disciplines and 3D programs disagree on how to label these axes. This book follows these conventions:

Face your right palm toward you and form a fist. Stick out your thumb; it's the positive x-axis. Stick out your index finger; it's the positive y-axis. Stick out your middle finger; it's the positive z-axis. The vector \(\begin{bmatrix}-5&8&2\end{bmatrix}\) is 5 units opposite your thumb, 8 units along your index finger, and 2 units along your middle finger.

You likely already know these terms because you have been dealing with Cartesian coordinate systems for many years. But now it's time to start programming with them.

Vector3 Class

Let's write an abstraction of three-dimensional vector. This time we won't give you the code. Instead you'll build up the abstraction bit by bit through a series of exercises.

Create file src/vector.ts and enter in the class you have built.

Vectors vs. Points

In physics, a vector is unrooted; it's a relative direction or offset that can be applied anywhere. A point, on the other hand, is an absolute position. In computer graphics, you might use a vector to represent the direction in which a player is facing and a position to represent the player's current location.

Both vectors and positions are sequences of three numbers, and a point behaves like a vector rooted at the origin. Because of the similarity between points and vectors, your Vector3 class will serve both notions. Not every operation that you'll add to this class applies to both, but most do.

Immutability

Though you just defined three setters for your Vector3 class, you should keep your classes immutable as much as possible. That is, instead of mutating the state of a vector, give back a new instance with the modified state. For example, this invert function is mutable:

invert() {
  this.x = -this.x;
  this.y = -this.y;
  this.z = -this.z;
}
invert() {
  this.x = -this.x;
  this.y = -this.y;
  this.z = -this.z;
}

This inverse function is immutable:

inverse() {
  return new Vector3(-this.x, -this.y, -this.z);
}
inverse() {
  return new Vector3(-this.x, -this.y, -this.z);
}

Prefer the immutable inverse over the mutable invert because immutability reduces the danger of shared data being modified in unpredictable ways.

Testing with a Driver

Your Vector3 class is a foundational part of the library of graphics code you will develop throughout this course. To keep it bug-free, you'll need to test it. You could test it from within a renderer running in the browser, but sometimes a simple script that you can run from the terminal is easier to wrap your head around. Let's figure out how to write one of those.

First, add this toString method to your vector abstraction:

export class Vector3 {
  // ...

  toString() {
    return `[${this.x}, ${this.y}, ${this.z}]`;
  }
}
export class Vector3 {
  // ...

  toString() {
    return `[${this.x}, ${this.y}, ${this.z}]`;
  }
}

Create file src/driver.ts and add this code:

import {Vector3} from './vector.js';

const vector = new Vector3(1, 2, 3);
console.log(vector.toString());
import {Vector3} from './vector.js';

const vector = new Vector3(1, 2, 3);
console.log(vector.toString());

Notice that this script imports the JavaScript version of the file, not the TypeScript version. This script is going to be run by Node rather than through the browser, and Node doesn't currently support TypeScript directly. Run the tester in your terminal with this command:

npm run driver
npm run driver

The driver script is defined in package.json to run the TypeScript compiler and then execute the resulting build/driver.js.

Use this driver script as a sandbox to informally test your abstractions. In particular, add the following immutable methods to your Vector3 class and test their results:

add(that: Vector3), which returns a new Vector3 that is the sum of this and that. Adding two vectors means adding their corresponding coordinates.
scalarMultiply(scalar: number), which returns a new Vector3 that is a scaled version of this. Each coordinate is scaled, or multiplied, by scalar.
normalize(), which returns a new Vector3 in which each coordinate of this is divided by the vector's magnitude.

Suppose you have a vector 3, 4, 5. What is its magnitude? Suppose you multiple the vector by the scalar 1.5. What is the scaled vector's magnitude?

What is the magnitude of a normalized vector?

Vector3 and VertexAttributes

Earlier in the hello-cornflower renderer we made a vertex buffer for a list of positions using a flat array of numbers, like this:

const positions = [
  0.0, 0.0, 0,     // vertex 0
  0.5, 0.5, 0,     // vertex 1
];
const positions = [
  0.0, 0.0, 0,     // vertex 0
  0.5, 0.5, 0,     // vertex 1
];

Soon you will want to represent each position as a Vector3 so that you can perform mathematical operations. What happens if you define the positions array like this?

const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0.5),
];
const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0.5),
];

Currently this generates an error because the VertexAttributes class expects a flat array of numbers. To use an array of Vector3, you must first flatten the vectors using the flatMap function:

const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0),
].flatMap(position => position.xyz);
const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0),
].flatMap(position => position.xyz);

That's enough coding for this first renderer. Commit and push your code to your remote Git repository.

← Vertex ArrayLecture: Origin →