Tutorial Two - Campbell's Soup Can |
|
For this and all the tutorials you will need to download the WOC header and implementation files. I recommend that you unzip them into a folder named woc and locate it at the same level as (i.e. a sibling of) the project folders which use it. This is because the projects look for the WOC files at the path: ..\woc\ as we'll see later. |
|
| About the tutorial
This sample is a quick introduction to the higher-level services of WOC. Rather than definining our own geometry, we'll be making use of WOC's model loaders so we won't need to know too much about WOC's class hierarchy. You will learn how to define your own 3-D model, how to substitute your model for the view's default one, how to use some of the built-in model processing features and finally how to set the initial modelview transformations of your scene. |
WinZip® brings the convenience of Windows to the use of Zip files and other compression formats. If you don't have "the most celebrated shareware app in the history of computing" already, use the above link to download it. |
| The steps 1. You first need to set up a new project which should start life in the same state as the WocSkeleton project from the previous tutorial. You can either follow the previous tutorial again from scratch or, preferably, you can do some copying and renaming. I'll go through this second option now. First, copy the WocSkeleton project folder and rename it WocCampbellsSoupCan. Next, all files beginning WocSkeleton must be renamed to WocCampbellsSoupCan, leaving the file extension alone. Now we need to search for the literal 'WocSkeleton' in a few files and replace every occurrence with 'WocCampbellsSoupCan' - a task you can use Notepad's Edit/Replace... command to do, or better yet an advanced search and replace utility if you have one. The files in which to search and replace are: WocCampbellsSoupCan.cpp, WocCampbellsSoupCan.dsp, WocCampbellsSoupCan.dsw, WocCampbellsSoupCan.rc and resource.h. 2. You now have a renamed copy of the original skeleton project, so open it in Visual Studio and build and run it to check it's okay. From here on in, if I make reference to any WOC classes or concepts that are not either intuitive or clear from the context then please refer to the WOC Class Reference. There you'll find Class Diagrams illustrating the associations between classes, along with full explanations of each class and its members. Choose File/New... and add to the project a new C/C++ Header File named InitialiseOGLWndToCampbellsSoupCan.h. Paste the following function skeleton into it:
void InitialiseOGLWndToCampbellsSoupCan(CWocOGLWnd* pOGLWnd)
{
}
In the file WocCampbellsSoupCan.cpp, after
the inclusion of resource.h, include the
header file we have just created:
#include "InitialiseOGLWndToCampbellsSoupCan.h"Now, in the same file, immediately before the final return statement of WinMain, paste the following call to our new function:
InitialiseOGLWndToCampbellsSoupCan(theApp.GetMainView());
The function we defined earlier requires a pointer to a non-const CWocOGLWnd
object so here we are calling on the global CWocApp
object to return a pointer to its main view (which is the view belonging
to the application's main frame window).3. Returning to the InitialiseOGLWndToCampbellsSoupCan function, let's add some code. First, we'll clear out the default model which the WOC framework placed into our OGLWnd on startup.
pOGLWnd->ClearModels();
If you compile and run now, you'll see that the default model has gone.
Let's put something in its place.
CWocModel* pModel = pOGLWnd->AddModel();
CWocGroup* pGCan = pModel->AddGroup();
CWocModelLoaderOpenCylinder cylLoader;
cylLoader.Load(pModel, pGCan, 40, TRUE);
// Finishing touches.
pModel->GenerateFaceNormals();
pModel->AdjustOGLState(pOGLWnd);
The first line adds a new, initialised, Model to the OGLWnd and gets back
a pointer to it. The next line adds a new, initialised, Group to the new
Model and gets back a pointer to it. Next, we declare a model loader
instance, this one specialising in loading the geometry of an open cylinder,
or pipe, whose length and diameter are both one unit. The loader is asked
to load and it is supplied with: the Model and Group to load into, the number
of divisions to break the surface of the object into (what I call the geometry's
granularity), and a TRUE boolean
argument indicating that we require the loader to generate texture co-ordinates
as well as points. The model is now requested to compute face normals for
itself and then is asked to make appropriate settings to OpenGL's state
for its own requirements. As the default model will already have activated
lighting, this line is not strictly necessary here, but it is a good habit
to get into calling the function as the very last thing you do in the definition
of a model. Compile and run now and you'll see a rotating pipe which is
open at the ends. By default, only one side of a polygon is rendered so
when you see the pipe end-on it you will be looking inside it at the back
of the polygons and so nothing will be drawn for a moment until it rotates
some more. Activate the mouse manipulation mode (Ctrl + right-mouse
click) if you want to get a better view of the pipe, or even stop auto-rotation
via the OGLWnd Properties Dialog (double right-mouse click). Notice that
the surface of the pipe is not curved but faceted because of the type of
normals generated. To remedy this, change the function call above to GenerateVertexNormals,
recompile and re-run and notice the difference.4. Texturing next. If you are following along step-by-step then right now you'll need a couple of image files containing the can's end image and label. You will need the end-of-can image (25KB) and the soup can label image (47KB), both in compressed ZIP format. Unzip the bitmaps and place them in your project folder now. Then, on the ResourceView tab, import the bitmaps (the .BMP files) and dismiss the informational messagebox each time. It is worthwhile pointing out at this point that you must always use 24-bit BMP files with WOC, whether you incorporate them into your resources or read them directly from disk. WOC requires a byte per RBG component per pixel. Also, the OpenGL documentation requires that the dimensions of texture files be a power of two. However, you may be able to get away with simply making them even: you'll have to experiment. If whilst using your own texture files with WOC you have problems, these are the two points to check first - color depth and dimensions. Your bitmap resources will be called IDB_BITMAP1, or something similar. Rename them now to IDB_END and IDB_LABEL for the can-end and the label respectively and then paste the following line immediately before the 'finishing touches' comment:
pGCan->Material()->SetTexture(CWocTexture(pOGLWnd, IDB_LABEL));
The Material member of a Group returns
a pointer to its non-const Material. You may know from the WOC Class Reference
that any number of Groups may be referencing the same Material. Therefore
any changes you make to the Material will by association affect the referencing
Groups. Here we are calling SetTexture
on the Material pointer and supplying a temporary CWocTexture
object. The Texture's constructor requires a pointer to the OGLWnd because
it will be calling OpenGL functions which require an OpenGL rendering context
to be current. OGLWnd objects encapsulate a rendering context as well as
code to bring it in and out of currency. The Texture also needs to know
where to get its image data from. In this case were are using resources
but we could easily well have supplied the path to a file on disk. If you
compile and run, you will see that the pipe now has its label on and looks
more like a soup can.3. Now to close the soup can by giving it both top and a base. To do this we will create two new Groups which share the same Geometry. We will not define a Material for them, so they will default to a grey Material the definition of which is supplied by the static member CWocMaterial::SpecifyDefaultGLMaterial. In addition, we will define different transformations for the two Groups so that they end up at either end of the body of the can. Paste the following lines immediately before the 'finishing touches' comment:
CWocGroup* pGTop = pModel->AddGroup();
CWocModelLoaderDisk diskLoader;
diskLoader.Load(pModel, pGTop, 40, TRUE);
pGTop->Material()->SetTexture(CWocTexture(pOGLWnd, IDB_END));
pGTop->Transformations()->AddNew(CWocTranslation(0.5f, 0, 0));
pGTop->Transformations()->AddNew(CWocOrthogonalRotation(90, Y_AXIS));
CWocGroup* pGBottom = pModel->AddGroup(pGTop->Material(), pGTop->Geometry());
pGBottom->Transformations()->AddNew(CWocTranslation(-0.5f, 0, 0));
pGBottom->Transformations()->AddNew(CWocOrthogonalRotation(-90, Y_AXIS));
Note the different model loader being used here - one which specialises
in generating the geometry (and optionally texture co-ordinates) of a disk
with diameter one unit. The only unfamiliar parts here are the invocations
of the Transformations member of the new
Groups. This member returns a pointer to its non-const Transformations instance
and here we are using it to add new Transformation-derived objects. In either
case a CWocTransformation is first added
whose construction arguments represent the distances to translate in the
x, y and z directions respectively. The arguments used here push the disks
plus or minus half a unit along the x-axis (remember, the open cylinder's
length is one unit). The disks are now in position at the ends of the can,
but they lie on the z=0 plane at right-angles to their desired orientation.
In order to turn them onto the x=0.5 and x=-0.5 planes the second transformation
added to each Group is a CWocOrthogonalRotation
which rotates them 90 degrees and -90 degrees respectively about the y-axis.
Rotations are counter-clockwise about the chosen axis when looking towards
the origin along that axis. OpenGL uses a right-hand co-ordinate system
so you can use your right hand as a mnemonic. Point your thumb in the direction
of a ray leaving the origin along the axis in question and curl your fingers
around the imagined ray. Looking at the tip of your thumb, your fingers
will curl counter-clockwise. This same right-hand mnemonic can be used to
remind you that in an untransformed right-hand co-ordinate system the eye
looks down the positive z-axis toward the origin and that the normal vector
of a triangle points towards the eye if the triangle's vertices are drawn
in counter-clockwise order. For those interested, see the comment at the
top of woc.h with more details on the subject.One last, important thing to note about the above code fragment is the second call to AddGroup. This time we are supplying pointers to resources to be re-used by the Group being created, specifically the Material and the Geometry of the top-end of the can. 4. Not much left to do to finish up. We need to stand the can up on its base and also give it nicer proportions by making it a bit taller. Here's how (before 'finishing touches'):
pModel->Transformations()->AddNew(CWocOrthogonalRotation(90, Z_AXIS));
pModel->Transformations()->AddNew(CWocScaling(1.2f, 1, 1));
The thing to note here is that we have rotated the Model's local co-ordinate
system and so the x-axis is now pointing upwards. Therefore to make the
can taller we scale along the x-axis and not along the y-axis. Also, we
are scaling after translating in agreement with the way OpenGL composes
transformation matrices together: to nonuniformly scale a local co-ordinate
system and then rotate it would result in a shear (in the same way as a
rotation followed by a nonuniform scaling would in a grand, fixed co-ordinate
system). Note that the above transformations are defined on the entire Model's
Transformations so they will affect all of the Model's Groups. No
matter how the Groups of a Model are transformed, you can consider the entire
Model as one fixed unit when defining its own Transformations. Similarly,
as we'll do next, when defining the OGLWnd's Transformations, you
can consider all the Models which go to make up a scene as one fixed unit.
The entire scene in view in the OGLWnd will next be rotated slightly to
move the can's base closer to the eye. It is also too close to the eye so
it needs to be moved along the negative z-axis. Paste the following code,
this time right at the end of the function just inside the closing braces:
pOGLWnd->TransformationsPreservingAutoRotate()->OrthogonalRotate(-14);
pOGLWnd->TransformationsPreservingAutoRotate()->Translate(0, 0, -0.35f);
If we had used the OGLWnd's Transformations
member function then some auto-rotation functionality would have been switched
off because the WOC framework has to make certain assumptions about what
is in the OGLWnd's Transformations instance in order to safely expose
the adjustment of auto-rotation parameters to the programmer and the OGLWnd
Properties Dialog. Implicit in a call to Transformations
is the license to do whatever the caller wishes to the returned Transformations
instance. Therefore the framework has to protect itself by being cautious
and disabling access to auto-rotation properties. As long as we're not adding,
removing nor renaming a transformation in the OGLWnd's Transformations
then the protective precaution is not necessary. So, in cases when the user
is certain that auto-rotation will not be adversely affected, it is safe
to use the alternative TransformationsPreservingAutoRotate
member function which returns the same pointer as does Transformations
but it retains the integrity of auto-rotation. If you want to test this,
switch the above calls for calls to the paranoid Transformations,
compile and run and examine the Properties Dialog. You will notice that
the auto-rotate controls are disabled. Remember to change the code back
to the above version and now you'll see the auto-rotate controls are enabled.
Finally, paste the following code at the end of the function, just inside
the closing braces:
pOGLWnd->SetAutoRotateDegrees(-0.2f);
pOGLWnd->Light()->Transformations()->SetTranslation(1,2,2);
This simply slows down the auto-rotation and moves the default light (GL_LIGHT0)
to a slightly more interesting position. Compile and run now, and there's
the finished demo.A quick postscript about the texture used on the ends of the can. I couldn't find a suitable texture on the WWW so I began thinking about having a digital photo of a can-end taken. As it happened, though, I quickly wrote a new class CWocModelLoaderRippleGrid (by copying most of CWocModelLoaderGrid) which puts ripples into the normally plane grid. I temporarily altered the new class to leave the centre of the grid flat in order to emulate the end of a can and then rendered a model loaded by the new loader. It was a screenshot of the resulting lit model that ended up as the texture image. This was a very good illustration of leveraging WOC to come up with a quick and easy solution. |
| last updated: 4-oct-01 |