Outlining the Problem

Vector math

The nature of a vector.

The fundamental problem is that positions and directions are two different things but, when looked at from a purely programming perspective - where each is entirely defined by decimal values in each of the three Cartesian axis (X, Y and Z) - they appear pretty much the same. As a result they are usually stored using the same data structure and used interchangeably. To be fair, any absolute position is effectively a direction vector taken relative to a global origin point (X=Y=Z=0). Also, as not all vectors need to be normalised, there is no sure way of distinguishing the values defining a relative vector from those defining an absolute position.

Another complicating factor is that the word ‘vector’ has a different meaning in computer programming than it does in geometry and physics. In programming, a vector typically refers to a dynamic linear array or a managed memory container such as a flat list. In geometry and physics, which is the domain of primary interest here, a vector refers to any quantity that has both a direction and magnitude. The Vector class referred to in the introduction above likely gets its name more from the fact that it stores multiple non-hierarchical values (three scalar dimensions - X, Y and Z) than from the type of information it contains. Thus, from a purely programming perspective, it makes sense and is more efficient to use the same class for both.

However, there are several operations and calculations that are specific to either position or direction information. This makes it not only possible but highly probable that someone using the API will at some point perform an operation on an absolute position thinking that it is a direction vector, or visa versa. Because the two are numerically indistinguishable, the calculation will work without error or warning but the results will be wrong, and that kind of problem can be very difficult and confusing to debug.

Implicit Assumptions

Most Vector classes have a magnitude() or length() calculation method, which only really makes sense when applied to a direction vector. You can use it with an absolute position, but this actually calculates that position’s distance from the global origin. This may be what you want, but it implicitly assumes that you intended to treat the absolute position as a direction vector.

Consider also a distance() method, which only really makes sense when calculated between two absolute positions. Again, you can use it with two direction vectors but you don’t get the total distance travelled, you get the distance between the two end points of the vectors as it implicitly assumes you intended to treat them as positions. The potential to use this method with an absolute position for one parameter and a direction vector for the other only adds to the likely confusion.

Also, most Vector classes implement both dotProduct() and crossProduct() methods which, again, only really make sense when using direction vectors. Whilst it is possible to derive these from absolute positions, you need to understand that they will be treated as vectors and implicitly assume the origin as your reference point whether you intended it or not. This same confusion applies to angle calculations as well, which is usually where things go most wrong in APIs such as the one I am doing, so is probably deserving of a more explicit illustration.

Angle Calculations

Angle calculations are very different when using absolute positions or relative direction vectors. The angle between two direction vectors is easily calculable as they form two arms which will either diverge or be parallel. The angle between two positions on the other hand, doesn’t really make sense unless taken relative to a third point from which the two arms can be extended. Thus, the two instances need different method signatures to calculate the same value:

float angleBetween(Vector v1, Vector v2)
float angleBetween(Point center, Point p1, Point p2)

If we were using a single Vector class for both, there would be nothing to stop a user trying to calculate the angle between two absolute positions using the first method - thinking they were getting the angle of the direction from point one to point two when in fact they were getting the angle formed between the origin and the two points.

Using Separate Classes

Another good reason for using separate classes is to avoid confusion when using objects which include both positional and directional information. These include relatively common things such as rays, cameras or even triangles with vertex normals.

To illustrate, lets define a ray that has a start point in space and then heads off in a specific direction. Consider then the situation when creating such a ray in two different ways - the first using a direction vector and the second using a specific target point:

new Ray(Point start, Vector direction)
new Ray(Point start, Point target)

The two different constructors allow some flexibility and are relatively self-explanatory in use. However, if there were no differentiation between position and direction, using instead a single Vector class for both, the two constructor signatures would be identical.

new Ray(Vector start, Vector direction)
new Ray(Vector start, Vector target)

Thus, there could only be one way to create the ray and the constructor would have no way of checking that the second parameter contained specifically position or specifically direction information. The design would have to rely solely on nomenclature and the end-user to first understand and then properly accommodate the distinction at all times when working with rays.

Having separate Point and Vector classes greatly simplifies that whole process and avoids all sorts of complex syntax and nomenclatures, such as any and all of the following:

// All of these can be easily avoided.
new Ray(Vector start, Vector.getDirection(start, target))
Factory.createRayWithDirection(Vector start, Vector direction)
Factory.createRayWithTargetPoint(Vector start, Vector target)
Ray.FromPositionAndDirection(Vector start, Vector direction)
Ray.FromPositionAndTarget(Vector start, Vector target)

Exactly the same situation occurs with cameras and view definitions:

new Camera(Point eyePos, Vector viewDirection)
new Camera(Point eyePos, Point lookAt)

Conclusion

Whilst having separate Point and Vector classes will make the analysis API a bit different from other frameworks, I think that it does make the overall design a bit clearer and safer. There is definitely more tedium involved in creating the API as there is a lot of code duplication between the two classes and you have to include methods for adding and subtracting points and vectors or vectors and points, etc.

That summarises my current thinking on this but I’d certainly be interested in anyone else’s experience or perspective.


Click here to comment on this page.