wiki:Tutorial/OpenSG2/Basics

<< Previous Chapter: First Application Tutorial Overview Next Chapter: Node cores >>

Basics

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 SimpleSceneManager 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:

  • math using OpenSG classes
  • creating new nodes and cores
  • FieldContainers
  • Images
  • Loading and saving

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 optional U stands for unsigned.
RealN Floating point with either N = 32 or 64 bits precision
Fixed32 Fixed point number with 32 bits

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, fx}

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.

b Int8
ub UInt8
s Int16
us UInt16
f Real32
d Real64
fx Fixed32

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'd 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)
    std::cout << "these vectors are equal" << std::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 main difference is that points are intended to denote a position in space, whereas vectors denote a direction and (optionally a magnitude). It really only makes a difference for the 3 component vectors and points when applying matrices. In that case Pnt3N are treated as if the last component (w) was 1 and for Vec3N it is treated as 0.

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 with three or four components, where the fourth one 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

"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 coordinate 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 because
// the last component of the vector is treated as 0.

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

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

Apart from the mult function there is also multFull which should be used when the matrix contains a projection part, i.e. when the last row is different from (0, 0, 0, 1). Since this is only the case for few matrices you can safe a few cpu cycles by using mult most of the time or if you'd rather err on the side of caution always use multFull.

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!

"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 scale at once, as these share no components. However setting both, scale and rotation will yield a result you do not want in most cases. You need then to multiplicate two seperate matrices correctly...

Note that the matrix figure above has a typo: labels Rxy and Rxz must be swapped.

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 Quaternion 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 excellent 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.

"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 degrees, 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 objects in a scenegraph. In OpenSG the nodes are describing the hierarchy of the graph only! Here is a simple graph we want to build:

"A family scenegraph"

//First, we create all the nodes we need
NodeRecPtr grandpa = Node::create();
NodeRecPtr aunt    = Node::create();
NodeRecPtr mother  = Node::create();
NodeRecPtr 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
grandpa->addChild(aunt);
grandpa->addChild(mother);

mother->addChild(me);

// for now don't worry about the next line
commitChanges();

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 SimpleSceneManager 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 unusual 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 problem 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 the Appendix on Doxygen? 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 following two topics that are closely related:

Reference Counting - or: cleaning up after yourself

One of the problems of the C++ language is keeping track of and deallocating memory. Other languages like Java solve this through 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 management using new and delete. It's the program's responsibility 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 characterization 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 and 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 - excluding raw C++ pointers) to it. If this counter goes to zero, the program can't access the object anymore (there are no pointers to 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 OpenSG passes arguments to functions as raw C++ pointers and only when storing objects for a period longer than the current function call it uses pointer types that modify the reference count.

A second big issue with reference counting are circular pointers, i.e. suppose you start at some object and follow one of its pointers to the next object and then one of that object's pointers and so on, if you come back to the object you started at (or any of the objects you already visited for that matter) you have a circle. The easiest case is of course two objects each having a pointer to the other:

// this is just pseudo code to illustrate the example
SomeObjectRecPtr a = SomeObject::create();
SomeObjectRecPtr b = SomeObject::create();

a->setPointer(b);
b->setPointer(a);

// at this point the reference count of both objects is 2
// now we get rid of the "external" pointers a and b:

a = NULL;
b = NULL;

// at this point there is no way the application can access the objects
// a and b pointed to, so they should be destroyed.

// However, both objects still have a reference count of 1!

The above situation is a big problem and if both objects are actually if the same type there is not a good solution available. Fortunately, in practice most of the time the objects have different types and we can just have one of the pointers be "weak", i.e. the pointer will not keep the object from being destroyed. So now we have a pointer that points to an object that can be destroyed at any time; how do we know if the pointer still points to a live object or if it was already destroyed? Weak pointers will become NULL if the object they point to is destroyed, so we can simply test if the object is still alive by testing if the pointer is different from NULL:

NodeWeakPtr n;

if(n != NULL)
{
    std::cout << "n points to a live Node" << std::endl;
}

NOTE: This only applies for single threaded scenarios, in the presence of multiple threads the object pointed to by n might be destroyed after the if condition and before the body of the if is executed. You still need to use appropriate programming techniques to prevent race conditions when sharing resources in multiple threads.

ADVANCED: For those curious how WeakPtr can know if it points to a live object or not, here is a short explanation of how it works internally. Every object has not only the regulare reference count but also a weak reference count, that is simply the number of weak pointers pointing to that object. If the reference count drops to zero, but there are weak pointers the object is not destroyed, but all the pointers it holds are set to NULL (therefore reference counts of pointed-to objects are decremented). Comparing a WeakPtr actually tests if the reference count of the pointed-to object and if it is zero, the pointer is treated as a NULL pointer.

Pointers and how to choose the right variant

OpenSG comes with a number of pointer types that server different purposes, however most of the time you will only have to deal with one kind, the RecPtr. For every type derived from FieldContainer there is a corresponding RecPtr, e.g. FieldContainerRecPtr or NodeRecPtr. These should be used whenever you need to hold a pointer to an object and they are designed to interact nicely with raw C++ pointers and the usual syntax, so for example the following is fine:

void someFunc(Node *node);

NodeRecPtr n0 = Node::create();
NodeRecPtr n1 = Node::create();

n0->addChild(n1);   // operartor->()  works as usual

someFunc(n0);       // conversion to raw pointer

The one thing that does not work seamlessly is the casting of RecPtr? types (that is a language limitation), so instead of static_cast and dynamic_cast you will have to use the functions static_pointer_cast and dynamic_pointer_cast:

// assume there is a NodeRecPtr n pointing to a Node with a NodeCore set on it

NodeCoreRecPtr  nc = n->getCore();   // get the core
TransformRecPtr t  = dynamic_pointer_cast<Transform>(nc);

if(t != NULL)   // test if the cast succeeded
{
    std::cout << "Node has a Transform core.\n";
}

The other type of pointer you are likely to encounter when browsing some code or reading the doxygen documentation is TransitPtr?. These are used as return type from functions that create objects (also knowns as factory functions). It is good pratice to follow this example even if at times it may appear inconvenient, as TransitPtr? are not convertible to raw C++ pointers. To understand the reason for this (intentional) limitation let's have a look at the next example:

Node *factoryFunc(void)
{
    NodeRecPtr  node  = Node ::create();
    GroupRecPtr group = Group::create();

    node->setCore(group);

    return node;
}

Node        *n0 = factoryFunc();  // A1
NodeRecPtr   n1 = factoryFunc();  // B1

When factoryFunc returns, the reference count of the object pointed to by node drops to zero (because node goes out of scope and is destroyed), therefore n will point to an already destroyed object. Please also note that line B1 is not safe either as node may be destroyed before n1 is constructed, so for a (very brief) period the reference count could drop to zero again.

If on the other hand factoryFunc would have looked like this:

NodeTransitPtr factoryFunc(void)
{
    NodeRecPtr  node  = Node ::create();
    GroupRecPtr group = Group::create();

    node->setCore(group);

    return NodeTransitPtr(node);
}

Node        *n0 = factoryFunc();  // A2
NodeRecPtr   n1 = factoryFunc();  // B2

The first call on line A2 would have caused a compile time error, while line B2 would have worked perfectly fine and avoided the "pointer to a destroyed object" issue. Now, you might be wondering why we don't simply use NodeRecPtr as return type for factortyFunc? Recall that NodeRecPtr is implicitly convertible to Node * and therefore line A2 above would have compiled, resulting in exactly the same problem as in the initial example.

In rare cases you might also see WeakPtr used, although that usually happens inside internal code and you rarely need to worry about it. Their primary use is to prevent circular pointers as discussed above (here). Except for their special behavior with respect to reference counting they can be used just like RecPtr.

At this point you should have all the necessary knowledge about pointers to write single threaded OpenSG applications; the next section will quickly introduce the remaining pointer types for completeness sake. However, these explanations will contain references to concepts that have not been introduced yet. More details on these pointer types will be given in the chapter on Multithreading? after all the relevant concepts are introduced. If you are new to OpenSG and/or are reading this tutorial for the first time you can safely skip this part and continue reading Naming your Nodes below.

RefPtr is a synonym for RecPtr, but it is recommended to use RecPtr as that name emphasizes the fact that the changes are recorded in the changelist.

UnrecPtr is the suffix used to designate pointers that do manipulate the reference count of an object, but do not cause these these changes to be recorded in the current threads changelist. Using this type for temporary pointers that are to be stored in a Field is an optimization that prevents replaying the reference count changes to another (possibly remote) Aspect.

MTRecPtr or MTRefPtr denote pointers that are usable from multiple threads and will always return a pointer that points to the Aspect corresponding to the current thread. These pointers can be passed around between multiple threads, but your application still has to take to make access to these pointers mutually exclusive.

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 the chapter on Modelling? 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>

NodeRecPtr 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 not NULL. 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! This is especially important if you construct a std::string from the return value of getName().

Later I will introduce a helper class which searches the whole graph and returns the node matching a given name. (see NodeCores 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?.

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.

"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 corresponding 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 acquired by the following command

//n is some OSG::NodeRecPtr
const BoxVolume &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 possibility to avoid updating of the volume. Later (Geometry Utilities) 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 that

// ... continuing 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 NodeRecPtr 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.

"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 \ref TutorialTraversalActions?). 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 also add 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 (see NodeCores). 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 FieldContainer, cores too, need to be created via the static ::create() method. This example demonstrate how you can create a node containing a group core

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

n->setCore(g);

That's all! The "setCore()" method is used to assign a core to the node. As explained here 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

NodeRecPtr      n = Node     ::create();
TransformRecPtr 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
t->setMatrix(m);

n->setCore(t);

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 are never passed to siblings.

"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 discussion 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: Examples/Tutorial/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
TransformRecPtr 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:[BR] Now please, in the "main" function locate the lines that say

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

and replace them with the following code

NodeRecPtr scene;

// create all that stuff we will need:
//one geometry and one transform node

NodeRecPtr torus = makeTorus(.5, 2, 16, 16);
NodeRecPtr 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
transCore->setMatrix(m);

// now "insert" the core into the node
transNode->setCore(transCore);
// add the torus as a child to
// the transformation node
transNode->addChild(torus);

// "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 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

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

Matrix m;
        
// now provide some data...
        
// no rotation at the beginning
m.setIdentity();
        
// add the torus as a child to
// the transformation node
transNode->addChild(torus);
        
// "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 RecPtr to store the core pointer in. If you don't need that core right away you just call the function without parameters. Now, if we 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 - based on time
m.setRotate(Quaternion(Vec3f(0,1,0), time/1000.f));

//apply the new matrix to our transform core
transCore->setMatrix(m);

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: Examples/Tutorial/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. 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 way this is handled is by creating (on demand) copies of data that is modified by multiple threads. Every thread only sees the copy it is currently working on, which allows all other threads to run without having to worry about other things happening at the same time. Whenever it is convenient for the application it can merge the changes done in one thread into another thread. From that point on the two threads will actually share the same data until one of them modifies it again, creating once again two independent copies. As you have already seen all types derived from FieldContainer have protected constructors and objects need to be created using the static ::create() function.

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

TransformRecPtr 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 "FieldContainerFactory".

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
NodeRecPtr n_usual = Node::create();

//This does the same thing in another way
FieldContainerRecPtr n_factory = FieldContainerFactory::the()->createContainer("Node");

//it works with every type derived from Field Container
FieldContainerRecPtr g = FieldContainerFactory::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 NULL, 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 FieldContainerRecPtr 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 variables of types 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 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 a C++ standard library std::vector (i.e. a dynamically resizing array). SField and MField are template classes, and there are predefined typedefs for all the standard types in the system. These have names starting with SF and MF, e.g. SFBoxVolume and MFPnt3f.

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 as described in the section on Nodes.

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

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

    mother->addChild(me);

    NodeRecPtr n;
    
    n = me->getParent()->getParent();           // sets n to grandpa
    
    cout << "Grandpa has " << grandpa->getNChildren() << " children."
    
    n = grandpa->getChild(0);                   // sets n to aunt
    n = grandpa->getChild(1);                   // sets n to mother

This shows you the basic ways to get data from FieldContainers. They have accessor functions that are called getSFFieldname or getMFFieldname that return const pointers to the field and editSFFieldname, editMFFieldname that return non-const pointers that can be used to modify the field's content. As you can see above for single fields there are also convenience functions that return the value of the single field directly and for multi fields you get the same, provided you pass the index of the entry you are interested in (there is no range checking performed on the index, though).

The following example uses the OSG::DistanceLOD NodeCore, which was not introduced yet. A detailed explanation of this can be found here, but at this point it is sufficient to know that it has a multi field of Real32 (MFReal32) values called "range" and a single field of Pnt3f (SFPnt3f) called "center":

    DistanceLODPtr d = DistanceLOD::create();

    // 1) write a value to "center"
    d->setCenter(Pnt3f(0.0f, 1.0f, 2.0f));

    // 2) another way to write the same value to "center"
    SFPnt3f *pCenter = d->editSFCenter();
    pCenter->setValue(Pnt3f(0.0f, 1.0f, 2.0f));

    // 3) write some values to "range"
    d->getRange().push_back(0.0f);
    d->getRange().push_back(1.0f);
    d->getRange().push_back(2.0f);

    // 4) another way to write the same values to "range"
    MFReal32 *pRange = d->editMFRange();
    pRange->push_back(0.0f);
    pRange->push_back(1.0f);
    pRange->push_back(2.0f);

    Pnt3f  center;
    Real32 r;
    // 5) read value of "center"
    center = d->getCenter();

    // 6) another way to read the value of "center"
    center = d->getSFCenter()->getValue();

    // 7) read values of "range"
    for(UInt32 i = 0; i < d->getMFRange()->size(); ++i)
    {
        r = d->getRange(i);
    }

    // 8) another way to read the values of "range"
    MFReal32::const_iterator it  = d->getMFRange()->begin();
    MFReal32::const_iterator end = d->getMFRange()->end();
    for(; it != end; ++it)
    {
        r = *it;
    }

Note Because the code blocks after the comments 3) and 4) both append values, the "range" field will actually contain this: [0.0f, 1.0f, 2.0f, 0.0f, 1.0f, 2.0f].

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.

Editing FieldContainers

Now it is time to solve another mystery you encountered on previous tutorials: commitChanges() calls. To explain the need for these calls, lets have a look at what happens to the bounding volume if you add a child to a node: Since the new child may add geometry that is ouside of the bounding volume of its parent, the parents volume and therefore all other volumes up to the root of the scene are invalid. One way to deal with this is to mark all the volumes as invalid whenever a child is added. However, consider what that implies if you have a large scene with a long chain of nodes from the point you add to the root and you want to add a large number of children: Everytime a child is added, the chain would have to be traversed to the root marking the volumes as invalid. Clearly this is not very efficient. To this end FieldContainer have a changed() function that takes care of updating "dependent" information of the container based on which of its fields were changed. But when should this changed() function best be called? If it happens on every (write) access to a field we are back to the inefficient situation above, so the system needs some help from the user to determine when a set of related changes (like adding a number of children) is complete and "dependent" data should be recomputed. The commitChanges() function tells OpenSG exactly that: update all data that depends on changed fields.

Calls to commitChanges should be done as rarely as possible, as they may be somewhat expensive, but if your computations depend on OpenSG recalculating some values, just call it, so that you do not use outdated data. Probably the most common case you will encounter are actually the bounding volumes, but there are other types of FieldContainers that do similar tasks in their changed() function.

CoredNodeRefPtr - Core and Node under one roof

The Node and the Core separation has important advantages, but comes at the price of requiring two variables for each node. 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. The fact that two types are involved can not be completely hidden by the wrapper, but it shortens the amount of typing involved, which is always a nice thing to have.

The class that may help you out is OSG::CoredNodeRefPtr (and also OSG::CoredNodeMTRefPtr for use across thread boundaries). For all FieldContainers derived from NodeCore there are already CoredNodeRefPtr declared, they follow the naming scheme

    typedef CoredNodeRefPtr  <SomeContainer> SomeContainerNodeRefPtr;
    typedef CoredNodeMTRefPtr<SomeContainer> SomeContainerNodeMTRefPtr;

You can work with the CoredNodeRefPtr<> template directly, if you prefer, but when you use your own typedefs for them be careful to either use a different naming scheme or declare them in namespace OSG (preferably open it with OSG_BEGIN_NAMESPACE, close it with OSG_END_NAMESPACE), otherwise if the OSG namespace is active (i.e. through a OSG_USING_NAMESPACE) the two declarations may clash. Therefore it is recommended to use the typedefs supplied by OpenSG.

    GeometryNodeRefPtr cnpGeo = GeometryNodeRefPtr::create();

If you are working with cored pointers you have to keep in mind, that this object behaves similar to a core, but it does have a node 'around it'. That means operator->, 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

    NodeRecPtr n = cnpGeo.node();

In addition to wrapping the Node/Core? separation, CNPs also handle reference counting that same way RecPtr<> does. Thus the same limitations and additional features described in Tutorial/OpenSG2/Basics--BROKEN_LINK_TO_REFPTR? apply.

Well, next here is the code for a little example showing the cored node pointers in action. You can find the file in Examples/Tutorial/00CoredPointer.cpp

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


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

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

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

    //add the torus and box to the group node
    root.node()->addChild(torus);
    root.node()->addChild(box);
    
    return NodeTransitPtr(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 way of editing field containers (i.e. the long version).

Standard Geometry

OpenSG comes with some build-in functions 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 mostly 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

    NodeRecPtr 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 4res, 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 1038 polygons)! For better control use the LatLongSphere?, it has nicer texture coordinates anyway.

All of these geometry-generating methods return a NodeTransitPtr? 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 GeometryTransitPtr?. You only need to append a "Geo" to the above functions.

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

GeometryRecPtr g = makeConeGeo(10, 5, 32, true, false);
n->setCore(g);

Please note 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 add even more things to it ;-). Okay so let us think about it, before we begin to write code...

"'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 bear in mind 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 NodeTransitPtr?. 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

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

with

    NodeRecPtr 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
NodeTransitPtr createScenegraph(void)
{
    // First we will create all needed geometry
    // the body of the house
    NodeRecPtr houseMain = makeBox(20,20,20,1,1,1);
    
    // now the roof
    NodeRecPtr 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)
    NodeRecPtr chimney = makeCylinder(10,1,8,true,true,false);

    // Now we create the root node and attach the geometry nodes to it
    n->setCore(Group::create());
    n->addChild(houseMain);
    n->addChild(roof);
    n->addChild(chimney);

    return NodeTransitPtr(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
NodeTransitPtr createScenegraph(void)
{
    // we will use the variable to set our transform matrices
    Matrix m;
    
    // First we will create all needed geometry
    // the body of the house
    NodeRecPtr houseMain = makeBox(20,20,20,1,1,1);
    
    // now the roof
    NodeRecPtr roof = makeBox(14.14, 14.14, 20, 1, 1, 1);
    
    // we translate the roof to the correct position
    TransformRecPtr tRoof = Transform::create();
    m.setIdentity();
    m.setTranslate(0,10,0);
    m.setRotate(Quaternion(Vec3f(0,0,1), 3.14159/4));
        
    tRoof->setMatrix(m);
    
    NodeRecPtr roofTrans = Node::create();
    roofTrans->setCore(tRoof);
    roofTrans->addChild(roof);
    
    // and the chimney - we have the top and sides generated
    // but we have no need for the bottom (it is inside the house)
    NodeRecPtr chimney = makeCylinder(10,1,8,true,true,false);
    
    //now we translate the chimney
    
    //create the transform core
    TransformRecPtr tChimney = Transform::create();
    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);
    
    //insert the transform core into the node
    NodeRecPtr chimneyTrans  = Node::create();
    chimneyTrans->setCore(tChimney);
    chimneyTrans->addChild(chimney);

    // Now we create the root node and attach the geometry nodes to it
    NodeRecPtr n = Node::create();
    n->setCore(Group::create());
    n->addChild(houseMain);
    n->addChild(roofTrans);
    n->addChild(chimneyTrans);

    return NodeTransitPtr(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.

"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 ;-)

"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 not be 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 derived 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
ImageRecPtr 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};

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

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:

"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 Examples/Tutorial/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
    GeometryRecPtr boxGeo= makeBoxGeo(10,10,10,1,1,1);
    
    //Load the image we want to use as a texture
    ImageRecPtr image = Image::create();
    image->read("images/bricks.jpg");
    
    //now we create the texture that will hold the image
    SimpleTexturedMaterialRecPtr tex = SimpleTexturedMaterial::create();
    tex->setImage(image);
    
    //now assign the fresh texture to the geometry
    boxGeo->setMaterial(tex);
    
    // Create the node that will hold our geometry
    NodeRecPtr n = Node::create();
    n->setCore(boxGeo);
    
    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 Material.

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:

  • VRML97
  • OFF
  • OBJ
  • RAW
  • OSG (OpenSG ASCII format)
  • BIN (called OSB after 1.2)
  • Collada

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, lately Collada is becoming more popular. BIN/OSB 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: \ref Modeling.

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

    NodeRecPtr 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 imagine, the return type is a NodeTransitPtr? - if the loading process was not successfull for some reason "NULL" 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:

    SceneFileHandler::the()->write(n, "filename");

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

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

    return NodeTransitPtr(n);

Easy, isn't it? For more information about loading see chapter Modelling?.

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':
            SceneFileHandler::the()->write(scene, "data/output.osb");
            
            std::cout << "File written!" << std::endl;
            break;
        }
    }
    

Exercises

1) Transformations
Please recall the second tutorial. We translated the roof with the following transformation matrix
m.setIdentity();
m.setTranslate(0,10,0);
m.setRotate(Quaternion(Vec3f(0,0,1), 3.14159/4));

tRoof->setMatrix(m);

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()?

2) Loading
Modify the loading/saving tutorial, such that 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 report an appropriate error (the application should continue to run!).

<< Previous Chapter: First Application Tutorial Overview Next Chapter: Node cores >>
Last modified 7 years ago Last modified on 01/17/10 01:11:44