The Cross Product – For 3D Artists

In my last “Math For 3D Artists” post, I took a look at the Dot Product which is a simple, and immediately useful for artists, especially when writing shaders.

The Cross Product however is a powerful tool in the 3D rendering pipeline responsible for a lot of triangle calculation which makes what 3D artists do, work. In this post I’m going to explore what the Cross Product enables, and a more accessible use case for artists in Shader Graph.

TL;DR

The Cross Product can help us work out the surface normal of a triangle, which direction the surface is pointing, the surface area of a triangle, how to apply normal maps to materials, and even how to move and rotate things in 3D.

But being aware of how your triangles are made, even if only in theory, can help debug wonky meshes, write shaders, and who knows, maybe you’ll realise it’s not so scary and try yourself one day.

What is the Cross Product?

Unlike the Dot Product ·, which outputs a floating point value, and works on any dimensional vector (2D, 3D, etc), the Cross Product × inputs two 3D vectors, and outputs another 3D vector.

dot((0,1,0), (1,0,0)) = 0.0
cross((0,1,0), (1,0,0)) = (0,0,1)

The dot product of two vectors is the relationship between their length and angle, the Cross Product gives you a perpendicular vector (a vector at a right angle) to the two vectors. Take the image below;

Image of unit vector’s A and B, and their Cross Product
A × B

Unit vector’s A & B (red and the blue arrow) are fed into the Cross Product operator, either a node, or commonly represented as cross() in code. The output of this is the green vector, labelled as A × B. We’ve seen this arrangement of vectors before in 3D packages like Maya and Blender, it’s starting to look very similar to our axis gizmo.

Output Vector Length

The output of a cross() function is not often a unit vector (see Dot Product for an intro to unit-vectors). If vectors a & b are perpendicular and themselves unit vectors then it will be. It shouldn’t always be assumed, so it’s often best to normalise the output vector if all we need is the direction.

An interesting aspect of the Cross Product’s length however, is its relationship to the size of a triangle they create if their starting points were touching. Note that they do not need to be touching however for this to be true, this relationship is based on the length of the vectors and their angle, not their physical position in space. The following is just a nice way to visualise it.

The points a, b, and c of our triangle visualised above, with vectors A and B coloured in red and green.

Take the image above; lets assume we’re in 2D for now even though our Cross Product is a 3D process. vectors A and B in that diagram represent our input vectors of our Cross Product. The line connecting the ends of A and B is labelled C completes a triangle.

Now imagine we duplicated and flipped this triangle to create a parallelogram. This shape’s surface area is equal to the length of the output Cross Product. So if we halved this length, we would have the surface area, or size, of our triangle – labelled above as ΔABC.

What Units?

This will depend on your software package / engine. However most sane ones use units in meters. So a unit-length vector would be 1 meter long. This then means in our following example, the surface area of our parallelogram being 1 means it’s 1m2.

Image showing scenario where the length of the Cross Product of A and B would be 1

So in the above, we have an example where vector’s A and B are at right-angles to each other. The parallelogram which is created is in-fact a square. A and B are also unit vectors in this example, so our parallelogram has an area of 1, and our Cross Product is 1 unit long.

This animated triangle hopefully makes the relationship between the elements of the Cross Product I’ve discussed make a little more sense. As point b is moved (or more specifically vector A rotated around point a), we can see the surface area of the triangle changed, along with the length of the cross product, labelled cp.

Orthogonal Space

Along with the two input vectors and this output Cross Product, we now have what is known as Orthogonal Space (or sometimes Normal Space when speaking about texture mapping), which forms the basis of our co-ordinate system. In 3D packages, our co-ordinate space is along our cardinal axis, X, Y, and Z. These are represented above in Maya’s movement gizmo by the Red, Green, and Blue arrow gizmos.

Orthogonality

This generally means, when things are perpendicular, or at right-angles, to each other.

In Maya, we don’t need the Cross Product to help find any of the axis in our co-ordinate space as it’s intuitively defined as (1, 0, 0), (0, 1, 0), and (0, 0, 1), So why do we care? The Cross Product helps us define our orthogonal space whenever we need it and our points in space do not perfectly align with our cardinal axis. A key application of this is when we’re textures to meshes, like applying normal maps.

Computing Surface Normals

This is still going to be a more theoretical section, but it demonstrates the Cross Product in the context of 3D art quite well; and that’s how to compute a triangle’s Face Normal.

What is Normal?

3D artists should be well acquainted with a “normal”. It’s the unit vector, or direction, a triangle (polygon) is pointing. It instructs our renderer how to simulate lighting on our surface and is essential for deciding what’s the front and back of a triangle.

Face Normals

The way this works might not be directly clear at first. Remember, Cross Product; two vectors in, one perpendicular vector out.

Our hero, triangle abc

Take our triangle; we have 3 positions in space we’ll label a, b, and c. If we subtract one position from another, we are left with the vector from the second point to the first. So a-b = vector A, and a-c = vector B.

abc with the two vectors A and B, which form the basis of our Cross Product computation

We then supply AB and AC to our Cross Product function, normalise the output, and we have our normal.

abc having taken the Cross Product of the above, now has a face normal. Positioned in the centre of the triangle for clarity.

As the Cross Product is not commutative, the order we supply the input vectors in matters. Specifically, it tells is which way the vector is pointing, relatively up, or down. When we looked at the Dot Product, we used a surface normal and our view normal to determine which direction the triangle was facing. An easier way to visualise this would be using the “right hand rule”.

As in the image above, if you took your right hand (specifically not your left), the first vector supplied can be thought of as our index finger and forward, our middle finger being the second input vector. Our thumb is then our output vector.

If to swap input vectors, you’d need to point your hand to the left, so your index finger is now point in this new “forward”. You’d then rotate your wrist 180 degrees so that your thumb is now pointing down, and your middle finger is now pointing in the original forward before we flipped directions.

In regards to face normal calculation, our 3D software determines the front or back facing direction based on the order in which our vertices (triangle points) are stored. Flipping the triangle ABC would change it to ACB, and is similar to what Maya or Blender would do to the mesh if told it to reverse a triangle’s direction.

Unity & Unreal are Lefties

So as much as conventional Math, Physics, and Maya work assuming the “Right Hand Rule”, Unity & Unreal however use a “Left Hand” co-ordinate space. This doesn’t change the fundamental math happening, but it means that the order of vectors passed into the Cross Product needs to be changed.

Vertex Normals

There’s a catch when talking about face normals, this is because most 3D packages store mesh data as a list (or many lists) of vertices, not triangles. Storing mesh data as vertices is the most efficient way. If you were to store a list of triangles instead, a lot of information about each triangle would be repeated as many triangles share vertices.

If a face normal is the unit length vector of a triangle – a collection of 3 points in space, a vertex normal is an individual vertex point’s normal.

Vertex normals in magenta, notice they do not change as fast as the grey face normals. It’s this low rate of change which makes them smoother.

Vertex normals are much better for lighting calculations. If we were to light a mesh simply with face normals (or vertex normals aligned to face), we’d show how faceted and potentially low detail our 3D mesh really is.

We could simply increase the number of triangles in a mesh, as in the following image. We’d then have a nice smooth sphere.

Left: A default 100 face sphere, and next to it Right: a very smooth 500,000 face sphere.

In runtime 3D rendering, throwing millions of triangles at meshes – to the point where mesh wireframes are almost a solid image, as above, causes a massive headache if we want to render games at faster and smother framerates.

So back to the vertex normal; the way to find this is take the sum (add up) the face normals of each triangle the vertex is a part of, and then normalise it so it’s once again only 1 unit long. This gives us the average normal of the surrounding triangles, which avoids harsh lighting transitions.

Left; shading using face normals, Right; averaged vertex normals. The mesh is otherwise the same.

In fact, averaging vertex normals from surrounding faces is the most efficient way to render meshes. If you, the artist, specifically wanted hard shaded edges in your mesh, your 3D package would need to duplicate the vertex data as each vertex would need their own normal, even if the position was exactly the same.

In the above image (from Maya), note the segment of a sphere mesh on the left showing yellow singular averaged vertex normals. On the right it becomes a little harder to read as each triangle’s vertices have their own normal.

With our vertex normals calculated (or deliberately authored to be different), our 3D package now stores this in our list of vertex data, to be passed either to another 3D package (Maya to Unity), or from our 3D package to our renderer (Unity to GPU).

But what does our mesh data look like? If you’re curious, take a look at the Polygon Soup.

To Summarise

The cross product helps us compute the facing direction of a triangle. This value then continues on to help us better describe the lighting of the surface when we use it to get our averaged vertex normals. But how can we further use the Cross Product to help better shade our geometry? I’ve written another short post here on tangent space and normal maps to give a little extra foundation if you’re interested.

Billboard Shader – Angle Axis Rotation

A code-free implementation of the Cross Product in action would be via a billboard shader in a node-based shader tool. Billboarding is the practise of taking a simple mesh object, typically a quad (two triangles), and permanently orienting it to face the game camera. The most classic example of this would be ID Software’s original Doom.

There are many ways to ‘bill’ a ‘board’

The Cross Product isn’t the most efficient way to “billboarded” something, but it does break down this math nicely and puts the dot product and now the Cross Product to work.

One of the now-iconic visual elements of Doom (and other shooters in its wake) is this use of billboarding, and how every non-environment object always faces you. Sprite-based enemies were used as hardware and software limitations made it easier to draw rectangular animated sprite sheet graphics, then actually rendering complex 3D animated meshes.

The technique we’ll look at is in essence similar, but different as Doom’s engine wasn’t a 3D renderer as Unity or Unreal are today. ID Software’s engine didn’t actually draw 3D polygonal quads, rather rectangles in screen space.

The Rotation In Principle

To rotate a mesh via a shader, we’ll need to do some familiar math in the vertex part of our shader. The process follows as;

  1. Get the camera direction – the direction our billboard wants to face
  2. Get the billboards current forward facing direction
  3. Get the dot product of the target and current vectors.
  4. Take the inverse cosine of this dot product, also known as the arcos – this gives our rotation angle in radians.
  5. Compute the Cross Product of our two original vectors and normalise it. We now also how our rotation axis.
  6. Create a rotation matrix so we can rotate our billboard.
  7. Rotate our billboard with this matrix.
  8. Get our rotated billboard’s new “up” vector
  9. Get an “up” vector in view space
  10. Now perform the same dot and Cross Product process (3-5) on our “view up” and “billboard up”
  11. Perform one final rotation on the rotated vertex position from before with the Rotate About Axis function.

Shader Graph

Billboard Cross Product shader graph. Click to enlarge, notes follow.

Rotate About Axis Matrix Source Code

#ifndef HELPERS_DEFINED
#define HELPERS_DEFINED

void RotateAboutAxis_Radians_Matrix_float(float3 Axis, float Rotation, out float3x3 Out) {
    float s = sin(Rotation);
    float c = cos(Rotation);
    float one_minus_c = 1.0 - c;

    Axis = normalize(Axis);
    float3x3 rot_mat =
    {   one_minus_c * Axis.x * Axis.x + c, one_minus_c * Axis.x * Axis.y - Axis.z * s, one_minus_c * Axis.z * Axis.x + Axis.y * s,
        one_minus_c * Axis.x * Axis.y + Axis.z * s, one_minus_c * Axis.y * Axis.y + c, one_minus_c * Axis.y * Axis.z - Axis.x * s,
        one_minus_c * Axis.z * Axis.x - Axis.y * s, one_minus_c * Axis.y * Axis.z + Axis.x * s, one_minus_c * Axis.z * Axis.z + c
    };
    Out = rot_mat;
}

#endif // End HELPERS_DEFINED

Notes on this approach

I naively set out to demonstrate the Cross Product with a shader graph because I thought it would be a simple example, but I quickly discovered why billboard shaders aren’t best served by the Cross Product…

I’d originally oriented our billboard down the camera’s forward vector. This did lead to it always facing the camera, but it would then rotate wildly in its “forward” axis like a pinwheel – which is not ideal for billboards. To avoid this, we need to also constrain the billboards “up” direction in view space.

Shadergraph’s “Rotate About Axis” node

Normally to perform an angle-axis rotation, you’d use a suitable node – in this case Unity’s “Rotate About Axis” vector node. This node requires the axis about which you want to rotate your mesh, and the angle, or amount you wish to rotate it. It also needs the vector of what you want to rotate. This vector can be a position in space, or a direction, but in our case we’re rotating vertex positions.

Quick aside on Matrices

Our target vector is our camera’s direction, which helpfully is already supplied by Shadergraph, but our meshes “forward” isn’t as straight forward to find here. To get this, we need to take it from the object matrix.

Matrix construction node, showing an “identity” (default) matrix, a matrix with no translation, rotation, or additional scale.

I’ll revisit matrices properly one day, but to give enough context for things to make sense. A matrix is usually a grid of data, or a multidimensional array if you’re feeling fancy. You know a vector as 3 dimensional, x, y, and z – well a matrix would be 3x3 or 4x4 in our common use cases – multiple 3D or 4D vectors.

From the above image, we can see each of the columns represents each of the 3 directions of our matrix, with the final column being it’s position (translation) in space. So column 0 would be our matrices “right” (or x) axis, column 1 being our “up”, and column 2 being our “forward”.

Getting our forward vector from our object’s matrix

So in this example, where I’m getting column 2, I’m getting the “forward” direction. And because this is our world to object matrix (Inverse Model), this gives us our model’s world space “forward” direction relative to our object’s local space. I’m really going to need to visit linear transformation for this to truly make sense as to why this works, but in lieu of that, trust me for now.

Now that we have our two vectors for our Cross Product (camera direction, model forward), normalise them to ensure they are unit vectors, and then feed them to our Cross Product node. Remembering that output vectors from the Cross Product aren’t often unit vectors themselves, we then normalise this to get our rotation axis for our rotation step next.

The Rotate Above Axis node will then give you the rotated vector, in our case, our new vertex position to make the billboard face the camera. But because we also want if to point upwards, we need to perform another rotation, which means we need another pair of vectors to create our new rotation axis from.

Because we want the billboard facing up relative to our screen, we’ll use our view-space up vector (0,1,0) and our billboard’s up vector. We don’t want our original up vector as this is no longer helpful, we need our up after our first rotation, which means we need to rotate the model matrix itself.

Angle Axis Rotation

You can read up on Rodrigues’ rotation formula for an immediate explainer on how this works, or wait until I have the time to have a go at explaining it myself.

This is the reason for the custom function above, the source for which I added above. If you’re following along, copy that into a text file, put that into your Unity project, and name it something like helpers.hlsl.

This code is copied from Unity’s Documentation, but modified so it does not take the original input vector, and returns a float3x3 rotation matrix. This is so we can use it later to rotate our model matrix as well as our final position.

So the incoming pipe from the left of this image is our rotation matrix from our custom node. If you multiply this by the vertex position, you transform – specifically in this case rotate – the vertices by angle around the axis we passed into that custom node. This process completes what the built in “Rotate Around Axis” node does.

However the reason we want the matrix is demonstrated here (entering the image from the top). This rotate matrix also then rotates our object matrix so we can then find the “up” axis of our model after it’s been rotated to face the camera.

The final rotation is then similar to the first, except this time we can just use the “Rotate About Axis” node as supplied. Worth noting however that our Cross Product here uses our object’s new “up” vector, but rotates it to our view-space “up”, ensuring it’s aligned with the camera’s up. This “transform” node pictured above is taking up in view space, and transforming that so it’s relative to our object’s space.

These object-space rotations of our vertices can then be assigned back to our master node at the end of our graph, completing our billboard.

As we can see however from the above, our billboard now constantly faces the camera.

The Math Behind the Cross Product

If you’re not interested in looking under the hood here, you don’t need to. No one will think less of you if you skip this section.

From Brilliant

The above is the matrix used for solving the Cross Product for 3D vectors a and b. I’m not going to pretend that I can teach artists the matrix math and the linear transformation behind this today, so we’ll look at the second version of the Cross Product, shown in vector notation.

Linear Algebra & Matrix Transforms

This is quite scary-sounding, and the math behind it can be. So we’re going to breeze by it for now, but we’ll revisit later in another post with some practical examples. Thankfully, you can understand how to use matrices to perform transformations (and what that means), without needing to know how to math it out yourself.

In a slightly easier to digest version, we have (remember two terms next to each other is multiplication, so aybz is the y axis of the vector a, multiplied by the z axis of vector b);

a × b = (ay​bz​−az​by​, az​bx​−ax​bz​, ax​by​−ay​bx​)

It’s perhaps a little more familiar to 3D artists as it breaks down the equation for solving each of the x, y, and z components of the final vector we’re looking for – this is known as vector notation.

The way it works roughly is such; take the x component of the above Cross Product calculation ay​bz​−az​by We’re only looking at the y and z axis of our a and b vectors. The above calculation gives us the size of the x component (the determinant) of our final vector, or more intuitively how much our vector points in the x direction. We then do the same for the y and z components, except to calculate the y we don’t use the y component of our input vectors, and similarly for the z we don’t use components from our input.

The Determinant

There’s a chunk of fundamental Linear Algebra required to understand this, why it works, and why the y component calculation is slightly different. If you want a deeper demonstration then I can provide, try 3 Blue 1 Brown’s course on The Essence of Linear Algebra.

To Finish

Over this post we’ve touched on many different math and 3D art/rendering concepts, all related to, or made possible by the Cross Product. We’ve looked at examples of why this is a helpful mathematical function, and whilst day-to-day artists only need be appreciative of it’s presence, those pushing more into shaders, procedural art (Houdini or Substance Designer), or character rigging will find the background understanding of its existence and what it can do helpful.

If you have any examples applications of any of the concepts in this post and you want to share, I’d love to hear them, especially if you’re only now learning. Please reach out to me on socials and lets chat.