In which you learn how to navigate space and start implementing a utility class to represent locations and directions in 3D.
Space is the stage on which visual programs tell their stories. In order to situate objects at particular locations, a program's space must have 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.
Three-dimensional 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 describes the distance from the origin along a single axis of the coordinate system. Different disciplines label these axes differently. This book follows these conventions, which are found in many computer graphics systems:
For example, the location \(\text{target}\) is -5 units left of the origin, 8 units above the origin, and 2 units in front of the origin.
You 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.
Create a new file named vector.js
in a project and define a class named Vector3
:
export class Vector3 {
constructor(x, y, z) {
this.coordinates = [x, y, z];
}
toString() {
return `[${this.coordinates[0]}, ${this.coordinates[1]}, ${this.coordinates[2]}]`;
}
}
This book makes no attempt to teach you JavaScript. Read the code and work to understand it. Consult books, the web, and the humans around you if you have questions.
To test the constructor and toString
method, create another file named test-vector.js
and add this code:
import {Vector3} from './vector.js';
const vector = new Vector3(1, 2, 3);
console.log(vector.toString());
Run the tester in your terminal with this command:
node test-vector.js
A good vector class provides getters and setters for the individual coordinates. Place these definitions inside the Vector3
class:
get x() {
return this.coordinates[0];
}
get y() {
return this.coordinates[1];
}
get z() {
return this.coordinates[2];
}
set x(value) {
this.coordinates[0] = value;
}
set y(value) {
this.coordinates[1] = value;
}
set z(value) {
this.coordinates[2] = value;
}
These getters and setters make the class appear to have public instance variables named x
, y
, and z
. That way you can write code like this:
console.log(vector.x, vector.y, vector.z);
vector.x = 0;
vector.z = vector.y;
console.log(vector.x, vector.y, vector.z);
Append these lines to test-vector.js
and run the script.
These properties are just an illusion. Behind the scenes, vector.x = 0
turns into a setter function call.
In physics, a vector is a relative direction or offset. The starting location to which the vector is applied is not specified. A position, on the other hand, is an absolute location. In computer graphics, you might use a vector to represent the direction in which a player is jumping and a position to represent the player's current location.
Both vectors and positions are sequences of three numbers. Because of their similarity, your Vector3
class will serve both notions. Technically, some operations that you'll add to this class won't be supported by both types. But many graphics developers overlook their differences.
You will eventually need to be able to compute a vector's magnitude. Magnitude is the length of a straight line going from the origin to the vector's coordinates. In two dimensions, you have computed magnitude using the Pythagorean theorem. The x-coordinate is the length of one side, and the y-coordinate is the length of the other. The straight line is the hypotenuse, which has this length:
$$ \text{magnitude} = \sqrt{x^2 + y^2} $$
The Pythagorean theorem extends to three dimensions:
$$ \text{magnitude} = \sqrt{x^2 + y^2 + z^2} $$
This value is reasonably implemented with a getter:
get magnitude() {
return Math.sqrt(
this.coordinates[0] * this.coordinates[0] +
this.coordinates[1] * this.coordinates[1] +
this.coordinates[2] * this.coordinates[2]
);
}
Add this getter to your Vector3
class and add tests to print the magnitudes of the following vectors:
Reason about the results you see.
Though you recently added three setters to your Vector3
class, you are encouraged to keep your classes immutable as much as possible. That is, instead of mutating the state of an object, give back a new instance with the modified state. For example, this invert
function is mutable:
invert() {
this.coordinates[0] = -this.coordinates[0];
this.coordinates[1] = -this.coordinates[1];
this.coordinates[2] = -this.coordinates[2];
}
This inverse
function is immutable:
inverse() {
return new Vector3(-this.coordinates[0], -this.coordinates[1], -this.coordinates[2]);
}
Immutable classes are easier to depend on because they don't change in unpredictable ways. When you write code that relies on a mutable object, you must live in the fear that the object could change at any moment.
Add the following methods to your Vector3
class and test their results:
add(that)
, which returns a new Vector3
that is the sum of this
and that
. The parameter that
is assumed to be another Vector3
.
scalarMultiply(scalar)
, 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. What is the magnitude of a normalized vector?
You could use your Vector3
class to define the positions
array in hello-orange
, like this perhaps:
const positions = [
new Vector3(0, 0, 0),
new Vector3(0.5, 0.5, 0.5),
];
This will fail, however, because the VertexAttributes
class expects a flat array of numbers. If you really want to use Vector3
, flatten the vectors using the flatMap
function:
const positions = [
new Vector3(0, 0, 0),
new Vector3(0.5, 0.5, 0),
].flatMap(vector => vector.coordinates);
You may want to study up on flatMap
if you haven't seen it before. Ignoring it and going with the original assignment to positions
is also fine.
Once you're done coding, commit and push your code to Git.