The Dot Product – For 3D Artists
The Dot Product is a useful little function which is helpful for game artists to understand. In this post I’m going to look at at what the Dot Product is, how it’s used, and more importantly, what it’s useful for.
TL;DR
The Dot Product can be used when working out if two vectors are pointing in the same direction – so lighting or edge glows, or if a point in space is in front or behind something – Backface culling. These two fundamental vector operations are the basis of many shader effects, as well as many physics calculations.
The dot product (or scalar product as it’s also known) is shown mathematically with the dot ⋅
symbol, represents quite a simple mathematical operation, where a
and b
are 3D direction vectors;
(axbx + ayby + azbz)
= The cosine
of the angle of the two vectors (some number)
But rather then writing the above each time, it’s written mathematically as a⋅b
, in HLSL as dot(a, b)
, or perhaps more relevantly to artists, you’ll have a simple dot
node.
Broadly in 3D graphics, the dot product is useful for two main things;
- Checking the direction of one unit vector relative to another – are they facing the same way?
- Projecting a vector onto a unit vector – how far along the unit vector is the other? How far has something travelled in a specific direction?
Unit Vectors and Vector Normalisation
When discussing the dot product, it’s important to understand the role of vector normalisation and unit vectors. Normalisation is the process of converting a vector to a unit vector – a vector with a length of one, which describes only the direction information of the original vector, not the length.
(1, 0, 0) (0, 1, 0) (0, 0, 1)
are unit vectors for each of the 3 cardinal axis, X
, Y
, and Z
.
(1, 1, 1)
would not be a unit vector, as it has a length of 1.73
.
Normalisation ensures that the dot product only describes the (cosine of the) angle between the two vectors, not their length. This is particularly important in lighting calculations, direction checks, and other graphical computations where only the direction matters.
1. Direction Checking
The dot product of two unit vectors will result in a -1.0
to 1.0
scalar value.
If both vectors are;
- Pointing in the same direction, the result will be
1.0
- Perpendicular (90 degrees) to each other, the result will be
0.0
- Pointing in opposite directions, the result will be
-1.0
Diffuse Shading
This aspect of the dot product gives us one of the most basic shading methods in computer graphics, and one many 3D artists will be familiar with, Diffuse (Lambertian) shading.
Diffuse shading is the clamped (or saturated
) 0.0 - 1.0
value of the dot product between the light direction vector, and the surface normal which is being shaded. This value is then multiplied over the material’s colour to give a final value. In HLSL it’ll look something similar to;
color.rgb *= saturate(dot(lightDir, IN.normal));
Negative values are discarded here with the saturate()
function as we don’t want our colour value being scaled by negative lighting terms, 0
is as dark as it can be.
Worth noting that you’d very likely never implement the above in shader graph, or any node-based shader tool as they generally have a pre-written lighting model you’ll already use. Diffuse shading is a pretty old and primitive lighting model.
Half Lambert
Not the same as Diffuse Lighting, but very similar, and very well known, the “Half Lambert” shading model was used by Valve heavily over the years, most notably with Team Fortress 2.
The benefit of this lighting model, is we don’t have shading approaching total darkness as fast, and in such an unrealistic manner. This lighting model can appear cartoony, but it in some ways begins to mimic the idea of Sub Surface Scattering, albeit in a very naïve way.
The half lambert is very similar to Diffuse lighting, except instead of clamping the dot product value, we remap it into a 0.0 - 1.0
range instead. This means that the shading can still go to pure black, but only on faces pointing directly away from the light source.
color.rgb *= dot(lightDir, IN.normal) * 0.5 + 0.5;
Edge Glow (Fresnel Approximation)
The same principle applies as diffuse shading, except we’re using the view direction rather then the light direction, and we’ll apply this value to our final pixel colour differently.
float vDotN = dot(viewDir, In.normal);
float edge = 1-pow(saturate(vDotN), _glowPower);
float3 finalGlow = edge * _glowColor;
// Assuming a surface shader with an emission channel
surface.emission = finalGlow;
In the above example, the result of the dot product when multiplying the colour, would give you the “centre” and not the “edge”, so we One Minus 1-
the result of the Pow()
function which gives us the edge we want.
And it looks like this, note the power lets us expand or contract the glow.
Front or Behind Check
Another application of the dot product in this manner is to determine if something is in front, or behind a surface. To do this;
- Take an object’s position, subtract that from the vertex position and normalize it (to make it a unit vector).
- Dot this with the vertex normal
- If the resulting value is positive, the object is in front of the vertex’s face, if it’s negative it’s behind. If it’s 0 then it’s exactly on the surface.
One of the most common uses of this is to help with back-face culling, which might look something like;
float faceDot = dot(normalize(vertex.position - camera.position), vertex.normal);
bool isBackFacing = faceDot < 0;
The above example would appear in some form in a rendering pipeline, although bear in mind it might be heavily simplified. The Shadergraph version won’t actually work in most cases, as the Unity’s renderer has already culled back faces and thus the pixel shader won’t be invoked.
2. Distance Checking
This is marginally more complicated, but also pretty useful application. It’s main use is to check how far a point in space is, along a specified direction. The math of this is the same as above, except only one of your vectors would be unit vectors.
Distance From Plane
This could be used for finding how far away a vertex of a mesh is from an arbitrary plane in space is. In more practical terms, if you have a “scanning” effect on an object, and you want it’s shader to glow where the scanning progress, you’d use this.
uniform float3 _planeNomal;
uniform float3 _planePos;
uniform float _glowDistance;
...
float3 delta = _planePos - IN.pos;
float distance = abs(dot(_planeNormal, delta));
float glow = 1-saruate(distance / _glowDistance);
This is a little more of a complex example, but hopefully the animated Shadergraph example below should help out. We use our dot product as above, but then introduce a new function, abs()
or Absolute
.
Out dot product can be positive (vertex position is in front of the plane), or negative (behind the plane). In this example, we don’t mind if it’s ahead or behind, only how near it is. This is what the abs()
function tells us. We then divide the result of the abs()
function by _glowDistance
which turns this distance into a normalised range, a 0.0 - 1.0
value. The glow distance allows us to change how thick our glow line will be.
However, if our distance is greater then our _glowDistance
, then our value will be outside of a normalised range, so we then saturate()
it to keep it within our desired values. Once our distance is further then this, we don’t want to know anyway as this area of our mesh would not be glowing.
The final step is to one minus
the value, so the area where we want our glow is white, not black as it was, and thus ready to be masked as desired.
This dot product is also used in the point on plane calculation as seen in the fishbowl water, but I went into this in more specifics in the post, so I strongly suggest a read for a real world example.
To Wrap Up
These are a few of the more common applications of the dot product, there are more ways to use it, but this should give a good practical overview with some examples of how any why it is useful. With this tool under our belt, we can begin to move to more complex concepts to build more interesting and dynamic effects.
Next time in my new “Math for Artists” series we’ll take a look at the Cross Product
, the Dot Product's
older, hairier sibling who’s old enough to by alcohol and stay out past midnight. We’ll see how that relates to this, and what more complex effects we can achieve.