113. Lighting in 3D – Normals
The word “normals” comes up frequently in discussions on graphic lighting. What are they, and what are they used for?
Let’s go back to basics. If you have a light, and you have a surface (ball, cube, whatever), and you want to know how much light is going to reflect off that surface, what is the most important thing you need to know?
Suppose we have a cube, and suppose we look at just one of its six sides. How much light will reflect off that side?
Obviously, if the side is facing away from the light, it won’t reflect anything, and it will reflect the most when it is directly facing the light. So the most important information we need is which way is the side facing? And we need that information in a form that OpenGL can use easily.
Suppose you could lie on your back on the side of the cube, and look straight up, then that is the direction the side is facing. It is at an angle of 90 degrees to the surface (or if you want to start with the long names, perpendicular or orthogonal).
And that is all a surface normal is – the direction that a surface is facing.
Importantly, we are talking about a surface, not a point (a point doesn’t face in any direction) nor a line (a line doesn’t either), but at least three points. And guess what – all our meshes are made up of three pointed triangles.
So when we talk about normals in Codea/openGL, we are usually talking about which way the triangles in our meshes are facing.
Each triangle in our meshes has one normal that applies to all points in the triangle. You can think of a normal as a straight line sticking up from the surface, like this (the picture calls the surface a “plane”, which is just a flat surface).
Now as you will see, OpenGL can use this “normal” to calculate all the lighting we need. But how do we describe the normal in a way that OpenGL can understand? After all, it is a direction. It doesn’t even have a particular starting point on the surface or an end point out in space somewhere!
Points vsĀ directional vectors
If you’ve done any 3D modelling, you’ll know that each point in 3D space has an (x,y,z) position. So when you see (22, 4, -4) you’ll naturally assume it is the position of a point.
However, (x, y, z) can also be used to describe a direction. If you draw a line from (0, 0, 0) through (22, 4, -4), it gives you a direction. And this is how we describe our normal to OpenGL – as a point that a line starting at (0, 0, 0) would pass through, if it were going in the direction of the normal. So a normal looks just like a point, with an x, y and z value, eg (-4, 3, 2).
But how does OpenGL know whether (22, 4, -4) is a point or a direction? The answer is that OpenGL adds a fourth number, which is (for our lighting purposes anyway) always going to be either 1 for a point, or 0 for a directional vector.
So
- (22, 4, -4, 1) is a point in space
- (22, 4, -4, 0) is a direction [the line formed by joining (0,0,0) with (22,4,-4)]
You need to get used to seeing four numbers, and remember that 1 means a point, and 0 means a direction. If you mix them up, your code may not work.
This fourth number has the name w, so we talk about (x, y, z, w).
Why is w chosen to be 0 or 1? Because behind the scenes, everything is done with 4×4 matrices, and this choice makes the maths work nicely.
You’ll see that below, I often use just (x, y, z) and not (x, y, z, w). The four numbers only matter when you start transforming between one “space” and another, and multiplying by matrices, so (as long as you are careful) you don’t need four numbers all the time.
Calculating the normal
Now, for any given triangle in our mesh, we need a formula that will give us the “normal” direction vector. It needs to be perpendicular (at 90 degrees) to every corner of our triangle. There is a formula, and I’m not going to explain why it works, because we aren’t mathematicians.
Suppose our triangle has points A, B and C in that order (each point has an x,y,z value).
Then the normal is calculated like this: (B – A):cross(C – A)
Let’s look at this gibberish more closely.
We subtract A from B (which means subtracting the x values from each other, ie the y values from each other, and the same for z). So if A is (5, 3, 2) and B is (4, 6, 3), then B – A = (-1, 3, 1).
Now when you subtract points from each other, you get a direction (because it gives you the difference between the points, ie which direction to go in, to get from one to the other). So B – A gives us the direction from A to B.
Then we do the same for C – A, which gives us another direction.
You then need the cross product of our two directions, which means multiplying all the x, y and z values together, but not x times x, y times y and z times z. It is more complex than that, which is why it is good that Codea provides a cross function to do it for you.
The result of the formula above is a direction vector (x, y, z) , which is your normal. When you give it to OpenGL to play with, you need to add a fourth number, 0, to show it is a direction, ie (x, y, z, 0).
By the way, it does matter which vertices you choose to be A, B and C, because the direction can be point out of the surface, or back into the object. Obviously we want it to point out of the surface, and if you use the formula above (ie for any three vectors, subtract the second from the first, and the third from the first, and take the cross product), that’s what you’ll get.
What do you do with normals?
Codea does absolutely nothing with normals. We will have to do all the work ourselves, in the shader.
Actually, I’m not quite right. Codea does let you set a table of normals for a mesh, ie
mesh.normals = MyTableOfNormals
with one normal for each vertex in the mesh, in the same order as mesh.vertices. We’ll see how to use them later.
Just one thing. We calculated one normal for each triangle, but each triangle has three vertices. We give each of them the same normal, because it applies to all of them.
So the way you calculate normals is to loop through your vertices three at a time, calculating the normal for each triangle and then setting it for each of the three vertices, like this
local normal = {} for i=1, #vert,3 do local n = (vert[i+1] - vert[i]):cross(vert[i+2] - vert[i]) normal[i] = n normal[i+1] = n normal[i+2] = n end
I hope this explanation hasn’t been too confusing.
Just remember that
- normals tell OpenGL which way each mesh triangle is facing, and
- normals are directions, not points.
Trackbacks & Pingbacks