I'm going back to the example where we created a multicolored cube with Draw elements. I'm going to simplify this a bit, here.
In Create Vertices, I'm going to remove all the color data, so we're just passing in the vertices, and this code that deals with color and the point size stuff, too. This makes vertex count, vertices length divided by three, because there are only three elements per vertex. This also means we can get rid of this Stride value.
Let's clean up the shaders, too. We're not using point size or variant colors right now, so I'll get rid of some of that. We'll use variant colors later, though, so I'll leave that one in there. I'll set the GL frag color to a hard-coded value of a vec4 that creates a yellow color.
Back in the code, I'll also change the clear color to black, because this is going to look better on a black background. This gives us a single-colored cube.
Because all of the sides of the cube is the same color, a lot of the 3-D effect is lost, but in the real world, there are objects that are all the same color and yet still appear in 3-D. This is because the different angles of lighting hitting different angled surfaces shade them differently.
There are different types of lighting in WebGL. A directional light represents a simple light source coming from a particular direction, such as the sun. It's considered to be infinitely far away, and its rays are parallel.
A point light source is more like a light bulb. It has a specific location in space, and the angles of the rays will be different for each object, depending on the object's location in relation to the light.
Finally, there's ambient lighting. This is light that is reflected off all the surfaces in a space, and accounts for the fact that even things in shadows are not completely dark.
In this lesson, we'll just use directional lighting. Obviously, a surface will get more or less light depending on its angle to the light source. A surface directly in line with the light source will be fully lit. Surfaces at varying angles to the light source will be more or less lit, depending on their angle, and surfaces facing away from the light source will be in darkness.
We need a way to know which way surfaces are facing. We do this with something called normals. A normal is a 3-D vector that points directly away from a particular surface of an object, so for a tabletop or floor, the normal would be pointing straight up. For a wall, it would be pointing straight out from the wall, and a ceiling's normal would be pointing straight down.
What we do is provide a normal for each vertex. That normal would be a set of three xyz values that indicate the direction of that normal.
In Create Vertices, I'll add an array called Normals. This is going to hold four sets of xyz values for each surface. Let me explain it visually.
Here we have our cube. Imagine we're looking at this cube head on. The normals of the first four vertices will be pointing straight at us, so they'll have a positive z value, zero for x and y.
The next four vertices are the top surface, so its normals point straight up. Positive y, zero for x and z. We just go all the way around like that.
Back in our code, we add our first four normals as x, y, z points. Each will be zero, zero, one. That's our first two triangles. As we just saw, the next four normals have a positive y normal. The next set would be facing away from us, so they have a negative z, and the bottom face will have normals of a negative y.
The way the drawing indices is set up, the next two faces are on the left-hand side, so their normals will be negative x. The final two triangles would be on the right side with a positive x.
We need to pass these normals over to the vertex shader, so we'll need to create a buffer and all that. I'll copy over the coord code, change Buffer to Normal Buffer, bind the buffer, put the data into it, and get an attribute location for normal. We'll create that attribute later.
We'll use Vertex Attrib Pointer to pass the buffer to that attribute and enable it. Note that we do have to do this as a separate buffer. We can't interleave the data like we did with the colors. That's because we're reusing the vertices. Each time they're used, they may have a different normal.
For example, vertex number two, which is the top left front corner, is used to create a front-facing triangle, a top-facing triangle, and a left-facing triangle. Each time, it has a different normal, so we need to have the normals in a separate buffer.
Now we need a light source. As a directional light source, we just need a vector to use for the directional light and one to use as its color. These will both be vec3s. We usually use vec4s for color, but in the case of a light source, the alpha channel doesn't make much sense, so we can use a vec3.
We'll get the uniform location for a light color, which we'll also create shortly, and pass in values one, one, one for a white light. For direction, we'll get the uniform location for a light direction, and pass in some values for that.
At this stage, it's not super important what values go in there. Different numbers will just light the cube differently. Before we move on, let's clean up by unbinding that normal buffer.
We're passing in our normal data and our light source data. Time to jump over to the shaders. I'll create an attribute vec3 for the normals, a uniform vec3 for the light color, and one for the direction. Now comes the tricky stuff.
First, we want to normalize the normal, which ensures that it has an overall length of one. Actually, the way we've set things up here, all of the normals will have a length of one, but once we start making more complex shapes with surfaces that point in different directions, this will be more important.
We also want to normalize the light direction, then we want to get the dot product of the light direction and the current normal. We don't have time to dive in deeply to what a dot product is, but basically, it's the relationship between two vectors that represents how they are oriented to each other.
If two vectors are at 90 degrees to each other, the dot product will be zero. It will increase as that angle goes less than 90, and will go negative as the angle is greater than 90.
You can see that the dot product would be at its maximum when the normal and the light direction are the same angle. When the normal is 90 degrees from the light, no light will hit it, and the same if it's more than 90 degrees.
We don't really want a negative light factor, so we take the maximum of that dot product and zero. We wind up with max.ld.norm0. We can use that light color, the material color, and the dot product to calculate an overall color for this vertex. It'll be light color, times a hard-coded vec3 for yellow, times the dot product.
Finally, we can transform this vec3 into a vec4, and assign it to varying colors. Then, in the fragment shader, all we need to do is assign varying colors to GL frag color, and we're done. Let's see what we've created.
This may not be exactly what you expected. The cube is definitely shaded, but that shading isn't changing as the cube rotates.
That's because the transformation matrix we're applying applies to everything in the scene, including the light source. It's rotating around, the same as the cube. When we get into the advanced series, we'll look at setting up cameras and transforming individual objects, which will allow for more dynamic lighting.
But, you've gone through all the lessons in the series, and you should have enough knowledge to start looking into this on your own. Also, you might want to try changing the light color and direction, and the material color that we hard-coded in the shader to see how all these things interact to create realistic lighting effects.