Skip to content

115. Lighting in 3D – Diffuse directional light

September 28, 2013

I need to warn you up front that this post is the most complex so far (we get to use the normals I explained earlier), and you may need to take it slowly. I’ll try to explain it clearly.

Remember that ambient light is light that has already reflected off other objects, and comes from all directions so it applies to all objects equally.

The two other types of light – diffuse and specular – come directly from a light source to an object, from a point in space or from a certain direction, and only the sides of objects that face that light will be affected.  So they are directional, ie the position of the light matters.

So what is the difference between diffuse and specular light? The diagrams below may help.

specular

asphalt

A mirror or very shiny metal reflects light almost perfectly, so it bounces off in the opposite direction, and you will see a bright spot of light if you’re standing in the path of that beam of light. This is specular reflection.

If the surface is a bit rough (maybe it doesn’t look rough to us, but it may, to a beam of very small light particles), then the light will scatter in all directions when it hits the surface. You won’t see such a bright spot of light (or maybe no bright spot at all), but the surface is generally lit by the light. This is diffuse light (diffuse meaning scattered).

Most objects will have both. They will partly reflect the actual light spot itself (specular) and be generally lit by scattered light (diffuse). And it is up to us to decide how much of each we want to have – OpenGL won’t do that for us.

Now I’m going to talk only about diffuse light. We’ll handle specular light in a future post.

Diffuse light

Ambient light comes from all directions, and scatters in all directions (partly because it comes from all directions!).

Diffuse light comes from one direction (eg the sun, or a desk light), and scatters in all directions off the surface.

So if both ambient and diffuse light scatter in all directions, why aren’t they the same ?

Diffuse light comes from a specific direction, so it hits the surface at an angle. And that angle makes a difference to the brightness.

normal

In the diagram above, imagine light coming in at all the angles shown, from different lights. Which one will make the surface look the brightest, ie have the most effect? The answer is the light coming from directly above, shining straight down on the surface (marked “normal” above), and the lights having the least effect will be those at the left and right, coming in at very shallow angles.

So the maximum reflection is from directly above, in the middle, and minimum light is on each side (and if the light is below the surface, there is no light at all). And it didn’t take mathematicians long to figure out that there was a simple (to them, anyway) way of calculating the amount of light reflected.

If you calculate the difference in angle between

  • the direction of the light, and
  • the direction the surface is facing (that’s the “normal”, remember),

then if it is 0, the light is at a maximum, and if it is 90 degrees or more different, the light is zero. See the diagram below (N = normal). In this diagram, the strength of the light is about half the maximum, because the angle is about 45 degrees.

diffuse_angle

And there is a simple formula that calculates this for you.

Strength of light = cos(angle between light and normal)

because cos(0) = 1 and cos(90) = 0.

And the mathematicians found an even easier (again, easier for them, perhaps) way to program this.

Strength of light = dot(normal direction, light direction) / (length of normal direction x length of light direction)

(Yes, I know it looks more complicated, but just wait…..)

I’m not going to prove that this is the same as the formula using cos, but I want you to understand how we actually calculate using this formula.

The normal direction and light direction are both direction vectors of the form (x, y, z)

The  “dot” in the formula is the dot product of the two vectors, which means multiplying the two x values together, then the two y values, then the same for z, and adding up the result, which is a number.

So if the normal direction is (-10, 5, 4) and the light direction is (-8, 6, 2), which are close to each other and should produce an answer close to 1, then we can calculate

  • dot product = (-10 * -8) + (5 * 6) + (4 * 2) = 118
  • the length of the normal direction is the square root of (sum of squares of x, y and z values) = sqrt(100  + 25  +16) = 11.87
  • the length of the light direction = sqrt(64 + 36 + 4) = 10.2
  • strength of light = 118 / (11.87 * 10.2) = 0.97, ie nearly full brightness, as we expected.

If, instead, the light direction was at more of an angle, at (-1, 5, 4), then

strength of light = 46 / (11.87 x 6.5) = 0.6

I need to explain one more thing – why we divide by the length of the direction vectors, and the shortcut we use for this.

Normalising vectors

Let’s take the normal direction in our example above, which was (-10, 5, 4)

This direction is an imaginary straight line running through (0, 0, 0) and (-10, 5, 4). That’s what a direction vector means.

But there are lots of other points on that line that we could have used instead, to get exactly the same direction, such as

  • (-20, -10, 8) ie doubling all the numbers
  • (-5, 2.5, 2) ie halving all the numbers

Both of these fall on the same line as (-10, 5, 4). So if you multiply (-10, 5, 4) by any number, you get the same direction.

But if we divide our direction vector by its length, we get a standardised value that will not only always be the same no matter which point we choose on the direction line, but the values will always fall between 0 and 1, as we want. (The mathematicians will probably say the reason we divide by the length is simply because that is mathematically correct, but I like to try to find a logical reason for it).

A direction vector that has been divided by its length is called “normalised”, and we use them a lot in lighting, whenever we are working with directions. Both Codea and shaders have a function that calculates this for us.

  • Codea:  v2 = v1:normalize()
  • Shader: v2 = normalize(v1);

Our formula for light strength can be written as

Strength of light = dot(normal direction/length of normal direction, light direction / length of light direction)

which is the same as

Strength of light = dot(normalised normal / normalised light direction)

and this is the formula we will use. That’s quite simple, isn’t it?

Putting it all together

Diffuse light reflection depends on the angle at which the light hits the surface. The more directly it hits, the stronger the reflection, the maximum being in the normal direction (the way the surface is facing).

Our formula for diffuse light reflection is therefore dot(normalised normal / normalised light direction).

As with ambient light, our surface can only reflect colours that are in the surface colour itself. So a blue surface can’t reflect yellow light, but a white surface can. So, as with ambient light, we need to multiply the colours together.

The surface may also not be fully reflective, so we may need a number (0-1) that tells us this.

Our light source will also have its own strength (brightness).

Our final formula for diffuse light is

Diffuse light = Light colour * Light strength * Surface colour * Surface reflectivity * Diffuse reflection (ie dot product above)

Light directions and normal vectors

I haven’t actually explained what I mean by light direction. It gives me a chance to recap and make sure you get the idea of direction vectors.

A “normal” is a vector like (1, 1, -2), which is just a point in space, ie x=1, y=1, z=-2.

What makes this a direction is when we draw a line from (0, 0, 0) through this point. And what makes it a “normal” direction is that this line goes right through our surface and heads off in the direction that surface is facing.

gl_normaltransform02

So suppose we are standing at (0, 0, 0), and looking at a cube in front of us, which is pointed straight at us, so we can only see the front face. Suppose the cube is centred at (0, 0, -100), remembering that in 3D, our z depth values are negative. What is the normal for the front face?

The front face is pointing straight at us, so the normal comes straight towards us, through (0, 0, 0) and keeps going into the distance behind us. The x and y values don’t change – only the z depth value changes, and it increases, ie becomes more positive.

So the normal is (0, 0, z) where z can be any positive number,as big or as small you like, because all. There are infinitely many normals you can choose, but as explained above, before we “dot” them with the light direction, we have to divide by their length. In this example, this means our normal will be (0, 0, 1).

If, instead, the cube was at an angle of 45 degrees to the left, ie turned half sideways, then the normal would be (-a, 0, a), because the normal line would be moving towards us (positive z) and to our left (negative x) equally. So any value of a will do, as long as it is the same for x and z, to keep the line moving at 45 degrees. And the normalised value would be (0.707, 0, 0.707).

Why 0.707? Because it needs to have a “length” of 1, and the length is the square root of the sum of squares of x, y and z. The square of 0.707 = 0.5, so the sum of squares = 0.5 + 0 + 0.5 = 1, and the square root is 1, the final answer.

The light direction

What about the light direction? The light – such as the sun – is sitting way out somewhere in space, and hitting every object in our scene at the same angle, so how do we set the value for the direction?

Start at (0, 0, 0) and ask yourself which direction the light is in. Then mentally draw a line in that direction, choose any point on that line, and then normalise it, so its length is 1.

Example – if our camera is at (0, 0, 0), suppose we want a light that comes from behind us at an angle, so it aims towards the right, say at 45 degrees. Now mentally draw the line of the light. The x value will increase because the light is angled to the right, and the z value will decrease because the z value is negative as we go forward into the distance. And if the angle is 45 degrees, then the x and z values change at the same rate. So the normalised normal (why did they choose two names the same???) would be (0.707, 0, -0.707).

Suppose that we want the light to come from above at an angle, like the sun. Then we need to change the y value as well. You could experiment, but maybe you’d choose something like this  (1, 2, -1) so the y value changes twice as fast as x or z – and then normalise it, of course!

So if you stand at (0, 0, 0) and point at the light, that is the direction you need.

I hope this makes sense. Every now and then I get myself confused – it takes a bit of time to get used to vectors as being directions!

Example

Let’s try an example of calculating normal vectors

  • Light colour is (255, 255, 255) ie (1, 1, 1) in the shader
  • Light strength is 0.8
  • Light direction is (15, 3, 2)
  • Surface color is (0, 255, 255) ie (0, 1, 1) in the shader
  • Surface reflectivity is 0.7
  • Surface normal is (8, 14, -9)

Then

  • Normalised light direction = (0.97, 0.19, 0.13) dividing by length = 15.4
  • Normalised normal = (0.43, 0.75, -0.49) dividing by length = 18.5
  • Diffuse reflection = dot (normalised normal, normalised light) = 0.50
  • Diffuse light = (1, 1, 1) * 0.8 * (0, 1, 1) * 0.7 * 0.50 = (0, 0.28, 0.28)

which is a (not very strong) blue-green colour.

Here is some code that calculates this example. It includes three vertices which are used to produce the normal vector above.

function setup()
lightColor=vec3(1,1,1) --color(255,255,255)
lightStrength=0.8
lightDirection=vec3(15,3,2)

surfaceColor=vec3(0,1,1) --color(0,255,255)
surfaceReflectivity=0.7

--let's define a triangle so we can calculate the normal
v1,v2,v3 = vec3(3,4,5),vec3(4,6,9),vec3(8,5,11)
surfaceNormal = (v2-v1):cross(v3-v1)
print('Normal',surfaceNormal)

--calculate diffuse reflection
normalisedLight=lightDirection:normalize()
print('Length of light',lightDirection:len())
print('normalised Light',normalisedLight)
normalisedNormal=surfaceNormal:normalize()
print('Length of normal',surfaceNormal:len())
print('normalised normal',normalisedNormal)
diffuseStrength=normalisedLight:dot(normalisedNormal)
print('Diffuse strength',math.max(0,diffuseStrength))

--final colour
print(lightColor, surfaceColor)
--Codea won't multiply vec3’s, although the shader will
--so we have to do it ourselves, I wrote a function that does it
diffuse = vec3Multiply(lightColor * lightStrength , surfaceColor * surfaceReflectivity) * diffuseStrength
print('Diffuse',diffuse)
--we'll draw the background in this colour
diffusecolor=color(diffuse.x*255,diffuse.y*255,diffuse.z*255)
end

function draw()
background(diffusecolor)
end

function vec3Multiply(a,b)
return vec3(a.x*b.x,a.y*b.y,a.z*b.z)
end

After all that, we aren’t done with diffuse light. I’ll explain some variations in a later post, such as when we have a light that is much closer, or maybe inside our 3D scene. But next, I’ll deal with specular light.

Footnote – I’ve only used three numbers in my direction vectors above and left out the fourth w value, because it doesn’t make a difference when you subtract vectors or calculate the dot of two vectors, since w is 0 for directional vectors. So I could have included a fourth value of 0 in the light and normal direction vectors and it would have made no difference.

Advertisement

From → 3D, Shaders

One Comment

Trackbacks & Pingbacks

  1. 202. Asteroids (again) in 3D | coolcodea

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: