|<< Previous Chapter: Geometry||Tutorial Overview||Next Chapter: Windows and Viewports >>?|
This chapter is dedicated to another very important aspect of computer graphics: light sources. Well, most scenes have at least one, but often more, light sources to simulate a realistic environment. OpenSG features the common OpenGL light source types: spot, directional and point lights. The actual usage is easy and should be similar to OpenGL. At the end of this chapter there will be a tutorial in which we will add some light sources to the water mesh from the last chapter.
The most common type of light source is the point light. It is emitting light in all directions equally. You have to define a position and some attenuation parameters which will affect the intensity of light depending on the distance to the lit object. Additionally you have to provide information about the light's color, of course. This code fragment shows how a point light could be created
PointLightRecPtr pLight = PointLight::create(); //Attenuation parameters pLight->setConstantAttenuation (1); pLight->setLinearAttenuation (0); pLight->setQuadraticAttenuation(2); //color information pLight->setDiffuse (Color4f(1, 1, 1, 1)); pLight->setAmbient (Color4f(0.2, 0.2, 0.2, 1)); pLight->setSpecular(Color4f(1, 1, 1, 1)); //set the beacon pLight->setBeacon(someNode);
Directional light sources are faster to compute than point light sources, but often they look less realistic. Light is not emitted from a specific point in the world but from infinity so light is coming in parallel rays. In real world light from sun is nearly parallel too, but do not think that parallel light is looking any better. A directional light source only needs a direction, where the light is coming from and the color information thus the code is even shorter.
DirectionalLightRecPtr dLight = DirectionalLight::create(); //color information dLight->setDiffuse(Color4f(1,1,1,1)); dLight->setAmbient(Color4f(0.2,0.2,0.2,1)); dLight->setSpecular(Color4f(1,1,1,1)); //set the beacon dLight->setBeacon(someNode);
The spot light is the most expensive light source to compute. It is similar to a point light in that it has colors, attenuation and a position, but spot lights have an additional direction and angle which will define the area that is lit. Another value is needed to specify how fast intensity drops off as light reaches the border of the lit area. I suggest that you play around with the parameters to see what happens.
SpotLightRecPtr sLight = SpotLight::create(); //set how fast light intesity decreases at the border sLight->setSpotExponent(2); //set the opening angle sLight->setSpotCutOff(osgDegree2Rad(30)); //color information sLight->setDiffuse (Color4f(1, 1, 1, 1)); sLight->setAmbient (Color4f(0.2, 0.2, 0.2, 1)); sLight->setSpecular(Color4f(1, 1, 1, 1)); //set the beacon sLight->setBeacon(someNode);
As mentioned above, spot lights are pretty expensive and should be used with caution!
Light Position, Direction and Influence Area
If you read the above sections carefully you have noticed that I talked about positions and directions, but never set them on the lights. Instead I set something I haven't talked about: the beacon. The problem with light source nodes in scenegraphs is that their position in the graph can be used to define two things: their position and orientation (via the transformations defined by their ancestors), and their area of influence (via their descendants). It generally makes sense to control both independently, for example to have a light source attached to a moving object like a car, but influence the local surroundings like a street, and not the whole scene, like the mountains in the distance. But a single node can only be in one place in a graph at any time. Therefore in OpenSG a second node is employed, the beacon. The location of the light source in the scene graph defines its area of influence: only the descendants of the light source are lit. This generally means that light sources are located at the top of the graph, to influence a large part of it. The position and orientation of the light source is defined by the Node referenced as the beacon. Light sources are located at the origin of the beacon's coordinate system, the direction is along the positive z-axis. The following picture shows a simple graph with a light source.
This is a typical setup if you want to move the light source independent of the rest of the scene. But the beacon is not limited in any way, it can be an arbitrary Node in the graph, including nodes below the light source or even the light source itself. You can also leave the beacon unset (i.e. leave it as a NULL pointer), the light is then positioned in the coordinate system that is active at the point in the scene graph where the light node is.
The main motivation for specifying the position/orientation of the lights via a beacon Node and not via for example a Matrix is consistency and automatisms. This approach hides the difference between light sources and other Nodes for purposes of manipulation and animation. Light sources are manipulated exactly like any other node, by changing transformations on a Transform or ComponentTransform node. This also allows passive changes in the sense that a light source can be literally attached to another object and move whenever it moves. All just by setting the beacon.
For attaching a light to another object the current setup is still somewhat limited. Given that the position and orientation of the light source is defined by constant values in the coordinate system of the beacon, you'd have to add temporary transformations to move the light source inside this coordinate system, for example to move the two headlights of a car to the correct position within the car. To simplify these operations, the light sources actually do have position and direction Fields, as applicable to the given type of light source, but these are relative to the beacons coordinate system. If in doubt, just use the light source's Node as a beacon (or leave the beacon field empty, which has exactly the same effect).
Just to mention it: The camera uses the same beacon mechanism to define the position and orientation of the viewer. This unifies moving and animation of all relevant objects in the scenegraph to changing Transformation nodes.
Another comment: the SimpleSceneManager by default provides a directional light set up as a headlight. That is the reason why we didn't have a black sceen in the previous tutorials, although we had not defined a single light source.
Where's the switch?
So far there is no way to turn lights on and off, expect for removing them from the graph. That would be too inconvenient, therefore lights also have an on/off switch. It is a simple boolean SField (remember what these were? See Single and Multifields if you don't) called "on".
LightRecPtr l = roomlight; l->setOn(false); // nighty night!
To avoid confusion, I want to say some words about attenuation parameters. All light sources, except for the directional light, have attenuation parameters which specify how fast light intensity will decrease as the distance to an object increases. There are three values constant- , linear- and quadric attenuation. The default value for constant attenuation is one and zero for the others (meaning that by default there is no decrease in brightness). The following image shows results for some different attenuation values.
The image to the very left has only a very small factor for constant attenuation (0.1). Attenuation of the intensity is not really visible as the plane seems equally lit. The image in the middle has some reasonable values where as the right image has a quadric attenuation value of one resulting in a much too dark image.
As you can see, it is advisable to use these parameters with care for otherwise the reason for only getting a black window migth be too little light at the position where the actual objects are.
Tutorial - Light it up
As promised we are going to extend the last tutorial with some light sources, which will hopefully enhance the visual appearance of our water simulation ;-). Please take Examples/Tutorial/09geometry_water3.cpp as a starting point. The first thing we need to do is to shut off the default head light, which the simple scene manger creates automatically. This is very easy indeed, just add the following line in the main method somewhere after we created the simple scene manager and before we enter the glutMainLoop!
If you run the application you will notice that nothing has really changed. That is because of the material we defined previously. If you look what we have done before: We simply created a SimpleMaterial and assigned it to the geometry without providing any additional information. The material we created in that way is completely unaffected by light sources. It is equally lit from every direction. So first thing we do is to assign a correct material:
in the createScenegraph() function locate the line that says
SimpleMaterialRecPtr mat = SimpleMaterial::create();
and add the following code right below that line
When running it we see, as expected, nothing, because our only geometry is rendered black; there is no light source left to do the job! If you turn the headlight back on, you will see that the water mesh is blue and not red as the material's diffuse color we just set. Why that? If you look a few lines upwards you might remember that we provided every single vertex with a specific color - blue. Remove the block where the GeoColor3f property is created as well as the line that says
during the creation of the geometry core. That will remove the "hand-made" color values and will no longer overwrite the attributes of our red material. If you compile and execute the application now you will see red water. Notice, that the color will change if you move the camera. Remember that we already provided normals which where all parallel to the y axis. If you want to, you can change the material's color from red back to blue as this was only to demonstrate the conflict between assigned vertex colors and the color of a material. I do not want to persuade anybody to use red water... ;-)
If you saw nothing but a black screen, you forgot to turn the headlight back on. Anyway, turn the headlight off, as we want to add our own light sources now. We start with the most common a point light.
At first add two new headers
#include <OpenSG/OSGPointLight.h> #include <OpenSG/OSGSpotLight.h>
Locate the following block of code right at the very end of the createScenegraph() function
NodeRecPtr root = Node::create(); root->setCore(geo);
and replace the entire block with the following code
PointLightRecPtr pLight = PointLight::create(); NodeRecPtr root = Node::create(); NodeRecPtr water = Node::create(); NodeRecPtr pLightTransformNode = Node::create(); TransformRecPtr pLightTransform = Transform::create(); NodeRecPtr pLightNode = Node::create(); pLightNode->setCore(Group::create()); Matrix m; m.setIdentity(); m.setTranslate(50,25,50); pLightTransform->setMatrix(m); //we add a little sphere that will represent the light source GeometryRecPtr sphere = makeSphereGeo(2,2); SimpleMaterialRecPtr sm = SimpleMaterial::create(); sm->setLit(false); sm->setDiffuse(Color3f(1,1,1)); sphere->setMaterial(sm); NodeRecPtr sphereNode = Node::create(); sphereNode->setCore(sphere); pLightTransformNode->setCore(pLightTransform); pLightTransformNode->addChild(pLightNode); pLightTransformNode->addChild(sphereNode); pLight->setPosition(Pnt3f(0,0,0)); //Attenuation parameters pLight->setConstantAttenuation(1); pLight->setLinearAttenuation(0); pLight->setQuadraticAttenuation(0); //color information pLight->setDiffuse(Color4f(1,1,1,1)); pLight->setAmbient(Color4f(0.2,0.2,0.2,1)); pLight->setSpecular(Color4f(1,1,1,1)); //set the beacon pLight->setBeacon(pLightNode); water->setCore(geo); root->setCore(pLight); root->addChild(water); root->addChild(pLightTransformNode);
If you run the application it will crash and the terminal tells you something about a "segmentation fault". At first this is not really helpful, however if you next run the program under a debugger and and it crashes again you will be able to get a backtrace (the sequence of function calls that led to the crash). Quite often that does not directly point to the location where the problem is, but if you look at the information the debugger provides you are often able to figure out what went wront. With the knowledge of what your program was about to do at that point it should hopefully be possible to close in on the real problem. So what can it be in our case? When you look at the display function you see right at the beginning that we are accessing the geometry core of the scene node. That exactly is our problem! We need the geometry pointer to modify the geometry and we assumed that the core of the root node holds the geometry core. Well, that was correct, until we changed the design of the scenegraph. Now it is the one and only child of the root node which holds the geometry. Change the line that says
GeometryRecPtr geo = dynamic_cast<Geometry *>(scene->getCore());
GeometryRecPtr geo = dynamic_cast<Geometry *>(scene->getChild(0)->getCore());
Now you should see blue and lit water. The blue is not constant any more, you can see that the borders are in a darker blue than the center. This is because the light source is floating right above the center and there the distance is the shortest of course. Now we would have to calculate correct normals in order to render a more realistic result.
Water correct lit with incorrect normals
The normals need to be computed every frame as they change steadily. This is a problem because computation of normals is quite expensive. However this is not our concern for now. If you like to, you can try to compute the normals. You could also use the calcVertexNormals() utility function I introduced in the last chapter, but I warned you, that this will slow down the simulation significantly. calcVertexNormals() is very generic and can handle some pretty nasty situations, but that takes time. My Linux box with 1400 Mhz can't do the job in real time!
The code so far can be found in the file Examples/Tutorial/10water_lit.cpp. We will now replace the point light with a directional light. If you have not completed the last part of the tutorial you can take the file mentioned above as a starting point!
Replace the line
PointLightRecPtr pLight = PointLight::create();
DirectionalLightRecPtr pLight = DirectionalLight::create();
Just to mention it: We need not to include another header file for the directional light. We have used the simple scene manager which comes along with a default light source and thus includes automatically the header for directional light sources.
Next locate and remove the following lines
pLight->setPosition(Pnt3f(0,0,0)); //Attenuation parameters pLight->setConstantAttenuation(1); pLight->setLinearAttenuation(0); pLight->setQuadraticAttenuation(0);
and add this line:
I hope you wonder why the direction of the light is straight "up" into the sky and not the other way round, because I still wonder, too. Maybe I have overseen or misunderstood something, but to me it seems that the direction of a directional light is always inverse. So I am not quite sure, if it is a bug or a feature ;-) Anyway if you have a hint for me, fell free to send me a <a href="mailto:mail@…">mail</a>''
Water lit by directional light with wrong normals
If you now compare this result with the last one, where we used a point light you will notice the difference in shading. This one here is again uniformly shaded, whereas the point light result looked better. Well, you can now think about what happened... this question can be found again right down here in the exercises part ;-)
- 1) Different Light Sources - Different results
- A few lines up ahead we realized that the results from directional and point light sources differ in shading. Explain the cause for that phenomena.
- 2) Spot Light
- Now replace the light source with a spot light. You may take 10water_lit.cpp or 10water_lit2.cpp as a starting point.
|<< Previous Chapter: Geometry||Tutorial Overview||Next Chapter: Windows and Viewports >>?|