Exploring the Intricacies of Dot Product in Shader Mathematics
Written on
The dot product stands out as one of the most crucial and adaptable functions employed in shader programming. You may already be familiar with it, but if not, this article aims to enlighten you on its significance.
I can sense your curiosity about how I will substantiate this claim. I plan to illustrate the various applications of this fascinating mathematical concept within materials through concrete examples. My intent is to uncover a range of uses, from the well-known to the more obscure, and perhaps introduce you to a few tricks that may be new to you.
While I will primarily utilize Unreal Engine and its material editor for these examples, the principles can be applied across different engines. At the conclusion, I will provide links to the project files containing the examples.
Before diving into the applications, let's cover the fundamental concept. The dot product is mathematically represented as: A · B. It calculates the sum of the products of corresponding elements from two vectors: A[0] × B[0] + A[1] × B[1] + ... + A[n] × B[n]. The result is always a single scalar value, and there is no restriction on the dimensionality of the vectors involved—imagine the potential!
Now, let’s delve into the main applications:
- Evaluating vector similarity
- Implementing Fresnel effects
- Measuring vector length (e.g., distance between two points)
- Simulating fake lighting
- Creating economical round masks for particle effects
- Summing vector elements
- Converting color images to grayscale (desaturation)
- Selecting specific channels from textures
- Detecting vector sides using perpendicular dot product
- Rotating normal maps
- Rotating world positions
Evaluating Vector Similarity
When we compute the dot product (DP) of normalized vectors, the outcome will always fall within the range of [-1, 1]. A result of -1 indicates the vectors are pointed in exactly opposite directions, 0 signifies they are perpendicular, and 1 indicates they are identical. For instance, if two vectors form a 45° angle, the DP will be approximately 0.7071. This value corresponds to the cosine of 45°, which is (?2)/2 ? 0.7071.
For normalized vectors, the relationship can be expressed as: A · B = Cos(?), where ? is the angle between the vectors.
While this knowledge may not frequently surface in materials, grasping its implications expands creative possibilities. For instance...
Fresnel Effects
This is likely familiar to most.
A vertex or pixel normal vector relative to the camera generates a stunning gradient often used in materials to enhance brightness at the edges (or center) of objects, such as clothing or highlighted game characters.
Measuring Vector Length
To determine the distance between two points or to normalize a vector, one calculates its length. This is essentially the square root of A · A. This provides opportunities for optimization, especially when certain effects rely on distance, such as from the camera to a point in the world. Instead of calculating the square root—which can be computationally expensive—you can substitute the distance (or length) with the DP, yielding satisfactory results in some scenarios.
Two interesting facts: 1. In HLSL code for material nodes in the UE editor, the distance is expressed as length(A - B), where the subtraction is also performed in the upper part. 2. Epic provides a Length function in UE Materials that calculates the distance from zero to an input vector.
Simulating Fake Lighting
What happens when we compute: (world position of a pixel - position in the world)? For example, a light bulb.
To simulate artificial light in a material, one typically calculates the distance and adjusts it to achieve a natural light falloff. However, a more efficient approach exists! The formula for light attenuation is:
Light Strength / (distance(world position of a pixel, light bulb position)²).
As noted before, distance is the square root of A · A. Recognizing this allows us to simplify:
Light Strength / (dot((world position of a pixel - light bulb position), (world position of a pixel - light bulb position))).
This technique delivers economical fake lighting, though it unfortunately does not permit shadow casting on other objects.
Economical Round Masks for Particle Effects
Now, let's explore a more exciting application. If you've crafted materials for particle effects, you've likely created basic FX materials, such as a sprite cut into a circle. Typically, you might use pre-existing functions or compute it as follows:
saturate(1 - (distance(UV, 0.5) × 2)).
Often, you would raise this to a power before inverting it to achieve a non-linear final gradient, as depicted in the screenshot below.
In the previous example, I demonstrated that you can utilize a dot product to yield a result with a spherical gradient:
saturate(1 - (4 × dot(UV - 0.5, UV - 0.5))).
In this case, we replace distance and power with subtraction and DP, leading to performance improvements.
Summing Vector Elements
You can leverage DP to sum vector elements without masking and adding:
Vector.x + Vector.y + Vector.z,
Instead, simply compute the dot product with a unit vector:
dot(Vector, 1).
While I'm uncertain about the efficiency of this method, it appears more elegant in shader graphs.
Desaturation
In Unreal Engine, the desaturation node combines lerp and dot product in its functionality. Lerp determines the degree of desaturation while DP executes the desaturation (obviously). The DP is computed between the image pixels and the pre-defined (but adjustable) weights for each color (0.3, 0.59, 0.11). These values correspond to human color perception and were derived by experts in the field. If you're interested, search for "perceived luminance" online. The formula is:
lerp(Vector, dot(Vector, (0.3, 0.59, 0.11)), Alpha).
Selecting a Channel from a Texture
You might be surprised to learn that the Channel Mask node in UE materials operates using a dot product, specifically (texture · Vector), where the vector could be (0,1,0,0) to isolate the green channel from a given texture.
Detecting the Side of a Vector — Perpendicular Dot Product
This application is particularly intriguing, though it is limited to 2D scenarios. It involves calculating the dot product using a perpendicular vector relative to one of the original vectors.
For instance, if you have two vectors: Vector A (Ax, Ay) — the direction of the camera, and Vector B (Bx, By) — pointing from the camera to a specific point, say a post. To form a perpendicular vector to B, swap its components and alter the sign of one component, depending on the desired rotation direction. Thus, we create a perpendicular vector B (Bx, -By). Substituting this into the 2D dot product yields A · B.
In the video below, observe that looking to the left of the red arrow produces a positive result, while looking to the right yields a negative one. This technique can be applied, for example, to conceal objects.
Have you ever played Antichamber? There was a gallery room where walking around boxes displayed different content through each wall. Using the perpendicular dot product, we can achieve this effect; however, in the video below, I implemented it in a slightly different manner. I created a gate that reveals an object on the opposite side while obscuring it when viewed from the side.
For the golden monkey object, I calculated the perpendicular DP using the coordinates of the gate posts, allowing me to determine if the camera is looking between them.
In the example, I designed a blueprint that makes the object appear or disappear based on whether the camera passes through the gate before approaching it.
Rotating a Normal Map
Have you ever rotated a texture in a material? It’s straightforward. But how about rotating a normal map? It can be done easily if you know how to adjust the vectors on a rotated texture to ensure the correct display of directions. Failing to do this results in improper shadow and light reflections.
To fix this, convert the rotation angle into a direction vector and a perpendicular vector (as mentioned earlier, creating a perpendicular vector is straightforward). Then, compare both vectors using dot products with the rotated texture and combine the results into a new vector.
Notice that this mathematical approach closely resembles the one provided by Epic in the Custom Rotator, allowing for a more readable graph. The primary difference lies in the arrangement of cos and sin in the vectors.
Rotating World Position
The final concept I’ll explore is akin to the previous one but applied in 3D space. Occasionally, for certain effects, you may want to perform operations like highlighting without aligning to the world angle. This can be achieved by calculating the dot product between the World Position and any vector.
An extension of this idea is the ability to rotate the entire world position. This requires multiplying the rotation matrix by the World Position matrix. You guessed it—this involves using the dot product! The inverse transform matrix function available in UE materials allows us to provide new directions in the form of three vectors. Each of these vectors undergoes a DP with the world position, and the results are combined into a new vector. This technique can be employed, for example, to rotate an object at any angle across all three axes.
That’s All, Folks!
BOOM!! Mic drop. The dot product is undeniably cool. ;) I hope you’ve learned something valuable.
As promised, here are the links to the project files (UE 5.3). Check my Google Drive and Dropbox. If you encounter any issues downloading, feel free to reach out to me at [email protected].