Tutorial Three - Star Cluster

 Download the WocStarCluster EXE (75KB) in compressed ZIP format.

 Download the WocStarCluster project files (8KB) in compressed ZIP format.

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.


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.
   
About the tutorial
This sample isn't supposed to be a realistic simulation; the motion of each body isn't independently calculated from the gravity of every other body. The galaxy is just a rigid 3-D model being rotated in a similar way as the view's default model is. In spherical mode, though, the stars are packed more densely towards the centre which is a nod in the direction of realism.

What this sample is supposed to demonstrate is how to get more control over the definition of your 3-D geometry than is afforded by the model loaders that were seen in the previous tutorial.
 
   
The steps
1. You first need to set up a new project which should start life in the same state as the WocSkeleton project. Please perform the same copying and renaming process as in step 1 of the previous tutorial but this time we are replacing WocSkeleton with WocStarCluster.

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. Again, 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.

Choose File/New... and add to the project a new C/C++ Header File named StarClusterModelDef.h. Paste the following function skeleton into it:
void StarClusterModelDef(CWocOGLWnd* pOGLWnd)
{
}
In the file WocStarCluster.cpp, after the inclusion of resource.h, include the header file we have just created:
#include "StarClusterModelDef.h"
Now, in the same file, immediately before the final return statement of WinMain, paste the following call to our new function:
    StarClusterModelDef(theApp.GetMainView());
3. Before adding any more code, we'll look at some of WOC's functionality in use by the WOC framework itself. In Visual Studio, in the ClassView, expand the class CWocOGLWnd and double-click the InitialiseGL method to take you to its implementation. Look at the section after the multi-line comment where an identifier named m_DefaultModel is being manipulated. This identifier is a data member, of type CWocModel, belonging to the CWocOGLWnd class and is, up to this point in the code, in its constructed state and empty. Of course the object could safely be rendered in this state and nothing would be seen. The default model is loaded by means of the Load(CWocModel* pModel, CWocGroup* pGroup, BOOL bInitialise = FALSE) method of a helper class called CWocModelLoaderDefault (I have given the full method signature because there are two overloaded Load methods). Let's take a detour to look at the implementation of that method. Zero was passed as the pGroup argument so first we need to obtain a valid pointer to the Model's default Group. The CWocModel::Group method is called on the supplied Model object and, because no argument is passed, the default Group is created if necessary and a pointer to it is returned. No value for bInitialise was supplied so it defaults to FALSE. This means that if the Model already contains any definition then it is left intact; instead the number of points it contains is obtained so that the index of any newly-added points can be offset by this amount. The Model is empty at present, so the number of points, and hence the offset amount, will be zero. Now look at the line of code where SetMaterial is called on the default Group. The Material being set is a stock material (slate-colored plastic) a pointer to which is returned from the CWocModel::AddStockMaterial method. This is a demonstration of the technique of adding a new resource to the Model (in this case a Material) and obtaining a reference to it (in this case a pointer) for use by another of the model's resources (in this case a Group). It is also possible to obtain a pointer to a resource by requesting it by name from the Model via the appropriate method. For example, the stock slate-colored plastic material just added was given the name 'STOCK_SLATE_PLASTIC'. A pointer to this material can be obtained by passing the name to CWocModel::Material.

Two arrays are defined next: the first containing x,y,z triples of point co-ordinates and the second containing point indices. I'll explain what a point index is by first looking at the points. The point array passed to CWocModel::AddPointArray contains 168 values which are taken in groups of three (x, y and z values) and stored in the model as 168 / 3 = 56 points. These 56 points are numbered, or indexed, from 1 through 56 and by referring to an index in this range, any number of triangles may re-use any of these points. A triangle has three vertices, each a point, so typically a triangle will store the indices of three different points. If you now look at the second array, the array of point indices, you will notice that all of the index values are used several times. These are examples of different triangles re-using the same point. You may already have read that WOC stores its Geometry in the form of points and triangles (triangle vertices are inherently coplanar) so you may be wondering about the use of CWocGroup::AddQuadArray in the code (immediately after offsetting all the point indices in the array). In fact, a quad (quadrilateral) is just a shorthand definition of a pair of triangles. By supplying four point indices and calling it a quad, we are asking the AddQuadArray method to break down each group of four point indices into two triangles by sharing two of the points. AddQuadArray internally rips through the array and calls AddTriangle twice for each quad. You can of course call AddTriangle yourself to add Triangles individually, you can also add quads individually, and you can add Triangles in arrays, too. Points may also be added individually as well as in arrays. Lastly, the model loader calls on the model to calculate normal vectors for itself. One of GenerateFaceNormals() or GenerateVertexNormals() is necessary if a model is defined without normals as this one was. These methods use the triangle geometry to calculate surface normals which are used by OpenGL to properly light the model. You can of course add your own normals directly to the model using AddFaceNormal or AddVertexNormal but this is rarely necessary when they can be so easily calculated.

The model's geometry having been defined, let's return to the CWocOGLWnd::InitialiseGL method and note how it ends. SetShowNormals determines whether to draw lines representing the surface normal vectors or leave them invisible which is the default. This is a useful diagnostic if you suspect your normals might be faulty (or non-existent) and you want to see them; alternatively, you may like the artistic effect. In either case, the benefit of showing normals is lost if the model has a very large number of small polygons as it ends up looking like a fuzzy caterpillar. Finally, AdjustOGLState is called to give the model the opportunity to make the changes it needs to the OpenGL state. For instance, if the model has normals then lighting is activated and a light is created if necessary, and if the model uses textures then texturing is enabled.

3. To consolidate what we've just looked at, let's define a simple model before going ahead with the star cluster. Go back to the StarClusterModelDef function in StarClusterModelDef.h, and paste in the following code:
    pOGLWnd->ClearModels();

    CWocModel* pTetrahedron = pOGLWnd->AddModel();
    pTetrahedron->AddPoint(0.0f, 1.0f, 0.0f); // point 1.
    pTetrahedron->AddPoint(1.0f, 0.0f, 0.0f); // point 2.
    pTetrahedron->AddPoint(cos(PI*2/3), 0.0f, sin(PI*2/3)); // point 3.
    pTetrahedron->AddPoint(cos(PI*4/3), 0.0f, sin(PI*4/3)); // point 4.

    CWocGroup* pDefaultGroup = pTetrahedron->Group();
    pDefaultGroup->AddTriangle(1,3,2);
    pDefaultGroup->AddTriangle(1,4,3);
    pDefaultGroup->AddTriangle(1,2,4);
    pDefaultGroup->AddTriangle(2,3,4);

    pTetrahedron->GenerateFaceNormals();
    pTetrahedron->AdjustOGLState(pOGLWnd);

    pOGLWnd->SetFovY(45);
    pOGLWnd->TransformationsPreservingAutoRotate()->Translate(0, 0, -2);
What's happening here? First, four points are added to the Model: a floating-point x,y and z value per point. Then a pointer to the model's default Group is obtained by leaving the Group's name blank in the call to CWocModel::Group and letting it default to the default name. Next, triangles are added to the Group by specifying three point indices at a time. The result is a four-sided solid called a tetrahedron - a very simple solid. The SetFovY method sets the angle of the field-of-view in the y direction. Larger FovY angles give a fish-eye lens effect which makes more of a scene visible but makes the scene appear further away even though it isn't. The default field-of-view is 90 degrees.

If you build the project now and run it you will see that the default model has been replaced by the tetrahedron. Activate the mouse manipulation mode if you want to see the effects of the change in field-of-view.

4. That's two 3-D models we've looked at now, both involving points and triangles: two of the primitives of geometry. A triangle doesn't have to be rendered as a face. It can be rendered as the points of its vertices or as the lines which join its vertices. The Model method which determines the mode is SetPrimitiveMode and it accepts any of the OpenGL-defined constants that ::glBegin accepts. If you pass GL_TRIANGLES, GL_LINE_LOOP or GL_POINTS then triangles, lines or points are rendered respectively. If you pass any other of the constants then the most appropriate of the three primitive types is assumed. The star cluster model we're about to define will use the GL_POINTS primitive mode. The model defines a large number of randomly-located points, packages the points three-at-a-time into triangles, and then renders the triangles as larger-than-normal anti-aliased points. The random point-generation logic I will show here creates spherical coordinates (in the form {r, theta, phi} where r is the radius or distance from the origin, theta is the rotation about the y-axis in the range 0 to 2Pi radians and phi is the rotation above or below the y=0 plane in the range -Pi/2 to Pi/2 radians) and then uses one of the CWocVector class's constructors to convert to cartesian coordinates. The random angles are unbiased but, in order to produce greater star density closer to the origin, the radius values are randomly selected in the range (0,1) and then squared. If you download the WocStarCluster project files then you will find another function (not listed here) which generates the stars inside a cube rather than a sphere. You can compile the project to use either style.

Let's go straight into the code now and back it up with some explanations. In StarClusterModelDef.h, delete the code inside the function StarClusterModelDef and replace it with this:
    pOGLWnd->ClearModels();
    CWocModel* pModel = pOGLWnd->AddModel();
    CWocGroup* pGStars = pModel->AddGroup();

    long lNumStars = 1500;
    GLfloat fRotAroundYAxis, fRotFromY0Plane, fRadius; // Used to generate stars.
    for (int i = 1; i <= lNumStars - (lNumStars%3); i++)
    {
        fRadius = RAND1;    // Random value (0,1).
        fRotAroundYAxis = RAND_U(2*PI);         // Random value [0,2Pi].
        fRotFromY0Plane = RAND_LU(-PI/2,PI/2);  // Random value [-Pi/2,Pi/2].
        pModel->AddPoint(CWocVector(CWocVector::enumVectorConversionTypeSphericalToCartesian,
                                    fRadius,
                                    fRotAroundYAxis,
                                    fRotFromY0Plane
                                    )); // standard spherical-to-cartesian co-ord function.
        if (!(i%3)) // Every third vertex...
            pGStars->AddTriangle(i, i-1, i-2); // ...and add it to our group.
    }
There isn't much here that's unfamiliar. The first things we haven't seen before are the RAND macros all of which generate a random floating-point number. RAND1 generates in the range (0,1). RAND_U(n) takes an argument for an inclusive upper limit and generates in the range (0,n) and RAND_LU(m,n) takes arguments for inclusive lower and upper limits and generates in the range (m,n). There is no need for our code to seed the C Run-Time Library's random number generator, as this is done in the constructor of the CWocApp by using the current time. If you look inside the for loop in the code above, you'll see a call to an overloaded version of AddPoint which takes a CWocVector object instead of the three floating-point values we've seen before. This is to take advantage of an overloaded CWocVector constructor which performs the spherical to coordinate conversion I talked about above. Note that each time the variable i (which controls the for loop) becomes 0 modulo 3 (i.e. exactly divisible by 3) a triangle is added to the Model's only Group whose points are the current and the previous two points added to the Model. Now paste this last fragment of code at the end of the function:
    pModel->GenerateFaceNormals();

    pModel->SetPrimitiveMode(GL_POINTS);
    pModel->SetAntialiasing(TRUE);
    pModel->AdjustOGLState(pOGLWnd);

    pOGLWnd->MakeRCCurrent();
    ::glPointSize(4.5);
    pOGLWnd->MakeNoRCCurrent();
Notice the call to SetPrimitiveMode with GL_POINTS. We are also activating anti-aliasing before asking the Model to adjust OpenGL state. Finally, something which is not integrated into WOC yet is the size of points nor the width of lines; this is something you have to set manually. Whenever any code talks directly to OpenGL, an OpenGL rendering context must be current otherwise the commands are ignored. WOC handles this requirement internally but when you're making your own calls to OpenGL you will need to bracket the OpenGL API call(s) with calls to MakeRCCurrent and MakeNoRCCurrent. You can do this any time you have a valid pointer to a CWocOGLWnd. Compile and run now and see if you like the result. You may want to zoom out with the mouse so you can see the entire star cluster. Also, try adjusting the timer interval and the auto-rotate degrees either with the Properties Dialog or programmatically with the functions CWocOGLWnd::SetTimerInterval and CWocOGLWnd::SetAutoRotateDegrees respectively. Remember, rendering of the model occurs on a timer and this is also the time when automatic rotation takes place if it is activated. Setting the number of degrees by which to auto-rotate the model or the number of milliseconds between timer events are two ways to accelerate or slow the apparent rate of rotation. This application is not a demanding one so you will certainly be able to reduce the timer interval to zero if you wish, but in general you should set the timer interval as low as possible to get the smoothest frame rate for your CPU and then adjust the auto-rotation degrees to set the apparent speed of motion to your liking. If you find at any time that you are unsure which way the star cluster is rotating because there are no clues such as edges or hidden surfaces, then rotate the model slightly with the mouse and you will immediately see a greater movement from closer stars due to parallax and the true direction will spring out.
 
last updated: 4-oct-01