Main Page | Namespace List | Related Pages

Basics

underscore.png

Chapter Overview


>Datatypes
>The Math Stuff
>Colors
>Nodes
>Naming your nodes
>Volumes
>Cores
>Tutorial - it's moving!
>Field Container
>Editing Field Containers
>CoredNodePtr - Core and Node under one roof
>Standard Geometry
>Tutorial - More than a torus
>Images and Textures
>Tutorial - Using textures
>Loading and saving of scenes
>Tutorial - loading and saving
>Exercises

If you have read this tutorial right from the beginning, you are now able to install OpenSG, you know a few things about GLUT as well as the Simple Scene Manager and you have compiled and executed your first program. Well that is good, but you need to know a bit more before you write your first real application.

In this chapter I will show you the basic things of OpenSG, that is:

Note: A general comment about the class documentation that is referenced here sometimes: OpenSG uses C++ inheritance and templates quite extensively to simplify creating variants of classes that have similar functionality but different types. As a result, when looking at the documentation for a class like osg::Matrix, some of the documentation and some of the useful methods might actually be in the parent class osg::TransformationMatrix. So if in doubt, generally check the parent classes for functionality, too.

Datatypes

OpenSG has its own base types for integers and floats. In many cases you can get along by using int, float etc. as you normally would, but if you want to develop a cross platform application it is safer to use the OpenSG wrapper base types.

These types can be easily identified by their names
[U]IntN N defines how many bits to use. 8, 16, 32 or 64 are supported. The optinal U stands for unsigned
RealN Floating point with either N = 32, 64 or 128 bits precision

An unsigned 32 bit integer is therefore UInt32. The reason for using these wrapper is that the usual "int" variable is 32 bits long for most systems... but not for every system. By using Int32 you do not have to care, you can be sure that this type is always that long.

The Math Stuff

OpenSG comes with it's own classes for doing calculations with Vectors, Matrices etc. We all know that there are approximately a thousand other math libraries which are more or less equally powerful. If you are in need of lots of mathematical computations and high performance is very critical to you, you may consider using another software component for these tasks, but in every other case, you will be fine with OpenSG's build in functions.

As you might expect, there are classes for every need related to computer graphics: Vectors with different precision and with two up to four components, as well as the same for matrices.

There are 21 different vector type which I will not list individually, but the construction rules are similar to the base types.

VecN {b, ub, s, us, f, d, ld}

N is the dimension which must be chosen between two and four. From the list of letters you also must choose one, which defines the type used for storing the values.

The one you need in many circumstances is most likely to be Vec3f, a three dimensional vector with float values.

Working with vectors

The OpenSG vector classes can do everything you expect from them and probably even more. ;-) Here is an overview of the most important operations. Have a look at osg::Vec3f (or any other vector) for a full list of their methods

// two nice 3d vectors
Vec3f v = Vec3f(1,2,3);
Vec3f w = Vec3f(0.5,2.5,5);

// get the length of a vector 
Real32 l = v.length();
// float or double is possible, too
float l = v.length();

// if you only want to figure out which vector is longer you do not
// need the exact euclidian length. (You can spare the square root)
Real32 lq = v.squareLength();

// normalize a vector (length will become 1)
v.normalize();

// the cross product of two vectors
Vec3f e = v.cross(w);
// ATTENTION: cross product is only implemented for 3 dimensional vectors

// dot product
Real32 d = v.dot(w);

// access to individual components
Real32 c1 = v[0];
Real32 c2 = v[1];
Real32 c3 = v[2];

// you can use mathematical operations the same way as with integers and floats

Vec3f s = v+w;

if (v==w)
    cout << "these vectors are equal" << endl;

Points

Points are quite similar to vectors. They come in all the same variants like vectors, only replace "Vec" by "Pnt" and you have it. The difference compared to vectors is that points mark a definite location within the spacial domain, where vectors are not bound to a specific point as they represent a direction or power (remembering your math and physics lessons now ;-)...)

Let us say points are vectors with some fewer possibilities. Of course points cannot be normalized and there is no dot product between two points. Please notice that two Points can be added neither! If you need to convert between vectors and points, there are two useful methods to do that very efficiently

// conversion point to vector (let p be some point and v some vector)
v = p.subZero();

//or vice versa
p = v.addToZero();

Colors

Colors, too, are similar to vectors. They are available in three or four dimensions, where the fourth dimension represents the alpha channel, whereas the first three are RGB color channels. The color classes support also the HSV color model. They have named access methods (red, green and blue) with which you can access the values in a more reasonable way than with color[0]. Furthermore scalar multiplication is possible but most other operations known from vectors are missing as they are generally not needed.

Detailed information about the methods for colors can be found at osg::Color3f

Working with matrices

Matrices behave quite similar to vectors. There is no default type for vectors, but there is one for matrices: Like in OpenGL it is 4x4 with Real32 components. The multiplication convention is just the same as in OpenGL:
v'=M*v

In OpenSG matrices are stored column major like shown in the next picture

matrix_storage.png

Storage of a matrix in memory

Note that this is not how 2D arrays are usually stored in C/C++, but it is consistent with how OpenGL stores matrices.

The first column vector can be retrieved with a simple matrix[0]. Storing the values in this manner has a little advantage compared to row major storage as you can easily access the matrix coordiante space, especially matrix[3] yields automatically the translation from the origin.

There are several ways to construct a matrix that matches your needs. It is possible to create a matrix by providing all components one by one or by passing the base vectors or you can use some methods of the matrix class to create a matrix with certain properties.

// we want to create a matrix that scales the world at the y axis by factor 2 
// and also translates by (2,2,3). As we all know from old days in school the 
// corresponding matrix is 

// | 1 0 0 2 |
// | 0 2 0 2 |
// | 0 0 1 3 |
// | 0 0 0 1 |

// first we create the matrix by passing all values directly

Matrix m;

m = Matrix(1,0,0,2,0,2,0,2,0,0,1,3,0,0,0,1);
// ATTENTION : noticed something? The arguments are passed row major! This applies to
// this specific constructor only and is done to simplify copying matrices from paper!

// if we had base vectors like this...
Vec4f v1 = (1,0,0,0);
Vec4f v2 = (0,2,0,0);
Vec4f v3 = (0,0,1,0);
Vec4f v4 = (2,2,3,1);

// ... we can also make our matrix by ...

m = Matrix (v1,v2,v3,v4);
// of course column major, as expected

// But really practical are these variants

// this one resets the matrix to identity
m.setIdentity();

// we set the scale factor(s)
m.setScale(1,2,1);
// and the translation
m.setTranslate(2,2,3);

// ... and here we go

// now we want to multiply a matrix and a vector

Vec3f a = Vec3f(1,2,3);
Vec3f result;

m.mult(a, result);

// ATTENTION: remember matrix multiplication when you did it manually? We are 
// multiplying a 4x4 matrix with a 3x1 vector. Actually that should not work... 
// but in OpenSG it does, not because of a false implementation, but with 
// respect to the fact that the fourth row is rarely used as it is (0,0,0,1)
// in most cases. This is why OpenSG assumes that you actually multiply a 4x3 
// matrix with a 3x1 vector... and that is ok!

// as you might have guessed the result of the mutiplication is assigned to the 
// vector "result"

// multiplication of two matrices work in exactly the same way

Please notice the following, when using the setter methods for matrices. These methods overwrite only the components they need to, leaving all others untouched, i.e if you have a matrix and want to change the translation you have to be sure that the other values are correct, too. The method setIdentity() will overwrite all values of course!

matrixComponents.png

Matrix Components

This figure shows which components of the common 4x4 matrix will be changed by which kind of transformation. (T=translation, S=scalation, R{x,y,z}=rotation around the given axis). As you can see, it is safe to set a translation and scalation at once, as these share no components. However setting both, scalation and rotation will yield a result you do not want in most cases. You need then to multiplicate two seperate matrices correctly...

If you want to create a Matrix of a specific kind (transform, scale, rotate...) you can use the setTransform method. It is overloaded and there are a number of varieties that create different things, depending on the passed arguments. The most common ones are using a single Vec3f as an argument for a translation, a single Quaternon for a rotation, or a Vec3f and a Quaternion for rotation and translation.

Have a look at osg::Matrix for a complete list of all set and get methods available.

Quaternions

The last topic for this OpenSG-math-quick-tour are quaternions. The background of quaternions is not really easy to understand, but do not worry, you can use them without knowing how they actually work - I can stand proof for that. ;)

So what are quaternions about? You might know that interpolating between rotation matrices does not work that well. That is if you have a matrix describing a 30 degree rotation around the y axis and another matrix doing the same with 60 degree you just cannot interpolate the matrix elements between these two for an animation.

Quaternions are an execellent solution to this problem. They are described by an angle and a vector. The angle is the amount that you want to rotate around the vector you provided.

quaternion.png

A Sample Quaternion

This quaternion is described by a vector, which could be approximately v=(1.5,3,0.5). For example, if the provided angle would be 30 deegres, then the entire scene would be rotated by 30 degrees around the axis defined by that vector v. You see, rotations have never been so easy!

Note that internally the quaternions only use and store normalized vectors, as the length of the vector is irrelevant for describing the rotation.

As you might expect now, it is possible to interpolate between two quaternions and as these can be easily transformed into a matrix. So we can now realize custom rotations around any axis. The following example solves the situation described above

// well this is a bit theoretical... we will have more real-world examples 
// within the next tutorial

// we need a quaternion and a matrix
Quaternion q;
Matrix m;

// reset our matrix
m.setIdentity();

for (int t = 0; t < 30; t++){
    // the given angle is in radians per default
    q = Quaternion(Vec3f(0,1,0), (30+t/180)*PI );
    m.setRotation(q);
    
    // draw the scene here ... 
}

If you do not like radians you can also use degree by invoking

q.setValueAsAxisDeg(Vec3f(...), 90)

The big deal about quaternions is that you can interpolate between quaternions quite nicely, using the slerp() member function. As quaternions always define rotations using slerp() always gives you a valid rotation that linearly (i.e. with constant speed) interpolates between the input rotations.

Furthermore standard operations like length(), normalize(), inverse(), multiplication and others are possible. Detailed information are here : osg::Quaternion

Nodes

Nodes are in some way the most important object in a scenegraph. In OpenSG the nodes are describing the hierarchy of the graph only! Here is a simple graph we want to build:

simple_graph.png

A family scenegraph

//First, we create all the nodes we need
NodePtr grandpa = Node::create();
NodePtr aunt    = Node::create();
NodePtr mother  = Node::create();
NodePtr me      = Node::create();

// uh, what is happening here???
// I guess you expected something like
// Node n = Node(); or Node *n = new Node();
// do not worry, explanation will follow hereafter...

// now we create the hierarchy
beginEditCP(grandpa);
    grandpa->addChild(aunt);
    grandpa->addChild(mother);
endEditCP(grandpa);

beginEditCP(mother);
    mother->addChild(me);
endEditCP(mother);

// beginEditCP()??? That, too, will be discussed in detail later

Please do not wonder about that strange begin- and endEditCP thing. For now you only need to know, that we need these whenever we want to modify an OpenSG object (excluding basic math types and very few other). We will learn more about it in section Editing Field Containers. But for now we will be fine with the way it is.

This little piece of code would generate a graph that would look like in the picture above. Would you be able to render that graph? You can try out what will happen: Have a look at the code from our First Tutorial and replace the following line

    NodePtr scene = makeTorus(.5, 2, 16, 16);

with the example code from above. But be careful, with grandpa it will not work, you have to rename him to "scene", because the Simple Scene Manager is told to use scene as root. Or you can change the code to

mgr->setRoot(grandpa);
, whichever you prefer.

And? Did it work? We will come back later to this little example.

Creating new nodes and other objects with ::create()

You might wonder why objects are instantiated this unsual way. Actually the "normal" way will not work for most OpenSG objects. In fact calling

Node n = Node()
will get you an error during compiling like this one
osg::Node::Node() is protected within this context

Well, that is looking bad. What to do, if the constructor is obviously not public thus it cannot be called. I still remember when I faced this probem and wondered about it.

Actually every class that is derived from osg::FieldContainer has protected constructors. This includes pretty much all high-level objects like Nodes for the graph, Materials, Textures, Images etc. Other, lower-level, classes, like the math classes we just saw, have their constructors declared public as usual. So if you are unsure whether you can use the constructor directly, just have a look at the inheritance diagram and if none of the parents is of type FieldContainer then it will work. In section Doxygen Documentation Pages I'll explain how you can find the inheritance diagram in the doxygen documentation.

If that is not the case, you must invoke the static create() method as you saw above. This static method wraps the constructor. It is done this way for different reasons, the most important being able to ensure multithread safety, but we will discuss that in a separate chapter (Multithreading).

One consequence of the constructor not being public is you can't create arrays of OpenSG objects, so code like

Node myNodes[20];
myNodes[0] = Node();

doesn't work. This is not a real problem, as the objects are too big to be passed around by value anyway, and you should use pointers to them instead. Which brings us to the

...Ptr
classes.

What are these Ptr thingies?

Alright, we now understand when we have to use create, but another strange thing is, that the variable which represents a node is not of type "Node" or "Node *", but "NodePtr". Take it as a rule of thumb: For every object created with create() the return type is a corresponding objectPtr.

Here are some examples

NodePtr n = Node::create();
TransformPtr t = Transform::create();
GeometryPtr geo = Geometry::create();
ShearedStereoCameraDecoratorPtr sscd = ShearedStereoCameraDecorator::create(); 
// yes, that is an actual class, and a very useful one at that ;)

I think you get the picture. The Ptr classes act as pointers in pretty much any way. You can dereference them using

*
and call member functions on them using
->
The limitation is that you can't do pointer arithmetic on them and they can't be used to represent arrays of objects, every Ptr always points to one object. But that is not a real limitation, you can (and should) use arrays of Ptrs just like arrays of pointers quite easily.

Standard pointers have a defined value to indicate they are unset or invalid, the NULL constant. Given that the Ptrs are actual classes, you can't compare them to NULL. Instead there is a special value NullFC (for NULL FieldContainer), that can be used instead. It is recommended to check FieldContainerPtr against this value before dereferencing them, just like you should check normal pointers for NULL before using them.

Cleaning up after yourself - Reference Counting

One of the problems of the C++ language is keeping track of and deallocating memory. Other languages like Java solve by garbage collection, i.e. the Java runtime environment keeps track of all objects and object references in a program. Every now and then it goes through all the references and finds the objects that are not referenced any more, which means that the program has no way to access that object any more. These objects' memory is freed. The problem with this approach lies in the time the garbage collector takes every now and then, which for large or long-running programs can lead to annoying pause periods while the garbage collector is running, which is a serious problem for interactive or real-time programs.

C++ doesn't have garbage collection, and is designed for explicit memory managament using new and delete. It's the program's resposibility to know when an object is not used anymore and call delete on it. This can become pretty tedious, especially in a system where objects point to each other from different directions, which is a pretty nice characterisation of a scenegraph. Nodes point at their children, Geometries point at their Materials etc. For an application it would very hard to keep track of all these references to know when to delete the memory of an object.

Therefore OpenSG implements an approach known as reference counting. Each object derived from FieldContainer has a counter for all the references (i.e. pointers, or, more exact Ptrs) to it. If this counter goes to zero, the program can't access the object anymore, and the object's memory can be freed. This relieves the program from all responsibility of memory management.

Not quite. Reference counting is not free. The reference count needs to be kept consistent to be useful, and in a multi-threading environment that means locking. Locking can get pretty expensive, especially for a common operation like setting the value of a pointer. This happens a lot, especially for passing objects around as parameters to functions. If OpenSG had to lock the reference count of all parameter objects for every function call, the cost would be prohibitive.

Therefore this responsibility rests on the shoulders of the application writer, i.e. you. The two functions

addRefCP()
and
subRefCP()
increment and decrement the reference count of an object. When it goes to or below zero the object is destroyed. Objects are created with a reference count of zero.

The system helps where it can, in places like setting attributes of scene graph objects (e.g. setting the Material of a Geometry) or adding or subbing Nodes to/from other Nodes it automatically changes the reference count. This takes care of most cases, but can have some unexpected consequences, like the following piece of code demonstrates:

    Node a = Node::create();
    Node b = Node::create();
    Node c = Node::create();
    
    // we add "a" as a child to "b"
    beginEditCP(b);
        b->addChild(a);
    endEditCP(b);
    
    //no, we want "a" to be a child of "c"
    beginEditCP(b);
        // this removes "a" as a child of "b"
        b->subChild(a);
    endEditCP(b);
    
    //and now add it to "c"
    beginEditCP(c);
        c->addChild(a);
    endeditCP(c);

Well, looks good, does it not? Actually your compiler will not complain about it, as it is correct in it's syntax, but if you run an application with this piece of code it will crash! Think about, why it will crash, before reading ahead...

Okay, no big deal? The explanation stands right above the code... when we call subChild on a Node the reference count is reduced from one to zero and it is immediatly deleted from memory. When we want add a to c we are operating on a non existent object - and that is never good.

So if you want to change parents of a node you need to make sure that it's reference count stays above zero. You can manually increase or decrease the reference count of any object by calling

addRefCP(objectPtr)
subRefCP(objectPtr)
You have to increase the reference count via addRefCP first, before you delete the child of any node. Here is the correct code

    NodePtr a = Node::create();
    NodePtr b = Node::create();
    NodePtr c = Node::create();
    // all reference counts of a,b and c are 0
    
    // we add "a" as a child to "b"
    beginEditCP(b);
        b->addChild(a);
        // "a" now has a reference count of 1,
        // because adding it as a child increases
        // the count by one
    endEditCP(b);
    
    //no, we want "a" to be a child of "c"
    beginEditCP(b);
        // first we increase the reference count
        addRefCP(a);
        // reference count of "a" is now 2
        // this removes "a" as a child of "b"
        b->subChild(a);
        // reference count is now 1 again
    endEditCP(b);
    
    //and now add it to "c"
    beginEditCP(c);
        c->addChild(a);
        // and the reference count is now 2
        
        // to avoid problems we decrease the count by hand
        subRefCP(a);
        // and now the count is 1 as it should be
    endeditCP(c);

RefPtr - as smart as it gets

This can become tedious and lead to unexpected failures, therefore versions after 1.2 add a helper class that automates refcounting a little more (told ya using a newer version was better!). These are known as Smart Pointers, in OpenSG the class is called
    RefPtr<>

A RefPtr is just like any other pointer, except that it automatically increments and decrements the reference count of an object assigned to it. So when you set a RefPtr, its current object's refcount in decremeneted, and the new objects refcount is incremented. When the RefPtr object is destroyed, the object's refcount is also decremented.

An example:

    #include <OpenSG/OSGRefPtr.h>

    void moveNode(NodePtr parent, NodePtr newparent, int childnum)
    {
        RefPtr<NodePtr> c = parent->getChild(childnum); // refcount++, to make sure it survives
        
        beginEditCP(parent);
            parent->subChild(c); // refcount--, but the RefPtr keeps it safe
        endEditCP(parent);
        
        beginEditCP(newparent);
            newparent->addChild(c);
        endEditCP(newparent);
        
    } // refcount--, as the RefPtr is destroyed
    
    RefPtr<NodePtr> a = Node::create(); // we use this Node, keep it alive

    beginEditCP(a);
        a->addChild(Node::create()); // the new nodes refcount is now 1
    endEditCP(a);

    RefPtr<NodePtr> b = Node::create(); // keep this alive, too

    moveNode(a, b, 0);    
    // a has no children now, b has one

This is a non-standard use of the RefPtr, just to show what it does. In general, as a rule of thumb, I'd recommend using RefPtrs for all the pointers that are kept and used outside the function they're first used in. This applies to Ptrs that are global variables or kept in your own classes. Don't generally use them for temporary variables, the example above shows where it can make sense, but in general it's not necessary.

Reference counting can seem imposing at first, but it is extemely useful, and the RefPtr takes most of the tedium out of it. As it is only available is versions later than 1.2, it is not used in these tutorial examples, but if you can, use them in your own programs.

Naming your nodes

When working with big scenes it can be very useful to name your nodes. For instance you could name your node which holds the geometry for a car "car_geo". You will see that it will be much easier to search your graph for a node with a known name. In Chapter Using Models from Modeling Packages we will learn how to use modeling software like 3D Studio Max in conjunction with OpenSG. At this point I only want to mention that if you have named parts of your model, you can search for these nodes by using exactly the same names as specified in your modeling software.

It is very easy to assign a name to your nodes. Here is an example

// you will need this include file in order to work with named nodes
#include <OpenSG/OSGSimpleAttachments.h>

NodePtr n = Node::create();

// now we assign a name of our choice
setName(n, "BigScene");

// if we want to extract the name later on...
if (getName(n))
    cout << "This node is called " << getName(n);

It is very important to check if the result of getName() is true. If you skip the if clause and use the result right away, your program will crash if no name was set, as getName() returns NULL in that case!

Later I will introduce a helper class which searches the whole graph and returns the node matching a given name. (see Big Tutorial) However that is not the optimal way to traverse the graph - of course there are some powerful functions that you can use to traverse the graph fast. This will be discussed in chapter Traversal of the Graph

Volumes

Every node has an axis-aligned box as a bounding volume. That is the smallest possible box, containing all polygons, with all edges parallel to the axis of the coordinate system. As you might know or guess, these bounding volumes are used to speed up several processes like casting a ray or checking if an object is within the frustum of the camera. If you want to know whether a ray hits an object with 20.000 polygons or not, you can first test against it's bounding box and if that box is not hit the object can not be hit, saving you a lot of intersection tests.

bounding_box.png

The red object is enclosed by the bounding box

Normally these bounding boxes are managed by OpenSG automatically. If you modify geometry during runtime, the bounding box of the corressponding node will be marked as invalid. If and only if this node is used for the next time it's bounding box will be updated automatically. If you need to mark a volume as invalid for some reason, you can do so by invoking the following on any node

n->invalidateVolume()
Note that the invalidation will walk the graph back up. If a bounding box of any node is invalidated so is the bounding box of the node's parent. That will be repeated until the root node is reached.

The volume is stored at the node level (cores do not carry a volume) and they can be aquired by the following command

    //n is some osg::Node
    DynamicVolume &vol = n->getVolume(false);

The boolean parameter specifies whether the bounding volume should be updated or not. You have now several possibilities to work on the volume. With the help of the intersect() method can test manually if an osg::Point is inside the volume or if an osg::Line or an osg::Volume intersects the volume. Also you can extend the volume with another point. Very important is the possibilty to avoid updating of the volume. Later (Geometry Utility Functions) we will create a water model from the bottom up, which will be animated. This means that the volume will be updated every single frame (and also all parents) which is not necessary in every situation. So it would be a good idea, to set the volume to the maximum extend and disable updating of that single volume. The following code fragment shows how you could implement such a functionality

    // vol is of type osg::DynamicVolume  (from above)
    
    // this will clear the volume (i.e. contains nothing)
    vol.setEmpty();
    // two points are enough to define the bounding box
    vol.extendBy(Pnt3f(0,0,0));
    vol.extendBy(Pnt3f(100,100,100));
    //now we have a cube with all edges 100 units long
    
    //mark it as valid, so it will not be updated with the actual geometry
    vol.setValid(true);
    
    //finally we tell OpenSG to never modify/invalidate this volume
    vol.setStatic(true);

Notice: Please keep in mind that in this case the volume is totally static. Changes to the geometry are just ignored for the volume's status.

Cores

Cores in OpenSG are one of the most important datatypes. I hope you got a feeling how to create and use nodes throughout the last sections, but all we did so far was not really visible on screen except for the first complete tutorial. Anyway, remember that we used
NodePtr scene = makeTorus(.5, 2, 16, 16);
to create the scenegraph. The makeTorus is a high level command which creates a node already connected with a geometry core containing the data for the torus. If you want to fill your scene with life you will need to assign cores to each and every one of your nodes.

Empty Cores Are No Good

IMPORTANT:
A node without a core should not be attached to your graph. If your scene is beeing rendered and an "empty" node is encountered, the rendering routine will break at this point. The next figure will show what I mean.

empty_core_example.png

A graph with an empty node

The nodes in green color represent geometry nodes, whereas the red one indicates a node with a missing core. The numbers to the right are showing the order of traversal. The rendering traversal of the graph is depth-first, left-first (see section Actions). This will result in correct rendering of the terrain, but the house, car and trees will be skipped entirely! In such a situation the terminal will show you the following error message: "Recurse core is null, don't know what to do!"

Well, remember the family scene graph example a few pages (or a little bit of scrolling for the HTML version) ago? (see Nodes) If you tried it yourself, you realized that it didn't work and now we have seen why - because of missing cores! You could assign a group node, I'll present in the very next section, to every node. If you are doing so, the error message will not occur any longer, however in order to see anything, you need to add also some geometry. The upcoming tutorial will show you how to create nodes and cores and also how to animate your geometry.

All important cores will be introduced in the next chapter (Node Cores). However as real time graphics without any movements are quite boring, I will introduce the transformation and group core briefly here already.

Group core

The first one is the simplest of all of them: the osg::Group core. A group core does nothing in special it just makes it possible to have a node and attach several children to it. You see, this is the solution to our empty node problem from above. Like every other class derived from Field Container, cores too, need to be created via the static create() method of it's pointer class. This example demonstrate how you can create a node containing a group core

    NodePtr n = Node::create();
    // the group core is created just like the nodes were
    GroupPtr g = Group::create();

    beginEditCP(n);
        n->setCore(g);
    endEditCP(n);

That's all! The "setCore()" method is used to assign a core to the node. As explained in OpenSG Scenegraph Compared to Other Systems these cores can be referenced by as many nodes as you want to - and this is why it is called a scenegrah and not a scenetree ;)

The group core has no interesting methods as it does really nothing

Transform core

The osg::Transform core is most likely one of the most important. As you might expect this one is needed to move your geometry around. Creation, of course, is just the same as ever, but you have some more methods to play with, compared to the group core. Here is an example how you can create a node containing a transformation, which will translate all its children by five units along the z-axis

    NodePtr n = Node::create();
    TransformPtr t = Transform::create();
    
    Matrix m;
    m->setTranslate(Vec3f(0,0,5));
    
    // we want to modify our fresh transform object 
    // so we need to call begin-/ and endEditCP on it
    beginEditCP(t);
        t->setMatrix(m);
    endEditCP(t);
    
    beginEditCP(n);
        n->setCore(t);
    endEditCP(n);

If you now add children to that node they all will be translated by this transformaton you specified.

IMPORTANT:
I suppose that you expected it that way, but just to make sure you know: In OpenSG, like in most other scenegraph systems, transformations are only handed down to the children, but never are they passed to neighbours.

inheritance_example.png

A graph demonstrating the kind of data inheritance

Here we have two different transformation nodes A and B. Transformations are inherited by children only, so the terrain has transformation A assigned to it, where the house is transformed by B. The car and trees are not transformed at all, because there is no parent which has a transformation assigned to it.

Because we really need that kind of core to do anything other than boring stuff, I have introduced the transform core here. A detailed discussed about this and most other cores can be found in the next Chapter (Node Cores).

Tutorial - it's moving!

Well, let's have a look what we can do now. In theory we know, how we can execute code on a frame by frame basis (via the display callback) and we now can use transformations. That is wonderful, because by now we can realize our first animation! We already have a torus from the last tutorial (First Tutorial), so let us extend this one: The torus should turn around its own axis. Please use this tutorial as a starting point. If you have not completed it yourself you can find it here: progs/01firstapp2.cpp.

The first thing you have to do, is to add a global variable we will need for animation.

// add this line below the declaration of the SimpleSceneManager
// or anywhere else where it will be global
TransformPtr transCore;

This node will contain our transformation matrix, which will be altered every frame. Of course we do not need to make it global, but it is the easiest way for now... and the most efficient, too. There is another way down to the transform core: as we have an instance of the simple scene manager, we have access to the root, from there we have access to all the root's children, in this case to the transform node. From the node we can retrieve the core.

Let us continue:
Now please, in the "main" function locate the lines that say

That will be our whole scene for now : an incredible Torus
    NodePtr scene = makeTorus(.5, 2, 16, 16);

and replace them with the following code

NodePtr scene;
        
// create all that stuff we will need:
//one geometry and one transform node
        
NodePtr torus = makeTorus(.5, 2, 16, 16);
NodePtr transNode = Node::create();
        
transCore = Transform::create();
Matrix m;
        
// now provide some data...
        
// no rotation at the beginning
m.setIdentity();
        
// set the core to the matrix we created
beginEditCP(transCore);
    transCore->setMatrix(m);
endEditCP(transCore);
        
// now "insert" the core into the node
beginEditCP(transNode);
    transNode->setCore(transCore);
    // add the torus as a child to
    // the transformation node
    transNode->addChild(torus);
endEditCP(transNode);
        
// "declare" the transformation as root
scene = transNode;  

This piece of code will create a little scenegraph with a transformation as the root node and one child, which is the torus geometry.

By now you might be asking yourself "If you need a Core for every Node anyway, why is it so damn complicated to get one?". You will do the whole create Node, create Core, assign Core to Node process a lot in your own programs, and there are very few variations in it. Therefore versions after 1.2 add a convenience method makeCoredNode that helps creating Node and Core in one step. using it you can replace the above example with something shorter

NodePtr scene;
        
// create all that stuff we will need:
//one geometry and one transform node
        
NodePtr torus = makeTorus(.5, 2, 16, 16);
NodePtr transNode = makeCoredNode<Transform>(&transCore);     

Matrix m;
        
// now provide some data...
        
// no rotation at the beginning
m.setIdentity();
        
// now "insert" the core into the node
beginEditCP(transNode);
    // add the torus as a child to
    // the transformation node
    transNode->addChild(torus);
endEditCP(transNode);
        
// "declare" the transformation as root
scene = transNode;  

makeCoredNode does exactly what the name says: it creates a Node that has a Core of the type specified as the template parameter. If you need that Core you can pass a pointer to the Ptr to store the Core Ptr in. If you don't need that Core right away you just call the function without parameters. Again, as with the RefPtr, this is only available in versions after 1.2, so our examples will not use it.

If we now modify the matrix of the transformation node we will actually modify the position and/or rotation as well as the scaling of the torus. Just to make sure it works, I recommend to compile and execute the program. You should see the torus just like in the last tutorial. You also should be able to move the camera by pressing and holding the left mouse button while moving the mouse around. However we have more code but no more features than in the old tutorial - so let's change this now!

Locate your display() function - it should look like this

void display(void){
    mgr->redraw();
}

Insert the following code before the redraw method of the SSM is called

Matrix m;
        
// get the time since the application started
Real32 time = glutGet(GLUT_ELAPSED_TIME );
        
// set the rotation
m.setRotate(Quaternion(Vec3f(0,1,0), time/1000.f));
        
//apply the new matrix to our transform core
beginEditCP(transCore);
    transCore->setMatrix(m);
endEditCP(transCore);

Now again, compile and run (not you! The application ;-) )!

No difference? Nothing is moving? Try to move another window over your GLUT window and your torus will move. So what happened? The display callback is only called if the window needs to be redrawn - that is the case, if it is overlapped by some other window. Well, but that is not what we actually want, isn't it? We need the display method to be called at least about 25 times a second. That is where another callback comes into play: the idle callback function.

Jump to the setupGLUT() function where all the other callbacks are registered and add this one :

glutIdleFunc(display);

This tells GLUT to call the display function, whenever there is nothing to do. Now compile and run again and watch your torus turning around. By the way, the order in which the callbacks are registered does not matter.

You can find the full code here : progs/02movingTorus.cpp

Field Container

Now that we have seen how the very basic concepts of OpenSG work, it is time for some more background knowledge. In this section I will explain the field container concept to you. Even if you do not care about how things are realized and how they actually work, you should read this section as it is very essential to the way how OpenSG works. As I mentioned before, OpenSG was designed to handle multithreaded data in an easy way and that extends to clustering quite naturally. The developers decided to create containers which are protected against simultaneous access from more than one thread. With these field containers, the Ptrs were introduced as it was necessary to avoid working with standard pointers. Nearly every OpenSG specific class related to data storage is derived from FieldContainer. If you want to convince yourself how often this class is derived, then have a look at osg::FieldContainer. As I mentioned before: Every class that is derived from FieldContainer, needs to be created via the static create method! The return type is always classnamePtr

There are some more interesting aspects about the field containers. All containers are reflective, that is they provide information about themselves like the classname or a unique id. The following example shows a few operations that are possible on all field containers.

// this object could be any other node core, it
//would work just the same way

TransformPtr trans = Transform::create();
const Char8 *c = trans->getTypeName();
printf ("Typename: %s\n",c);

// This will print "Typename: Transform" to your terminal

const UInt32 id = trans->getTypeId();
printf("Type ID: %d\n", id); 

// And this will print "Type ID: 82"; or something else, the IDs are assigned at runtime
// and can change.

There are some other get functions that might be useful in certain situations. If you need to now more look them up here: osg::FieldContainer - usage should be as simple as above.

There is also a completly different way to create instances of field containers than create() : the "factory".

Field Container Factory

The factory can be very useful in some special cases. With the factory at your hand it is possible to create objects by using strings instead of the static class method create(). Here are some examples

//You know this
NodePtr n_usual = Node::create();

//This does the same thing in another way
FieldContainerPtr n_factory = ContainerFactory::the().createContainer("Node");

//it works with every type derived from Field Container
FieldContainerPtr g = Container::the().createContainer("Group");

This comes in very useful when writing a loader for example. Without this possibility you would have no choice but to write a huge if-then-else cascade, where you have to catch every possible type. It can also be used to conservatively use components of which you're not sure whether they exist (they might not have been linked to the program). If the FieldContainer you get back is NullFC, you know the class does not exist in the system.

Fields

But being able to create FieldContainers very generally is only a part of the solution, because the general FieldContainerPtr can obviously not give you access to any of the data fields in the concrete class. It's a base class, it can't know anything about the data its children add.

Or can it? Not directly of course, but using a little abstraction it can.

Data in OpenSG FieldContainers is not stored as simple variables, but as instances derived from the osg::Field class. As these Fields are very general, there is not much you can do with them, but enough to make them interesting. If you know the type you can downcast the generic Field into the concrete type. If you don't, you can access the Field's data in string form using the getValueByStr() and pushValueByStr().

Access to a FieldContainer's Fields is possible through its getField() method, which can take an index or a name to identify a Field. But how do you know which Fields the FieldContainer has, and what their names are?

Every FieldContainer can give you a descriptive structure that contains information about the Fields contained in the Container, called the osg::FieldContainerType. The FieldContainerType keeps an osg::FieldDescription for each Field in the FieldContainer. The FieldContainerType knows the number of Fields (and tells it via getNumFieldDescs()), and returns FieldDescriptions either per index (getFieldDescription()) or per name (findFieldDescription()).

These methods allow you totally generic access to any kind of FieldContainer known to the system only knowing its naming or having a pointer to an instance. This allows you to write programs that can handle any kind of FieldContainer, even the ones that have been added after the program was written. This can be used for generic input/output operations, and for generic user interfaces, among other things.

However, these are relatively advanced topics. In most of your own programs you will probably prefer to use the usual way of instanciation via create() and access the data directly. But as your programs become more complicated or more general (or both), remember that there is a very general framework here that can abstract a lot of the concrete details.

Single and Multifields

In OpenSG we have two different kinds of fields: single- and multifields. As the names are already telling, the osg::SField stores a single value whereas the osg::MField is comparable to an STL vector (i.e. a dynamically resizing array). SField and MField are template classes, and there are predefined instances for all the standard types in the system. These have names starting with SF and MF, e.g. SFNodePtr and MFNodePtr.

Let's start with a simple example. You've already seen the Node class. Nodes keep a bunch of children in a multi field called children and some other data, like a pointer to their parent Node in a single field called parent. Now assume you have the family scenegraph described in Nodes.

    NodePtr grandpa = Node::create();
    NodePtr aunt    = Node::create();
    NodePtr mother  = Node::create();
    NodePtr me      = Node::create();

    setName(grandpa, "Grandpa");
    setName(aunt,    "Aunt");
    setName(mother,  "Mother");
    setName(me,      "Me");
    
    beginEditCP(grandpa);
        grandpa->addChild(aunt);
        grandpa->addChild(mother);
    endEditCP(root);

    beginEditCP(mother);
        mother->addChild(me);
    endEditCP(mother);

    NodePtr n;  
    
    n = me->getParent()->getParent();           // sets n to grandpa
    
    cout << "Grandpa has " << grandpa->getChildren().size() << " children."
    
    n = grandpa->getChildren()[0];             // sets n to aunt
    n = grandpa->getChildren().getValue(1);    // sets n to mother

This shows you the basic ways to get data from FieldContainers. They have accessor methods that are called get<Fieldname> for each Field. Single-value fields just return their value that can be used directly. For multi-value fields a reference to the actual MField is returned, which acts pretty much like an STL vector, for example it supports the [] operator to access elements, and the size() method to find out how many elements it has, as well as iterator-based access. For consistency reasons the MFields also have a getValue() method, which acts just like the [] operator.

Of course the Field also support changing values

    NodePtr uncle = Node::create();
    setName(uncle, "Uncle");
   
    beginEditCP(grandpa);
    grandpa->getChildren().push_back(uncle); // add uncle to grandpa's children
    endEditCP(grandpa);
    
    beginEditCP(uncle);
    uncle->setParent(grandpa);  // set uncle's parent to grandpa
    endEditCP(uncle);

    cout << "Grandpa's children: ";
    for(MFNodePtr::iterator it = grandpa->getChildren().begin();
        it != grandpa->getChildren().end(); ++it)
        cout << getName(*it) << " ";

For SFields the setValue() method is all it needs. For MFields the standard vector methods like push_back apply. For consistency reasons they also have a setValue method that takes the data to set and the index of the element to change.

Note that these are just examples, for child/parent manipulation it's better to use the special Node methods that you've seen before like addChild() or subChild(), as they automatically take care of the parent field consistency.

Editing Field Containers

Now it is time to solve another mystery you encountered on previous tutorials: "begin- /endEditCP() blocks". As I mentioned before at the beginning of this chapter, we need these begin-/endEditCP brackets every time we want to modify an OpenSG specific object. To be exact we do only need these when modifing an object that is derived from osg::FieldContainer. Vectors or Points for example are not derived from FieldContainer and thus need no begin-/endEditCP() block. By the way CP stands for "Container Pointer"...

Alright, so as we saw before we need to start with a beginEditCP(object), where object is the one we want to edit, of course. Then one or more modifications should follow and finally we are ending up with endEditCP(object). Here is another example how to do it right:

NodePtr n = Node::create();

beginEditCP(n);
{
    GroupPtr g = Group::create();
    NodePtr c1 = Node::create();
    NodePtr c2 = Node::create();
    
    GeometryPtr g1 = generateSomeGeometry();
    GeometyrPtr g2 = generateSomeOtherGeometry();
    
    beginEditCP(c1);
    beginEditCP(c2);
        c1->setCore(g1);
        c2->setCore(g2);
    endEditCP(c2);
    endEditCP(c1);
    
    n->setCore(g);
    n->addChild(c1);
    n->addChild(c2);
}
endEditCP(n);

Noticed something? I did some things different than before. Your begin/end block can span more lines of code than these that actually modify the object and these blocks must not be seperate. As you can see I have two blocks (editing c1 & c2) "overlapping" while these are inside the block which modifies n. The additional {}-brackets are used for cosmetic reasons only, they are not neccessary, but they can be extremly useful, if you have more complex graphs.

Maybe you wonder what these blocks are for anyway. Well and again it is multithread safety... by using begin- and endEditCP you tell the system when something is going to be changed. Normally every thread would need a full copy of the data, but that would result in very high memory consumption, so in OpenSG much of the data is shared. The multifields I just introduced are shared for example. Only when data is changed it will be copied to the current thread. The changes are recorded in a change list which will be used to syncronize the threads again at a later time. Have a look at chapter Multithreading for more details about multithreading. This is roughly why the system needs to be informed when something is going to be changed.

So what happens, if you forget a begin-/endEditCP block? If you are running a single threaded application on a single machine it will work in most cases, but you should not treat this as an excuse to leave them all out. I said in most cases, but you have no garantee that it will work at all, as this is not the way OpenSG is meant to be used! I know that it is sometimes a bit annonying to write half a mile of code, but please make the editCPs your friends ;-) You will be rewarded whenever you want to drive your application with multiple threads and/or in cluster mode.

But even more important: what happens if you skip the editCP blocks if you are using mutiple threads or are running a cluster? Depending on how violently you skipped the editCP blocks, your modifications will occur in one thread only and that is most likely not what you want. No matter what actually happens to your data, you have a good chance crashing your application. So if you are using multiple threads or a cluster you have to pay special attention to your editCP blocks! If your applications are crashing for some strange reason you should first check if you have missed such a block. Especially clusters are pretty sensitive to this, 90% of cluster problems are inconsistent or plainly forgotten begin-/endEditCP blocks.

begin- endEditCP with Field Masks

By invoking beginEditCP(object) you tell the system that you want to change everything. You can specify explicitly which attributes you want to modify. This will save some computing power, although it is often not necessary, when initializing the graph. In performance critical sections this will be much more important. For example, lets say we have a geometry node representing water, which we need to update every frame. We need to update the positions and maybe the nomals too, but color, texture coordinates and other geometry attributes stay untouched. If you just call beginEditCP the system can't know what you change, so it has to assume you changed everything. For a cluster application that means every bit of data has to be transfered across the network, even if it didn't change. The consequences are less severe for non-cluster applications, but still serious.

Here is the same example from above, but this time with correct specification of the changing attributes

NodePtr n = Node::create();

beginEditCP(n, Node::ChildrenFieldMask | Node::CoreFieldMask);
{
    GroupPtr g = Group::create();
    NodePtr c1 = Node::create();
    NodePtr c2 = Node::create();
    
    GeometryPtr g1 = generateSomeGeometry();
    GeometyrPtr g2 = generateSomeOtherGeometry();
    
    beginEditCP(c1, Node::CoreFieldMask);
    beginEditCP(c2, Node::CoreFieldMask);
        c1->setCore(g1);
        c2->setCore(g2);
    endEditCP(c2, Node::CoreFieldMask);
    endEditCP(c1, Node::CoreFieldMask);
    
    n->setCore(g);
    n->addChild(c1);
    n->addChild(c2);
}
endEditCP(n, Node::ChildrenFieldMask | Node::CoreFieldMask);

NOTICE:
If you want to modify more then one field at once you can use the binary or operator ( "|" ) to join the field masks.

With that the code becomes even a bit longer, but that should not scare you to use it in performance critical parts of your application!

If you want to specify the fields to be changed you need to know the appropriate field mask. That is not as hard as it sounds, because the names are very easy to "guess", here are a few examples

Set the core of a node Node::CoreFieldMask
Add or sub a child Node::ChildrenFieldMask
Set the matrix of a transform core Transform::MatrixFieldMask
Set the choice field of a switch Switch::ChoiceFieldMask

You see it is very easy in most cases. In general it is

    ClassToBeEdited::FieldToBeEditedFieldMask
If you are unsure about a certain field mask then have a look at the tutorials if that what you want to do was used before. If not you have to look it up in the doxygen class documentation. You can find the available masks at the "Static Public Attributes" section.

More Convenient FieldContainer handling

People new to OpenSG see a lot of the described specifics as pretty tedious. You get used to it pretty quickly, but it does make code pretty long. To simplify and shorten it a couple new features were added after the 1.2 release, that simplify storing and changing FieldContainers and Nodes.

FCEditor and FCEdit

osg::FCEditor is a simple helper object that wraps beginEditCP and endEditCP in its constructor and destructor, coupling the FieldContainer changes to the lifetime of the object. Let's rewrite last section's example using CPEditor:

{
    NodePtr n = Node::create();
    CPEditor ned(n, Node::ChildrenFieldMask | Node::CoreFieldMask);

    {
        GroupPtr g = Group::create();
        NodePtr c1 = Node::create();
        NodePtr c2 = Node::create();

        CPEditor c1ed(c1, Node::CoreFieldMask);
        CPEditor c2ed(c2, Node::CoreFieldMask);

        GeometryPtr g1 = generateSomeGeometry();
        GeometyrPtr g2 = generateSomeOtherGeometry();

        c1->setCore(g1);
        c2->setCore(g2);

        n->setCore(g);
        n->addChild(c1);
        n->addChild(c2);
    } // as c1ed and c2ed are destroyed here, they calls endEdit
} // as ned is destroyed here, it calls endEdit

Looks a bit more friendly, doesn't it? The only slight annoyance is that you have to come up with a name for the CPEditor object. Don't despair, CPEdit relieves you from even that little chore:

{
    NodePtr n = Node::create();
    CPEdit(n, Node::ChildrenFieldMask | Node::CoreFieldMask);

    {
        GroupPtr g = Group::create();
        NodePtr c1 = Node::create();
        NodePtr c2 = Node::create();

        CPEdit(c1, Node::CoreFieldMask);
        CPEdit(c2, Node::CoreFieldMask);

        GeometryPtr g1 = generateSomeGeometry();
        GeometyrPtr g2 = generateSomeOtherGeometry();

        c1->setCore(g1);
        c2->setCore(g2);

        n->setCore(g);
        n->addChild(c1);
        n->addChild(c2);
    }
}

CoredNodePtr - Core and Node under one roof

CPEditor takes care of the begin/endEditCP, but you still need to handle two separate classes, the Node and the Core. However, you will rarely have a node without a core or a core without a node, in most cases they appear together, thus it is logical to have a wrapper class that takes care of both. As the possibilities of C++ are not unlimited this is only possible to some extend, but the following concept can reduce the code length somewhat.

The class that may help you out is osg::CoredNodePtr. This class provides a node as well as a core for you. osg::CoredNodePtr is a templated class, so you need to define the type of core you want to use. This is how you would define a cored node pointer that will manages a geometry core

    CoredNodePtr<Geometry> cnpGeo = CoredNodePtr<Geometry>::create();

If you use a specific type of CNP more than once, it makes sense to typedef it.

If you are working with cored pointers you have to keep in mind, that this object behave similar to a core, but it does have a node 'around it'. That means the -> operaror, as well as the = operator refer to the core. The following example shows how you can assign a geometry core to the cnpGeo object we created above

    // geo is of type osg::Geometry and contains
    // some geometry
    cnpGeo = geo;

If you need the node itself for any reason, you can access it with

    NodePtr n = cnpGeo.node();

In addition to wrapping the Node/Core separation, CNPs also handle reference counting that same way RefPtr<> does. Thus the same limitations and additional features described in RefPtr - as smart as it gets apply.

Well, next here is the code for an little example showing the cored node pointers in action. You can find the file in progs/examples/00coredPointer.cpp

I will show the createScenegraph() function only, because that is where all interesting changes are.

typedef CoredNodePtr<Group> GroupNodePtr;
typedef CoredNodePtr<Geometry> GeometryNodePtr;

NodePtr createScenegraph(void)
{
    //create the torus geometry (core and geometry)
    GeometryNodePtr torus = GeometryNodePtr::create();
    torus = makeTorusGeo(0.5,2,8,12);

    //create box
    GeometryNodePtr box = GeometryNodePtr::create();
    box = makeBoxGeo(0.5,0.5,0.5,1,1,1);

    //create the group node and core
    GroupNodePtr root = GroupNodePtr::create();
    root = Group::create();

    //add the torus and box to the group node
    beginEditCP(root);
    root.node()->addChild(torus);
    root.node()->addChild(box);
    endEditCP(root);

    addRefCP(root.node()); // keep the root node around, even after the CNP is destroyed
    
    return root.node();
}

Well, you can try how much lines it would take, if using the usual way... I am sure that will be about twice as long.

Notice: I personally started learning OpenSG, before these wrapper class existed, so I am from the "old school". Some things will work quite different, so to not confuse you and to not leave out the people using the old version, I decided to use the usual concept of editing field containers (i.e. the long version) as this is the more righteous way ... ;-)

Standard Geometry

OpenSG comes with some build-in functionality to generate standard geometries. One of these was already used in the first tutorial: a torus. As these geometries are very easy to generate and used often for testing purposes, I will list them here for reference

makePlane(xsize, ysize, resHor, resVer);
makeBox(xsize,ysize,zsize, resHor, resVer, resDepth);
makeCone(height, bottomRadius, resSides, doSide, doBottom);
makeCylinder(height, radius, resSides, doSide, doTop, doBottom);
makeTorus(innerRadius, outerRadius, resSides, resRings);
makeSphere(resDepth, radius);
makeLatLongSphere(resLat, resLong, radius);
makeConicalFrustum(height, topRadius, bottomRadius, doSide, doTop, doBottom);

The methods themself should be selfexplaining - they do that what they are supposed to do. All sizes and radii are of type Real32, where as all resolution parameters (beginning with res) are Int16. Parameters beginning with do are of type bool, they enable or disable the rendering of the appropriate parts. The following example generates a 10 unit height cone with 32 edges with a top but without a bottom

    NodePtr n = makeCone(10, 5, 32, true, false);

Always be careful with the resolution parameters. Big values can lead to really huge geometry data. Especially the sphere resolution depth should be used moderatly, because this one uses a recursive subdivison algorithm proportional to 4^res, meaning a value of four is already a pretty smooth sphere and a value of 64 will kill your machine (unless you have enough memory for about 10^38 polygons)! For better control use the LatLongSphere, it has nicer texture coordinates anyway.

All of these geometry-generating methods return a NodePtr ready to be inserted into a scenegraph. Often you have already existing nodes and are only in need of the geometry. In this case you do not need to throw your node away as there are the same functions that return a GeometryPtr. You only need to append a "Geo" to the above functions.

//Another way to generate the cone
NodePtr n = Node::create();

GeometryPtr g = makeConeGeo(10, 5, 32, true, false);
beginEditCP(n, Node::CoreFieldMask);
    n->setCore(g);
endEditCP(n, Node::CoreFieldMask);

Please notice that in this special case it is not very useful to use the makeConeGeo Version, as this does the same as the example before and is much longer in code, but in other circumstances it can be just the other way round.

Tutorial - More than a torus

This time we are capable of doing some more interesting stuff than displaying and rotating a torus. In this tutorial we will build a complete house with roof and chimney. And maybe you can build even more things into it ;-). Okay so let us think about it, before we begin to write code...

house_wire.png

'Wireframe' of our house

This is a frontal wireframe view of the house we want to build. Because we are lazy we are cheating a bit when it comes to building the roof. We have no appropriate standard geometry so we use a box with the correct length and rotate it by 45 degrees. So the diagonal length of the box must be as long as the top side of our house is: The diagonal length must therefore be twenty and our old friend Pythagoras tells us that the edge length have to be approximately 14.14. The chimney will be a cylinder with height 10 and radius 1, just stuck into the roof. Please notice that I am not claiming to do excellent modeling work here.

We will use a very similar framework than we did before, but this time we write a method which will create a scenegraph and return a NodePtr. Of course that is not really necessary, but it is easier to read. In larger projects it could even be useful to put this method into its own file.

Like we did in the last tutorial, exchange the line that says

    NodePtr scene = makeTorus(.5, 2, 16, 16);
with
    NodePtr scene = createScenegraph();

Now add this function at the beginning of your file (It has to be defined before the main function where it is used!)

//File : 03MoreThanATorus.cpp

//This function will create our scenegraph
NodePtr createScenegraph(void)
{
    // First we will create all needed geometry
    // the body of the house
    NodePtr houseMain = makeBox(20,20,20,1,1,1);
    
    // now the roof
    NodePtr roof = makeBox(14.14, 14.14, 20, 1, 1, 1);
    
    // and the chimney - we have the top and sides generated
    // but we have no need for the bottom (it is inside the house)
    NodePtr chimney = makeCylinder(10,1,8,true,true,false);

    // Now we create the root node and attach the geometry nodes to it
    NodePtr n = Node::create();
    beginEditCP(n, Node::CoreFieldMask | Node::ChildrenFieldMask);
        n->setCore(Group::create());
        n->addChild(houseMain);
        n->addChild(roof);
        n->addChild(chimney);
    endEditCP(n, Node::CoreFieldMask | Node::ChildrenFieldMask);
    return n;
}

Compile and execute the application - and while doing so, think about what we will see!

If you zoom out a bit (pressing the right mouse button while moving) the only thing you will see is a single box. That is because the smaller box as well as the cylinder are inside of the big box. So next we need to translate these to the correct positions.

//File : 03MoreThanATorus2.cpp

//This function will create our scenegraph
NodePtr createScenegraph(){
    // we will use the variable to set our transform matrices
    Matrix m;
    
    // First we will create all needed geometry
    // the body of the house
    NodePtr houseMain = makeBox(20,20,20,1,1,1);
    
    // now the roof
    NodePtr roof = makeBox(14.14, 14.14, 20, 1, 1, 1);
    
    // we translate the roof to the correct position
    TransformPtr tRoof = Transform::create();
    beginEditCP(tRoof, Transform::MatrixFieldMask);
        m.setIdentity();
        m.setTranslate(0,10,0);
        m.setRotate(Quaternion(Vec3f(0,0,1), 3.14159/4));
        
        tRoof->setMatrix(m);
    endEditCP(tRoof, Transform::MatrixFieldMask);
    
    NodePtr roofTrans = Node::create();
    beginEditCP(roofTrans, Node::CoreFieldMask | Node::ChildrenFieldMask);
        roofTrans->setCore(tRoof);
        roofTrans->addChild(roof);
    endEditCP(roofTrans, Node::CoreFieldMask | Node::ChildrenFieldMask);
    
    // and the chimney - we have the top and sides generated
    // but we have no need for the bottom (it is inside the house)
    NodePtr chimney = makeCylinder(10,1,8,true,true,false);
    
    //now we translate the chimney
    
    //create the transform core
    TransformPtr tChimney = Transform::create();
    beginEditCP(tChimney, Transform::MatrixFieldMask);
        m.setIdentity();
        // -5 along the x-axis and 2.5 along the z axis
        // translates the chimney away from the center
        // 15 along the y-axis translates the chimney to fit on top
        // of the big box (have a look at the figure above2,5
        m.setTranslate(-5,15,2.5);
        
        tChimney->setMatrix(m);
    endEditCP(tChimney, Transform::MatrixFieldMask);
    
    //insert the transform core into the node
    NodePtr chimneyTrans  = Node::create();
    beginEditCP(chimneyTrans, Node::CoreFieldMask | Node::ChildrenFieldMask);
        chimneyTrans->setCore(tChimney);
        chimneyTrans->addChild(chimney);
    endEditCP(chimneyTrans, Node::CoreFieldMask | Node::ChildrenFieldMask);

    // Now we create the root node and attach the geometry nodes to it
    NodePtr n = Node::create();
    beginEditCP(n, Node::CoreFieldMask | Node::ChildrenFieldMask);
        n->setCore(Group::create());
        n->addChild(houseMain);
        n->addChild(roofTrans);
        n->addChild(chimneyTrans);
    endEditCP(n, Node::CoreFieldMask | Node::ChildrenFieldMask);
    return n;
}

Have a close look at the transformation matrices. Maybe you wonder how to figure out the correct translation values. Well, you need to know where the pivot is. When using OpenSG standard geometry the pivot is at the geometric center. The next figure shows the intial situation, when the geometry is created, but yet not translated.

In general the OpenSG standard geometries are modeled after the ones defined in the VRML97 ISO standard.

house_wire_init.png

Initial situation before translating

The red cross marks the pivot for all three objects. If we want to set the correct y (=height) value for the chimney we need to translate it by half the height of the big box (which is 10) and the half height of the chimney itself (which is 5) and you see, we need to translate it by 15 units along the y-axis.

The next one shows how we need to translate the roof. Notice that I left out the chimney here for didactical purposes ;-)

house_wire_roof.png

Translation and rotation of the roof

First we need to translate the roof 10 units along the y axis, so that the pivot of the roof lies exactly on top of the houses body. Next we rotate the roof by 45 degrees

Images and Textures

Every realtime rendering system can of course load images and use these as a texture for your models. OpenSG would be not a real scenegraph system if it were not able to load several image formats. Please notice that you need to enable support for the image formats you want to use when configuring the OpenSG library (See : Installation on Linux). If you are using a precompiled package, all available image formats are enabled.

The supported image formats are png, jpeg, tiff, gif, ppm, rgb and sgi. Others may follow in future versions, but actually you can do all you need with the provided formats. Images in OpenSG are stored in a field container class called "Image". But this class is not used directly to texture your models. In most cases you will create an instance of "SimpleTexturedMaterial" to which an image can be assign to texture your models.

First we have a look at how images can be loaded. This is very easy, as you can see here:

//create a new image object
ImagePtr img = Image::create();

// and now we load the image from disk
img->read("myVeryNiceImageFile.jpg");

The good thing is that the image loader is pretty smart, as he automatically detects the filetype by the file extension and thus one method can load all formats which are supported - there is no need for loadPNG, loadJPG etc.

Of course you can generate your own image by hacking code.

ImagePtr img = Image::create();
UChar8 data[] = {0,0,0, 50,50,50, 100,100,100, 255,255,255};

beginEditCP(img);
    img->set( Image::OSG_RGB_PF, 2, 2, 1, 1, 1, 0, data);
endEditCP(img);

These are the paramters of the set method

    set (
        UInt32 pixelFormat,
        Int32 width,
        Int32 height = 1,
        Int32 depth = 1,
        Int32 mipmapCount = 1,
        Int32 frameCount = 1,
        Time frameDelay = 0.0,
        const UInt8 *data = 0,
        Int32 type = OSG_UINT8_IMAGEDATA
    )

As you see most of the paramters have default values assigned to them. If you want to create a simple two dimensional image you actually need to set the pixel format, width, height and the data only, all other default values are good for that.

pixelFormat
The first paramter of the set method defines the pixel format. The two most important are OSG_RGB_PF and OSG_RGBA_PF. If you are using RGB pixel format, you need to provide three components for each pixel, so in our example we have four pixels from black to white each consisting of three values for the color channels Red, Green and Blue. RGBA adds a fourth component "alpha" which defines the opacity of the pixel. A value of zero is a fully transparent (i.e. invisible) pixel where as 255 is not transparent at all.

width, height, depth
These parameters define the size of the image. The image class is capable to store 1D, 2D as well as 3D images. The dimensions you do not need should be set to one (not zero!). That is, a 1D image should have the width of your choice and height and depth set to one.

mipmapCount
If you do not know what mipmapping actually is, then leave this paramter as it is! ;) If you want to know more about mipmapping, have a look at http://www.sgi.com/software/opengl/advanced98/notes/node35.html.

frameCount, frameDelay
These parameters are used for animated textures. The frameCount defines how much images will be used and the delay says where to start. A setting of 0.0 here means of course to start from the beginning.

data
This one is carrying the actual data. Please notice that you have to pay special attention to this: The number of arguments you pass here must be exact. You will need

    width*height*depth*frameCount*{3,4}
values. The last digit have to be three in RGB and four in RGBA mode. If this number is not exact, your application will crash or at least it will do something different as you want.

The data is stored row after row beginning at the bottom left corner, just like OpenGL! The following figure illustrates this:

image_storage.png

The direction in which the pixels are stored in memory

You need to remember this, whenever you provide the image data "by hand", if you do not your image is displayed mirrored.

Tutorial - Using textures

Now we will learn how we can assign textures to geometry, making our scenes even more beautiful ;) Please use the framework progs/00framework.cpp as a starting point.

You need to add two new include files in order to load and display images used as a texture

#include <OpenSG/OSGSimpleTexturedMaterial.h>
#include <OpenSG/OSGImage.h>

The following code describes the createScenegraph() function, which will create a simple textured box.

    //File : 04Textures.cpp
    
    //create the geometry which we will assign a texture to
    GeometryPtr boxGeo= makeBoxGeo(10,10,10,1,1,1);
    
    //Load the image we want to use as a texture
    ImagePtr image = Image::create();
    image->read("images/bricks.jpg");
    
    //now we create the texture that will hold the image
    SimpleTexturedMaterialPtr tex = SimpleTexturedMaterial::create();
    beginEditCP(tex);
        tex->setImage(image);
    endEditCP(tex);
    
    //now assign the fresh texture to the geometry
    beginEditCP(boxGeo, Geometry::MaterialFieldMask);
        boxGeo->setMaterial(tex);
    endEditCP(boxGeo, Geometry::MaterialFieldMask);
    
    // Create the node that will hold our geometry
    NodePtr n = Node::create();
    beginEditCP(n, Node::CoreFieldMask);
        n->setCore(boxGeo);
    endEditCP(n, Node::CoreFieldMask);
    
    return n;    

Just zoom out a bit and turn the camera around and you will see a wonderful textured cube! Of course there are a lot more properties that can be set in the SimpleTexturedMaterial object, but we will have a closer look at that in Chapter Materials.

Notice:
If you create your own geometry (not the OpenSG standard geometry) then you have to supply all texture coordinates - these are generated automatically when using standard geometry, but not if you are creating your own!

Loading and saving of scenes

The next interesting and important topic is loading and saving of models and whole scenes. OpenSG can load some more or less common formats:

As far as I know VRML97 and OBJ are the most important formats as nearly every 3D modeling package can at least export one of them. BIN is the OpenSG native binary format and can be very useful for scenes that are pretty stable and that you need to load often, as it loads very fast. You can find more on modeling packages here : Using Models from Modeling Packages.

Loading scenes and models from disk is quite easy. Normally a simple

    NodePtr n = SceneFileHandler::the().read(filename);
will do. If you have, let's say a VRML-file, the generic loader will automatically select the appropriate loader. Like when loading images, you can use one and the same command for loading all supported filetypes.

As you can see, the return type is a NodePtr - if the loading process was not successfull for some reason "NullFC" is returned. It might be a good idea to check against the success of loading, else you might crash you application.

Saving a scene is as nearly as simple, if you're not using version 1.2 or older:

    SceneFileHandler::the().write(n, filename);
. In 1.2 it takes a little more than that, see below.

Tutorial - loading and saving

Again, take the 00framework.cpp file as a starting point. And again you have to add a new include file:
    #include <OpenSG/OSGSceneFileHandler.h>

Insert the following code into the createScenegraph() method

    NodePtr n = SceneFileHandler::the().read("data/terrain.wrl");
    return n;

Easy, isn't it? However, sometimes problems occur when loading scene from disk, especially in version 1.2. For more information about problems with loading see chapter Using Models from Modeling Packages.

Well, now we implement the possibility to save the scene by pressing a key. Three changes are necessary:

1.
At first we need to register a new callback function which will listen to keyboard input. Add

    glutKeyboardFunc(keyboard);
to the setupGLUT function

2.
Add the following function somewhere before setupGLUT.

void keyboard(unsigned char k, int , int ){
    switch(k){
        case 's':
        
            // there were some changes in the interface since version 1.2.0
#if OSG_MINOR_VERSION > 2
            // this is the cvs version or version 1.3+
           
            SceneFileHandler::the().write(scene, "data/output.bin");
#else
            // this code applies to version 1.2
            FILE* outFile = fopen("data/output.bin", "wb");
            if(outFile == NULL){
                cout << "File could not be created!" << endl;
                return; 
            }
            //create the writer object
            BINWriter writer(outFile);
            
            //write the file now
            writer.write(scene);
#endif
            
            cout << "File written!" << endl;
            break;
    }
}
Notice:
There were some changes in the interface of the BINWriter class since version 1.2.0. This is the reason for using these ugly define statements.

3.
Additionally you have to add "<OpenSG/OSGBINWriter.h>" to your list of files to include

Exercises

Ex. Transformations

Please recall the second tutorial (Tutorial - More than a torus). We translated the roof with the following transformation matrix

beginEditCP(tRoof, Transform::MatrixFieldMask);
    m.setIdentity();
    m.setTranslate(0,10,0);
    m.setRotate(Quaternion(Vec3f(0,0,1), 3.14159/4));
        
    tRoof->setMatrix(m);
endEditCP(tRoof, Transform::MatrixFieldMask);

What would happen if we swap the "setTranslate()" and the "setRotate()" commands? After you found out: why did it happen that way? Can you replace both of them with a single setTransform()?

Ex. Loading

Modify the loading/saving tutorial (Loading and saving of scenes) in that way, files can be loaded from the command line. If you type
    ./05loading file1.wrl file2.wrl file3.wrl
the application should load the three specified files. Also check if the file was successfully loaded and if not throw an appropriate error (the application should continue to run!).

Next Chapter: Node Cores


Generated on Thu Aug 25 04:55:58 2005 for OpenSG by  doxygen 1.4.3