PVector
|
Both addition and subtraction with vectors follows the same algebraic rules as with real numbers.
The commutative rule: u + v = v + u The associative rule: u + (v + w) = (u + v) + w The fancy terminology and symbols aside, this is really quite a simple concept. We're just saying that common sense properties of addition apply with vectors as well. 3 + 2 = 2 + 3 (3 + 2) + 1 = 3 + (2 + 1) |
Moving onto multiplication, we have to think a little bit differently. When we talk about multiplying a vector what we usually mean is scaling a vector. Maybe we want a vector to be twice its size or one-third its size, etc. In this case, we are saying "Multiply a vector by 2" or "Multiply a vector by 1/3". Note we are multiplying a vector by a scalar, a single number, not another vector.
To scale a vector by a single number, we multiply each component (x and y) by that number.
Vector multiplication:
w = v * n
translates to:
wx = vx * n
wy = vy * n
Let's look at an example with vector notation.
u = (-3,7)
n = 3
w = u * n
wx = -3 * 3
wy = 7 * 3
w = (-9, 21)
The function inside the PVector class therefore is written as:
void mult(float n) {
// With multiplication, all components of the vector are multiplied by a number.
x = x * n;
y = y * n;
}
And implementing multiplication in code is as simple as:
PVector u = new PVector(-3,7); u.mult(3); // This PVector is now three times the size and is equal to (-9,21).
Example: Vector multiplication
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
// PVector multiplication!
// The vector is now half its original size (multiplied by 0.5).
mouse.mult(0.5);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
Division is exactly the same as multiplication, only of course using divide instead of multiply.
void div(float n) {
x = x / n;
y = y / n;
}
PVector u = new PVector(8,-4);
u.div(2);
|
As with addition, basic algebraic rules of multiplication and division apply to vectors.
The associative rule: (n*m)*v = n*(m*v) The distributive rule, 2 scalars, 1 vector: (n + m)*v = n*v + m*v The distributive rule, 2 vectors, 1 scalar : (u +v)*n = n*u + n*v |
Vectors: Magnitude
Multiplication and division, as we just saw, is a means by which the length of the vector can be changed without affecting direction. And so, perhaps you're wondering: "Ok, so how do I know what the length of a vector is?" I know the components (x and y), but I don't know how long (in pixels) that actual arrow is itself?!
The length or "magnitude" of a vector is often written as: ||v||
Understanding how to calculate the length (referred from here on out as magnitude) is incredibly useful and important.
Notice in the above diagram how when we draw a vector as an arrow and two components (x and y), we end up with a right triangle. The sides are the components and the hypotenuse is the arrow itself. We're very lucky to have this right triangle, because once upon a time, a Greek mathematician named Pythagoras developed a nice formula to describe the relationship between the sides and hypotenuse of a right triangle.
The Pythagorean theorem: a squared plus b squared equals c squared.
Armed with this lovely formula, we can now compute the magnitude of as follows:
||v|| = sqrt(vx*vx + vy*vy)
or in PVector:
float mag() {
return sqrt(x*x + y*y);
}
Example: Vector magnitude
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
// The magnitude (i.e. length) of a vector can be accessed via the mag() function.
// Here it is used as the width to a rectangle drawn at the top of the window.
float m = mouse.mag();
fill(0);
rect(0,0,m,10);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
Vectors: Normalizing
Calculating the magnitude of a vector is only the beginning. The magnitude function opens the door to many possibilities, the first of which is normalization. Normalizing refers to the process of making something "standard" or, well, "normal." In the case of vectors, let's assume for the moment that a standard vector has a length of one. To normalize a vector, therefore, is to take a vector of any length and, keeping it pointing in the same direction, change its length to one, turning it into what is referred to as a unit vector.
Being able to quickly access the unit vector is useful since it describes a vector's direction without regard to length. For any given vector u, its unit vector (written as û) is calculated as follows:
û = u / ||u||
In other words, to normalize a vector, simply divide each component by its magnitude. This makes pretty intuitive sense. Say a vector is of length 5. Well, 5 divided by 5 is 1. So looking at our right triangle, we then need to scale the hypotenuse down by dividing by 5. And so in that process the sides shrink, dividing by 5 as well.
In the PVector class, we therefore write our normalization function as follows:
void normalize() {
float m = mag();
div(m);
}
Of course, there's one small issue. What if the magnitude of the vector is zero? We can't divide by zero! Some quick error checking will fix that right up:
void normalize() {
float m = mag();
if (m != 0) {
div(m);
}
}
Example: Normalizing a Vector
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
// In this example, after the vector is normalized it is
// multiplied by 50 so that it is viewable onscreen.
// Note that no matter where the mouse is, the vector will
// have same length (50), due to the normalization process.
mouse.normalize();
mouse.mult(50);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
Vectors: Motion
Why should we care? Yes, all this vector math stuff sounds like something we should know about, but why exactly? How will it actually help me write code? The truth of the matter is that we need to have some patience. The awesomeness of using the PVector class will take some time to fully come to light. This is quite common actually when first learning a new data structure. For example, when you first learn about an array, it might have seemed like much more work to use an array than to just have several variables to talk about multiple things. But that quickly breaks down when you need a hundred, or a thousand, or ten thousand things. The same can be true for PVector. What might seem like more work now will pay off later, and pay off quite nicely.For now, however, we want to focus on simplicity. What does it mean to program motion using vectors? We've seen the beginning of this in this book's first example: the bouncing ball. An object on screen has a location (where it is at any given moment) as well as a velocity (instructions for how it should move from one moment to the next). Velocity gets added to location:
location.add(velocity);
And then we draw the object at that location:
ellipse(location.x,location.y,16,16);
This is Motion 101.
- Add velocity to location
- Draw object at location
The driving principle behind object-oriented programming is the bringing together of data and functionality. Take the prototypical OOP example: a car. A car has data -- color, size, speed, etc. A car has functionality -- drive(), turn(), stop(), etc. A car class brings all that stuff together in a template from which car instances, i.e. objects, are made. The benefit is nicely organized code that makes sense when you read it.
Car c = new Car(red,big,fast); c.drive(); c.turn(); c.stop();
In our case, we're going to create a generic "Mover" class, a class to describe a shape moving about the screen. And so we must consider the following two questions:
- What data does a Mover have?
- What functionality does a Mover have?
class Mover {
PVector location;
PVector velocity;
Its functionality is just about as simple. It needs to move and it needs to be seen. We'll implement these as functions named update() and display(). update() is where we'll put all of our motion logic code and display() is where we will draw the object.
void update() {
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
}
We've forgotten one crucial item, however, the object's constructor. The constructor is a special function inside of a class that creates the instance of the object itself. It is where you give the instructions on how to set up the object. It always has the same name as the class and is called by invoking the new operator: "Car myCar = new Car(); ".
In our case, let's just initialize our mover object by giving it a random location and a random velocity.
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
Let's finish off the Mover class by incorporating a function to determine what the object should do when it reaches the edge of the window. For now let's do something simple, and just have it wrap around the edges.
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
Now that the Mover class is finished, we can then look at what we need to do in our main program. We first declare a Mover object:
Mover mover;
Then initialize the mover in setup():
mover = new Mover();
and call the appropriate functions in draw():
mover.update(); mover.checkEdges(); mover.display();
Here is the entire example for reference:
Example: Motion 101 (velocity)
// Declare Mover object
Mover mover;
void setup() {
size(200,200);
smooth();
background(255);
// Make Mover object
mover = new Mover();
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
// Call functions on Mover object.
mover.update();
mover.checkEdges();
mover.display();
}
class Mover {
// Our object has two PVectors: location and velocity
PVector location;
PVector velocity;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
void update() {
// Motion 101: Locations changes by velocity.
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
}
Ok, at this point, we should feel comfortable with two things -- (1) What is a PVector? and (2) How do we use PVectors inside of an object to keep track of its location and movement? This is an excellent first step and deserves an mild round of applause. For standing ovations and screaming fans, however, we need to make one more, somewhat larger, step forward. After all, watching the Motion 101 example is fairly boring -- the circle never speeds up, never slows down, and never turns. For more interesting motion, for motion that appears in the real world around us, we need to add one more PVector to our class -- acceleration.
The strict definition of acceleration that we are using here is: the rate of change of velocity. Let's think about that definition for a moment. Is this a new concept? Not really. Velocity is defined as: the rate of change of location. In essence, we are developing a "trickle down" effect. Acceleration affects velocity which in turn affects location (for some brief foreshadowing, this point will become even more crucial in the next chapter when we see how forces affect acceleration which affects velocity which affects location.) In code, this reads like this:
velocity.add(acceleration); location.add(velocity);
As an exercise, from this point forward, let's make a rule for ourselves. Let's write every example in the rest of this book without ever touching the value of velocity and location (except to initialize them). In other words, our goal now for programming motion is as follows -- come up with an algorithm for how we calculate acceleration and let the trickle down effect work its magic. And so we need to come up with some ways to calculate acceleration:
ACCELERATION ALGORITHMS!
- Make up a constant acceleration
- A totally random acceleration
- Perlin noise acceleration
- Acceleration towards the mouse
class Mover {
PVector location;
PVector velocity;
PVector acceleration; // A new PVector for acceleration.
And incorporate acceleration into the update() function:
void update() {
// Our motion algorithm is now two lines of code!
velocity.add(acceleration);
location.add(velocity);
}
We're almost done. The only missing piece is initialization in the constructor.
Mover() {
Let's start the mover object in the middle of the window. . .
location = new PVector(width/2,height/2);
. . . with an initial velocity of zero.
velocity = new PVector(0,0);
This means that when the sketch starts, the object is at rest. We don't have to worry about velocity anymore as we are controlling the object's motion entirely with acceleration. Speaking of which, according to "algorithm #1" our first sketch involves constant acceleration. So let's pick a value.
acceleration = new PVector(-0.001,0.01);
}
Are you thinking -- "Gosh, those values seem awfully small!" Yes, that's right, they are quite tiny. It's important to realize that our acceleration values (measured in pixels) accumulate into the velocity over time, about thirty times per second depending on our sketch's frame rate. And so to keep the magnitude of the velocity vector within a reasonable range, our acceleration values should remain quite small. We can also help this cause by incorporating the PVector function limit().
// The limit() function constrains the magnitude of a vector. velocity.limit(10);
This translates to the following:
What is the magnitude of velocity? If it's less than 10, no worries, just leave it whatever it is. If it's more than 10, however, shrink it down to 10!
Let's take a look at the changes to the Mover class now, complete with acceleration and limit().
Example: Motion 101 (velocity and constant acceleration)
class Mover {
PVector location;
PVector velocity;
// Acceleration is the key!
PVector acceleration;
// The variable, topspeed, will limit the magnitude of velocity.
float topspeed;
Mover() {
location = new PVector(width/2,height/2);
velocity = new PVector(0,0);
acceleration = new PVector(-0.001,0.01);
topspeed = 10;
}
void update() {
// Velocity change by acceleration and is limited by topspeed.
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
// display() is the same
// checkEdges() is the same
}
Ok, algorithm #2 -- "a totally random acceleration." In this case, instead of initializing acceleration in the object's constructor we want to pick a new acceleration each cycle, i.e. each time update() is called.
Example: Motion 101 (velocity and random acceleration)
void update() {
acceleration = new PVector(random(-1,1),random(-1,1));
acceleration.normalize();
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
While normalizing acceleration is not entirely necessary, it does prove useful as it standardizing the magnitude of the vector, allowing us to try different things, such as:
(a) scaling the acceleration to a constant value
acceleration = new PVector(random(-1,1),random(-1,1)); acceleration.normalize(); acceleration.mult(0.5);
(b) scaling the acceleration to a random value
acceleration = new PVector(random(-1,1),random(-1,1)); acceleration.normalize(); acceleration.mult(random(2));
While this may seem like an obvious point, it's crucial to understand that acceleration does not merely refer to the speeding up or slowing down of a moving object, but rather any change in velocity, either magnitude or direction. Acceleration is used to steer an object, and it is the foundation of learning to program an object that make decisions about how to move about the screen.
Vectors: Static vs. Non-Static
Before we get to acceleration algorithm #4 (accelerate towards the mouse), we need to cover one more rather important aspect of working with vectors and the PVector class, the difference between using static methods and non-static methods.Forgetting about vectors for a moment, take a look at the following code:
float x = 0; float y = 5; x = x + y;
Pretty simple right? x has the value of 0, we add y to it, and now x is equal to 5. We could write the corresponding code pretty easily based on what we've learned about PVector.
PVector v = new PVector(0,0); PVector u = new PVector(4,5); v.add(u);
The vector v has the value of (0,0), we add u to it, and now v is equal to (4,5). Easy, right?
Ok, let's take a look at another example of some simply floating point math:
float x = 0; float y = 5; float z = x + y;
x has the value of 0, we add y to it, and store the result in a new variable z. The value of x is not changed in this example (neither is y)! This may seem like a trivial point, and one that is quite intuitive when it comes to mathematical operations with floats. However, it's not so obvious when it comes to mathematical operations with PVector. Let's try to write the code based on what we know so far.
PVector v = new PVector(0,0); PVector u = new PVector(4,5); PVector w = v.add(u); // Don't be fooled, this is incorrect!!!
The above might seem like a good guess, but it's just not the way the PVector class works. If we look at the definition of add() . . .
void add(PVector v) {
x = x + v.x;
y = y + v.y;
}
. . . we see that it does not accomplish our goal. Number one, it does not return a new PVector (the return type is "void") and number two, it changes the value of the PVector upon which it is called. In order to add two PVector objects together and return the result as a new PVector, we must use the static add() function.
Functions that we call from the class name itself (rather than from a specific object instance) are known as static functions.
// Assuming two PVector objects: v and u // Static: called off of the class name. PVector.add(v,u); // Not static: called off of an object instance. v.add(u);
Since you can't write static functions yourself in Processing, it is something you might not have encountered before. In the case of PVector, it allows us to generically perform mathematical operations on PVector objects, without having the adjust the value of one of the input PVector's. Let's look at how we might write the static version of add:
static PVector add(PVector v1, PVector v2) {
PVector v3 = new PVector(v1.x + v2.x, v1.y + v2.y);
return v3;
}
There are several differences here:
- The function is labeled as static.
- The function does not have a void return type, but rather returns a PVector.
- The function creates a new PVector (v3) and returns the result of adding the components of v1 and v2 in that new PVector.
PVector v = new PVector(0,0); PVector u = new PVector(4,5);PVector w = v.add(u);// The static version of add allows us to add two PVectors // together and assign the result to a new PVector while // leaving the original PVectors (v and u) intact. PVector w = PVector.add(v,u);
The PVector class has static versions of add(), sub(), mult(), div().
Vectors: Interactivity
Ok, to finish out this tutorial, let's try something a bit more complex and a great deal more useful. Let's dynamically calculate an object's acceleration according to a rule, acceleration algorithm #4 -- "the object accelerates towards the mouse."
Anytime we want to calculate a vector based on a rule/formula, we need to compute two things: magnitude and direction. Let's start with direction. We know the acceleration vector should point from the object's location towards the mouse location. Let's say the object is located at the point (x,y) and the mouse at (mouseX,mouseY).
As illustrated in the above diagram, we see that we can get a vector (dx,dy) by subtracting the object's location from the mouse's location. After all, this is precisely where we started this chapter -- the definition of a vector is "the difference between two points in space!"
dx = mouseX - x
dy = mouseY - y
Let's rewrite the above using PVector syntax. Assuming we are in the Mover class and thus have access to the object's location PVector, we then have:
PVector mouse = new PVector(mouseX,mouseY); // Look! We're using the static reference to sub() because // we want a new PVector pointing from one point to another. PVector dir = PVector.sub(mouse,location);
We now have a PVector that points from the mover's location all the way to the mouse. If the object were to actually accelerate using that vector, it would instantaneously appear at the mouse location. This does not make for good animation, of course, and what we want to do is now decide how fast that object should accelerate towards the mouse.
In order to set the magnitude (whatever it may be) of our acceleration PVector, we must first ________ that direction vector. That's right, you said it. Normalize. If we can shrink the vector down to its unit vector (of length one) then we have a vector that tells us the direction and can easily be scaled to any value. One multiplied by anything equals anything.
float anything = ????? dir.normalize(); dir.mult(anything);
To summarize, we have the following steps:
- Calculate a vector that points from the object to the target location (mouse).
- Normalize that vector (reducing its length to 1)
- Scale that vector to an appropriate value (by multiplying it by some value)
- Assign that vector to acceleration
void update() {
PVector mouse = new PVector(mouseX,mouseY);
// Step 1: direction
PVector dir = PVector.sub(mouse,location);
// Step 2: normalize
dir.normalize();
// Step 3: scale
dir.mult(0.5);
// Step 4: accelerate
acceleration = dir;
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
|
Why doesn't the circle stop when it reaches the target?
The object moving has no knowledge about trying to stop at a destination, it only knows where the destination is and tries to go there as fast as possible. Going as fast as possible means it will inevitably overshoot the location and have to turn around, again going as fast as possible towards the destination, overshooting it again, and so on, and so forth. Stay tuned for later chapters where we see how to program an object to "arrive"s at a location (slowing down on approach.) |
Let's take a look at what this example would look like with an array of Mover objects (rather than just one).
Example: Accelerating towards mouse
// Creating an array of objects.
Mover[] movers = new Mover[20];
void setup() {
size(200,200);
smooth();
background(255);
// Initializing all the elements of the array
for (int i = 0; i < movers.length; i++) {
movers[i] = new Mover();
}
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
// Calling functions of all of the objects in the array.
for (int i = 0; i < movers.length; i++) {
movers[i].update();
movers[i].checkEdges();
movers[i].display();
}
}
class Mover {
PVector location;
PVector velocity;
PVector acceleration;
float topspeed;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(0,0);
topspeed = 4;
}
void update() {
// Our algorithm for calculating acceleration:
PVector mouse = new PVector(mouseX,mouseY);
PVector dir = PVector.sub(mouse,location); // Find vector pointing towards mouse
dir.normalize(); // Normalize
dir.mult(0.5); // Scale
acceleration = dir; // Set to acceleration
// Motion 101! Velocity changes by acceleration. Location changes by velocity.
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
}
This tutorial is for Processing version 1.1+. If you see any errors or have comments, please let us know. This tutorial is adapted from the book The Nature of Code by Daniel Shiffman.