Beauty of the Dot Product



How does this boring, flat circle become a fully three-dimensional sphere? Well, there isn't a short answer to that question, but a lot of it has to do with mathematics. In particular, the magical thing that gives you the illusion of depth is the dot product.

What is the dot product, you ask? Well, first, let's get a little bit of background knowledge.

You can define any point in 3D space in terms of its X, Y, and Z coordinates. For example, let's say we have a point P, and we want it to exist in the center of the scene, or the 'origin'. We could look at its position as the vector:

0.0, 0.0, 0.0

We can use this general X,Y,Z format to do more than define positions in space. We can also use this to represent directions, which are extremely important in computer graphics. A normal in computer graphics is simply a vector that tells us which direction a surface is facing. Let's say we have an infinite plane A at our point P we established earlier. If we want it to face upright, and our up axis is Y, we can say that this plane A has a normal N at:

0.0, 1.0, 0.0

Pretty nifty, right? Let's visualize this.


If that makes sense, then we can dive into why the dot product is basically magic.
First off, let's clarify what it even is, mathematically.
Given two 3D vectors a and b, the dot product between a and b is as follows:

dot(a,b) = (a.x * b.x) + (a.y * b.y) + (a.z * b.z)

Looks pretty harmless, right? It's a fairly simple calculation. The beauty is in what this calculation is telling you about the relationship between the two vectors. Before we dive any deeper, let's quickly run through normalizing a vector. This will be imperative to discover the power of the dot product.
Let's say we have a 3D vector a, defined as follows:

1.0, 1.0, 1.0

Normalizing this vector means keeping that vector pointing in the same direction, but changing its unit length to equal 1. For the purposes of this article, that's more than sufficient for us to keep going!
How do we normalize the vector, anyway? Well, first we need the magnitude of the vector, defined as follows, using the same vector a as an example:

magnitude(a) = sqrt ( (a.x ^ 2) + (a.y ^ 2) + (a.z ^ 2) )

Essentially, we're taking the length of the vector here! The next step to finally normalize our vector is trivial!
normalize(a) = a / magnitude(a)

By dividing our vector a by the scalar we get from calculating the magnitude, we get a normalized vector with unit length 1. You can check this for yourself by taking the magnitude of the resulting vector!
With all of this knowledge, we can take a closer look at why the dot product is so magnificent. I think it's fun to plug some arbitrary numbers in and see what you get - maybe you can decipher what it's telling you on your own! Make sure you normalize your vectors before computing the dot product.
Let's try taking the dot product of our normalized vector a with an identical vector, b. After normalizing, we get that:

a = 1.0 / sqrt(3), 1.0 / sqrt(3), 1.0 / sqrt(3)
b = 1.0 / sqrt(3), 1.0 / sqrt(3), 1.0 / sqrt(3)

Ok, now let's try taking the dot product of these two!
What you should end up getting is the scalar number 3/3, or 1. What did the dot product just tell us? That these two vectors are equivalent. Let's try another one.

a = 1.0 / sqrt(3), 1.0 / sqrt(3), 1.0 / sqrt(3)
b = -1.0 / sqrt(3), -1.0 / sqrt(3), -1.0 / sqrt(3)

This time I've multiplied our original vector b by the scalar -1, and then normalized the vector. This results in the exact opposite vector from a. Take a wild guess as to what the dot product will yield to us in this case, -1!
The dot product allows us to determine the likeness of two vectors! On it's own, it might not seem so useful, but it makes up one of the core computations in the world of shading. Let's take a look at my sphere example in particular.
A 3D sphere is essentially made up of a ton of 3D vectors representing vertices. Each vertex has a normal, what we discussed earlier, determining what direction the vertex is facing. Let's call our sphere S, and the vertices a set V, and the normals an adjacent set N.
If we don't have anything else in our scene, we'll simply get the sphere with a flat color. Each pixel will compute what you set the base color to be (an arbitrary r,g,b vector), and only that. But we want that beautiful shading that makes it look like it's a real sphere! We need to add a light. Just like in real life, objects in 3D space respond to light in the scene, through the power of mathematics.
We'll define our light L with a 3D vector LP which determines the position of the light. That's useful to us, because we want to know the direction L is facing in relation to our vertices V. We can determine this direction with simple vector subtraction:

for every vertex v in V:
LD (light direction) = LP - v

Great! Now we have the direction of the light relative to each vertex, and we already had the normals of each vertex in our set N. Now, for every vertex v in V, we can determine how much the light should be affecting the shade of color at v. To do this, we first normalize both vectors, and then... Well, you may be able to guess.

diffuse = dot(LD, n)

Where n is a normal in N. Boom, using this simple calculation, we're able to achieve what is called diffuse shading, and get the sphere to look like the end of the gif up top! Here's a diagram if you're confused.


Apologies for my poor drawing skills. Anyway, if we look at the vertex v1 and its accompanying normalized normal n1 (the hat just means it's normalized), and compare it to the light direction LD1, it's almost the exact same vector. Thus, taking the dot product at that vertex, we'd probably get a scalar close to 1. Then, when calculating the color of every pixel with this formula:

diffuse = dot(LD1, n1)
pixelColor = objectColor * (lightColor * diffuse)

We'd essentially be adding the lightColor at its full value! As R,G,B values get closer to 1, they get closer to white, or the brightest something can be. Now, let's look at the other vertex I highlighted.
Vertex v2 with normal n2 and light direction LD2 garners a slightly different calculation.

diffuse = dot(LD2, n2)
pixelColor = objectColor * (lightColor * diffuse)

Because the two vectors are so different, the diffuse scalar will be really small, close to 0. Thus, the lightColor will be attenuated in such a way that the objectColor will get closer to 0, and thus the resulting color will be darker; close to black.
What happens to the vertices in between these two extremes? Since each vertex has a slightly different normal and light direction, each will have a slightly different diffuse calculation. Overall, this will create a sort of gradient effect that you see in the gif at the top - successfully simulating the illusion of 3-dimensionality and lighting. Pretty cool, right?
So, overall, the dot product can do a whole lot of things other than diffuse shading. But, it's such a simple calculation, and produces such visually pleasing results, that it's hard not to be amazed by just this example. I hope this was interesting to you!

This article is ported from my old blog and was originally posted 10/23/2019.

Comments

Popular posts from this blog

Let's Implement a Gaussian Blur Algorithm in Python

Using Google Sheets Python API to Manage Scriptable Data in Unity

Demystifying the Cross Product