Using the Dot Product
To begin, we reject the vector from the normal. I mean to subtract from the vector its projection over the normal. The vector projection looks like this:
Vector project(Vector a, Vector b)
{
return b * (dotProduct(a, b)) / (dotProduct(b, b));
}
However, if we know we are projecting on a vector of already 1
, then dotProduct(b, b)
is 1
. So we are left with:
Vector projectOnUnitVector(Vector a, Vector b)
{
return b * (dotProduct(a, b));
}
So we are going to do this:
Vector undesiredMotion = normal * (dotProduct(input, normal))
Then we subtract, which leads us to the accepted answer of the linked question:
Vector undesiredMotion = normal * (dotProduct(input, normal));
Vector desiredMotion = input - undesiredMotion;
And then we can normalize the result and scale it to the length we want, like this:
Vector undesiredMotion = normal * (dotProduct(input, normal));
Vector desiredMotion = input - undesiredMotion;
Vector output = normalize(desiredMotion) * length(input);
Using the Cross Product
I want to reiterate that I describe above is the general solution. If you are working in 2D, you have some advantages. First of all: You know all your vectors are coplanar.
So, we know that the result will be the normal, rotated a quarter turn (either clockwise or counterclockwise, we don't know yet) and scaled up to the length of the input.
And the second advantage is that doing the quarter turn rotation is trivial:
Vector2D rotateQuarterTurnCounterClockwise(Vector2D v)
{
return Vector2D(-v.y, v.x);
}
Or:
Vector2D rotateQuarterTurnClockwise(Vector2D v)
{
return Vector2D(v.y, -v.x);
}
But, what way do you want to turn it? Well, you want the result to turn from the input to the same side as the normal turns from the input. In other words, you the output have the same sign curl as the normal. I mean, you want the wedge product to have the sign. I mean, the length of the cross product. The cross product has different sign if the vectors are turning to one side or the other.
I mean:
sign(length(cross(input, normal))) == sign(length(cross(input, output)))
The cross product here would expand the vectors with 0
on the third axis. I remind you that we are working in 2D. So this is an external product (the result is outside the space of the inputs).
Now, as you know, if we multiply one tentative output by -1
, we get the other one. To reiterate, we either want the direction of the normal rotated a quarter turn clockwise or counterclockwise.
So if we do this:
Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));
We are always getting a direction that turns to the same side no matter what (if the direction was turning the other side, the product flips it). And then we do this:
Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));
direction = direction * sign(length(cross(input, normal)));
So we get a direction that turns from the input to the same side as the normal turns from the input.
Then you can scale it:
Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(length(cross(input, direction)));
direction = direction * sign(length(cross(input, normal)));
Vector2D output = direction * length(input);
Actually, it will be like this when we are done with it, I explain what is going on below:
Vector2D direction = rotateQuarterTurnCounterClockwise(normal);
direction = direction * sign(wedge(input, direction));
direction = direction * sign(wedge(input, normal));
Vector2D output = direction * length(input);
I want to reiterate that the approach I'm describing is for 2D only.
Although, you could take arbitrary input and normal vectors in 3D, find a plane that contains both, project, do the approach in this answer, and un-project the result… That extra work would decrease precision, decrease performance, and be error prone. Thus I would advice against it.
Alright, let us unpack this:
sign(length(cross(a, b)))
The cross product is like this:
Vector3D crossProduct(Vector3D a, Vector3D b)
{
return Vector3D(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
}
We are, however, defining one that works on 2D vectors. And this happens:
Vector3D cross(Vector2D a, Vector2D b)
{
return Vector3D(
a.y * 0.0 - 0.0 * b.y,
0.0 * b.x - a.x * 0.0,
a.x * b.y - a.y * b.x
);
}
In other words:
Vector3D cross(Vector2D a, Vector2D b)
{
return Vector3D(
0.0,
0.0,
a.x * b.y - a.y * b.x
);
}
Only the z axis of the result has value (after all, it has to be perpendicular to the inputs, and the inputs are in the XY plane). Consequently, the length is the value of the z axis. Let me define a function for that:
float wedge(Vector2D a, Vector2D b)
{
return a.x * b.y - a.y * b.x;
}
Therefore, instead of writing this:
sign(length(cross(a, b)))
We write this:
sign(wedge(a, b))
And nobody has to complain that you can't make a cross product of 2D vectors.
For contrast, this is dot product:
float dotProduct(Vector2D a, Vector2D b)
{
return a.x * b.x + a.y * b.y;
}
Wait a minute… This:
float wedge(Vector2D a, Vector2D b)
{
return a.x * b.y - a.y * b.x;
}
Is this:
float wedge(Vector2D a, Vector2D b)
{
return dotProduct(a, Vector2D(b.y, -b.x))
}
A dot product with a vector rotated a quarter turn clockwise!
Using Trigonometry
I suppose we can go about it yet another way: trigonometry. The idea is that we want to rotate the input vector. And we need to find out by what angle.
First we need to find the angle between the input and the normal… And that could be done with the dot product or the cross product, but since we want to do something else… Good old atan2:
float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
To be clear: this is also a 2D only solution. Project and un-project shenanigans apply.
To find out if it is a positive or negative turn:
float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);
And the output angle will be:
float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);
float outputAngle = normalAngle + quarterTurn * turnSign;
Here quarterTurn
is the angle that represents a quarter turn. That is 90º
, or TAU/4
radians, I mean… PI/2
.
And thus we need a turn of:
float inputAngle = atan2(input.y, input.x);
float normalAngle = atan2(normal.y, normal.x);
float turnSign = sign(normalAngle - inputAngle);
float outputAngle = normalAngle + quarterTurn * turnSign;
float turn = outputAngle - inputAngle;
We can rotate the input vector with a rotation matrix:
Vector output = RotationMatrix(turn) * input
Which is something like this:
+- -+ +- -+
| cos(turn) -sin(turn) | | input.x |
output = | | * | |
| sin(turn) cos(turn) | | input.y |
+- -+ +- -+
Which is the same as:
+- -+
| input.x * cos(turn) - input.y * sin(turn) |
output = | |
| input.x * sin(turn) + input.y * cos(turn) |
+- -+
In other words:
float c = cos(turn);
float s = sin(turn);
Vector2D output = Vector2D(input.x * c - input.y * s, input.x * s + input.y * c);
Wait, wait, I can rewrite that as two dot products:
float c = cos(turn);
float s = sin(turn);
Vector2D xRot = Vector2D(c, -s);
Vector2D yRot = Vector2D(s, c);
Vector2D output = Vector2D(dotProduct(input, xRot), dotProduct(input, yRot));
Muahahahaha!
Embrace the dot product.