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:
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:
- The x-axis spans from left to right.
- The y-axis spans from bottom to top.
- The z-axis spans from beyond the screen to in front of the screen. That is, the positive z-axis points toward the viewer, not away from the viewer.
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.