# 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;

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.

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`

.

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.

### 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.

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*.

### Face Normals

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

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`

.

We then supply `AB`

and `AC`

to our Cross Product function, *normalise* the output, and we have our normal.

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.

### 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 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.

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.

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.

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.

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;

- Get the camera direction – the direction our billboard wants to face
- Get the billboards
*current*forward facing direction - Get the dot product of the target and current vectors.
- Take the
*inverse cosine*of this dot product, also known as the`arcos`

– this gives our rotation angle in*radians*. - Compute the Cross Product of our two original vectors and normalise it. We now also how our rotation axis.
- Create a
*rotation matrix*so we can rotate our billboard. - Rotate our billboard with this matrix.
- Get our rotated billboard’s new “up” vector
- Get an “up” vector in view space
- Now perform the same dot and Cross Product process (3-5) on our “view up” and “billboard up”
- Perform one final rotation on the rotated vertex position from before with the Rotate About Axis function.

### Shader Graph

#### 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.

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*.

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”.

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.

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.

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.

In a slightly easier to digest version, we have (remember two terms next to each other is multiplication, so `a`

is the _{y}b_{z}`y`

axis of the vector `a`

, *multiplied* by the `z`

axis of vector `b`

);

`a × b = (a`

_{y}b_{z}−a_{z}b_{y}, a_{z}b_{x}−a_{x}b_{z}, a_{x}b_{y}−a_{y}b_{x})

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 `a`

We’re only looking at the _{y}b_{z}−a_{z}b_{y}`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.

## 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.