wiki:Tutorial/OpenSG1/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

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:

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

b Int8
ub UInt8
s Int16
us UInt16
f Real32
d Real64
ld Real128

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

#!cpp

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

#!cpp

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

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

#!cpp

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

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

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

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

#!cpp

//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 \ref TutorialBasicEditingFieldContainers?.

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

#!cpp

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

#!cpp

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

#!cpp

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 operator* and call member functions on them using

operator->. 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 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 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:

#!cpp

    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

#!cpp

addRefCP(objectPtr)<br>

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

#!cpp

    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:

#!cpp

    #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 extremely 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 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

#!cpp



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

Later I will introduce a helper class which searches the whole graph and returns the node matching a given name. (see \ref TutorialNodeCoresTutorial?)

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

#!cpp

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

#!cpp

        //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 possibility to avoid updating of the volume. Later (\ref TutorialGeometryUtilities?) 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

#!cpp

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

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

#!cpp

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

#!cpp

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

"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: progs/01firstapp2.cpp.

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

#!cpp

// 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:[BR]

Now please, in the "main" function locate the lines that say

#!cpp

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

#!cpp

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

#!cpp

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

#!cpp

void display(void){

    mgr->redraw();

}

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

#!cpp

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 :

#!cpp

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.

#!cpp

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

#!cpp

//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 the section on Nodes.

#!cpp

    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->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 methods that are called getFieldname 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 a std::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 operator[].

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

#!cpp

    DistanceLODPtr d = DistanceLOD::create();



    beginEditCP(d);

    {

        // 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->getSFCenter();

        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->getMFRange();

        pRange->push_back(0.0f);

        pRange->push_back(1.0f);

        pRange->push_back(2.0f);



    }

    endEditCP(d);



    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->getRange().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 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:

#!cpp

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 guarantee 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

#!cpp

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?::FieldToBeEdited?FieldMask?. 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:

#!cpp

{

    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:

#!cpp

{

    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

NOTE: you had to typedef the core that you want to use in the previous version of OpenSG while that tutorial was on documenatation, but you do not have to do that now as they are already defined with different types, so the following line is not required while write the code

#!cpp

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

#!cpp

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

#!cpp

        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 \ref BasicRefPtr? 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.

#!cpp





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).

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

#!cpp

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

#!cpp

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

#!cpp

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

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

#!cpp

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

with

#!cpp

    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!)

#!cpp

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

#!cpp

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

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

#!cpp

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

#!cpp

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

#!cpp

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

#!cpp

#include <OpenSG/OSGSimpleTexturedMaterial.h>

#include <OpenSG/OSGImage.h>

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

#!cpp

    //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 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
  • BIN (called OSB after 1.2)

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 : \ref Modeling.

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

#!cpp

    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:

#!cpp

    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:

#!cpp

    #include <OpenSG/OSGSceneFileHandler.h>

Insert the following code into the createScenegraph() method

#!cpp

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

  1. Add the following function somewhere before setupGLUT.
   #!cpp

   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.

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

Exercises

1) Transformations

Please recall the second tutorial. We translated the roof with the following transformation matrix

#!cpp

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

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

Attachments (11)

Download all attachments as: .zip