Wednesday, May 13, 2009

OpenGL ES From the Ground Up, Part 5: Living in a Material World

Okay, in the last installment, we talked about lights, how to set them up and what attributes they have. We also talked about the three components of light, diffuse, ambient, and specular. If you're not completely clear on those, you might want to review, as those components are used heavily in defining materials.

component.jpg

Source image from Wikimedia Commons, modified as allowed by license.


As a starting point for this installment, we're going to use the Procedural Sphere project from this posting. We're switching to a sphere rather than continuing with the icosahedron that we've been using up to now because a sphere is the ideal shape for showing how the different components of lights and materials interact with each other.

What is Color


So, let's talk about what color is. This is probably a review for anybody who passed grade school art, but let's do it anyway. Why do things have color in the real world? What causes it?

Light that we can see is called the visible spectrum of light. We perceive different colors based on the wavelength of that light. At one end of the visible spectrum, we have the higher-frequency, lower wavelength purples and blues, and at the other end, we have the lower-frequency, higher wavelength oranges and reds:

visible_spectrum.png


Electromagnetic waves that fall outside of this range - both higher and lower - are not "visible light", though it is something ofan artificial distinction since the only difference is the wavelength, frequency, and the ability of the human eye to perceive it. However, that ability to perceive electromagnetic waves is everything, so we as far as OpenGL is concerned, the visible spectrum is all we care about.

Now, "white light" contains equal amounts of all the wavelengths. In other words, white light is white because it contains all (or at least most) of the frequencies of visible light. If you've ever experimented with a prism, you've seen this effect in action:

PrismAndLight.jpg


The prism refracts the white light so that light with different wavelengths is separated. This is also the process by which rainbows are created and ZOMG Ponies! No, not really on the ponies, but it's true about the rainbows.

Now, if you look at an object, and it appears blue, what is happening is that the object is absorbing most of the lower end of the visible range. The object is absorbing red, orange, yellow and green light. Depending on the shade of blue, it may also be absorbing violet and some blue.

But most of the blue wavelengths are being reflected back towards your eye. Because some of the visible light has been absorbed, the light being reflect back to your eye is no longer white because it no longer contains all the wavelengths of the visible spectrum.

Simple enough, right? Let's see how this applies to OpenGL.

OpenGL Materials


To define a color for a material in OpenGL ES, we define how that material reflects light, just like how things work in the real world. If a material is set up to reflect red light, then it will appear red when shown under regular, white light.

In OpenGL (at least when when using smooth shading and lights) materials don't have just a single color. We have the ability to specify exactly how the material reflects each of the three components of OpenGL lights (ambient, diffuse, and specular) separately. We additionally have the ability to specify the material's emissive property, which we'll talk about a little later.

Specifying a Material


To create a material in OpenGL, we make one or more call to either glMaterialf() or glMaterialfv(). Similar to the way we specified lights in the last installment, we often have to make multiple calls to these functions to fully define the material because each attribute or component has to be specified individually by making a separate call. Any component or attribute that is not specified defaults to zero or, in the case of colors, to black.

The first parameter passed to either glMaterialf() or glMaterialfv() is always a GL_ENUM which specifies whether the material affects the front, back, or both the front and back of polygons. There's actually no point in having this first argument in OpenGL ES other than for compatibility with OpenGL ES, because there's only one valid option: GL_FRONT_AND_BACK, which simply indicates that materials are used for any polygon that is drawn. If you remember from part 1, triangles have a front and a back that is determined by their winding (order the vertices are drawn). By default, only the front of a triangle is drawn in OpenGL, but it's possible to make OpenGL draw the backside, or even to draw only the backs, and regular OpenGL would let you specify different materials for front and back by passing either GL_FRONT, GL_BACK, or GL_FRONT_AND_BACK. But, OpenGL ES doesn't support any option other than GL_FRONT_AND_BACK, so just pass that always, 'kay?

The second parameter to glMaterialf() or glMaterialfv() is a GL_ENUM that identifies which component or attribute of the material you are setting. These are the same values we passed into glLightfv(), like GL_AMBIENT, along with some new ones you'll see in a moment.

The final value is either a GL_FLOAT, or a pointer to an array of GL_FLOATs that contains the actual value for the attribute or component.

The most imoprtant components of a material that need to be set are the ambient and diffuse components, because they define how a material reflects the bulk of the light being shined on it. The code project we're using today has defined white light, much like the light produced by the sun, or a light bulb in that it has all wavelengths or colors of light represented equally. If our light were not white, the appearance of our sphere would be different. Blue light reflecting off a red material, for example, would produce a purple shade. For simplicity, we're going to work only with white light and you can feel free to experiment with changing the colors of the lights on your own to see how lights and materials interact. For the most part, they interact in OpenGL ES the way they would in real life.

Here is what the project looks like when run, before adding any materials:

iPhone SimulatorScreenSnapz001.jpg


As you can see, we've got some ambient light and considerably more diffuse light.

Ambient and Diffuse


When talking about materials in OpenGL, we need to talk about the ambient and diffuse components at the same time because these two work together to define the perceived color of the object. Remember, diffuse light is the top part of the sphere in very first picture in this posting (the brighter yellow), and ambient is the darker yellow part on the underside. How the material reflects these two components will determine what color you perceive the object to be. Now, the picture above could have been achieved in more than one way. Likely, the yellow sphere reflects the ambient and diffuse properties in the same proportion, but the scene has less ambient light defined.

Probably 90% of the time or more, you will specify the ambient and diffuse parameters of a material exactly the same. By doing this, it becomes the amount of diffuse and ambient that determines the shading and appearance of the object. There is, in fact, a way to specify a material's diffuse and ambient components at the same time with a single call to glMaterialfv(). Here is how we might declare our material to be a shade of blue:

    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);

Just like with glColor4f(), setting a material dictates the way everything is drawn until another material is specified. If the code above is placed before our drawing code, and we now run the project, we're going to see that our sphere has become a single shade of blue. The underside is still darker because our ambient light is not as strong as our diffuse light:

iPhone SimulatorScreenSnapz002.jpg


There may be times when you want more control and you want to specify how the material reflects the diffuse and ambient light separately. For example, we could do the following to create a material that reflects the blue from the ambient light but the red from the diffuse light:

    GLfloat ambient[] = {0.0, 0.1, 0.9, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
GLfloat diffuse[] = {0.9, 0.0, 0.1, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);

If we run this, we get a very different appearance:

iPhone SimulatorScreenSnapz003.jpg


In this case, it looks like we're shining a colored light on the sphere, even though we're not. The reason that it looks that way is that we're reflecting the directional light differently than the ambient light.

In most cases, if you want to give the appearance of colored lights, then create colored lights and specify your material colors using GL_AMBIENT_AND_DIFFUSE. But, there are times when you might want to separate them out to create special effects or to fake an isolated, colored spot light without incurring the overhead of an additional light. Remember: Every light you add increases the calculations that have to be performed every second, so it's not a bad idea to cheat sometimes.

Specular and Shininess


You can also specify how your light reflects the specular component of the scene's lights separately from the diffuse and ambient components. This gives you the ability to control how bright the specular "hot spot" is. A separate parameter, called GL_SHININESS works together with the material's specular component to define how big the specular hot spot is. If you do specify a GL_SPECULAR value for your material, you should also define its shininess. The higher the shininess, the smaller the specular reflection, so the default value of 0.0 tends to completely overwhelm the diffuse component and generally looks bad.

Let's return to our blue sphere, and add a specular hot spot to it.

    GLfloat ambientAndDiffuse[] = {0.0, 0.1, 0.9, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, ambientAndDiffuse);
GLfloat specular[] = {0.3, 0.3, 0.3, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 25.0);

We've used a dim white for our specular value. This may seem low, but the way specular is calculated, a little seems to go a long way. The quality of the specular component of the lights is multiplied by the material's specular component, and the resulting light is concentrated in the area dictated by the material's shininess. Here is what the sphere looks like with the code above:

iPhone SimulatorScreenSnapz004.jpg


We now have a small circular area on the sphere that's reflecting more light. We can make this spot brighter by increasing either the specular component of our light or lights, or by increasing the shininesst of our material. We can also change the size of the highlight by tweaking the shininess. The shinier the material, the more concentrated the specular highlight. If we change the shininess from 25.0 to 50.0, for example, we get a much tighter hot spot, which gives the appearance of a more highly polished surface.

iPhone SimulatorScreenSnapz005.jpg


Here's a little warning, though. Part of the reason why I switched to spheres for this posting, and the reason why I used a relatively high vertex count for this sphere, is because specular highlights on low-poly objects don't look very good. Watch what happens if I reduce the number of vertices in our sphere to a more game-friendly number:

iPhone SimulatorScreenSnapz006.jpg


The specular highlights tend to make the triangle edges stand out, and often specular highlights just don't look good for low-poly objects like those you'd use in a game. In regular OpenGL, there is something called a shader that can be used to achieve good results with low-poly models, but the version of OpenGL ES on the iPhone does not have shaders. The only way you can generally make low-poly objects look good in games is by forgetting about the specular component altogether and using texture mapping, something we'll look at starting in the next installment.

Emission


There's one last important attribute of materials that we need to look at, and it's called the emission (or sometimes the emissive) component. By setting the emission component, we make the material look like it is emitting light of the color we specify. Now, it's not really emitting light. Nearby objects, for example, will not be affected by another object's emissive light. If you want to make an object like a light bulb that actually shines on other objects, you need to combine the emission component with an actual light at the same location as the object because (and this is going to sound redundantly redundant) but in OpenGL ES, only lights actually emit light. But, the emission component can be used to give objects a nice glow.

We could, for example, add a nice green glowiness to our blue sphere, by adding this code:

    GLfloat emission[] = {0.0, 0.4, 0.0, 1.0};
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emission);

This would give us a result that looks like this:

iPhone SimulatorScreenSnapz007.jpg


The emission component affects the entire material, so the value specified for GL_EMISSION is multiplied by any type of light that falls on a specific spot on the object. Notice that even the specular highlight in the picture above has taken on a bit of a blue-green cast rather than being pure white. It's subtle in the brighter spots, and most noticeable in the bottom area where only ambient light is being reflected, but it affects everything.

That's All She Wrote


Well, I though long and hard about some kind of Madonna-reference or at least an 80's music reference for the conclusion here to tie it together with the posting's title, but I was completely unable to come up with anything even remotely witty, so I'm going to have to just leave it here.

In the next installment, we will look at texture mapping, which can be used both for two-dimensional sprites and also for giving three-dimensional objects more complex surfaces than materials allow for.

No comments:

Post a Comment