wiki:DevGuide/Geometry

Geometry

Geometries define the data used for rendering. Geometries don't necessarily have to be leaves of the tree. Due to the Node/Core? divison every Node with an OSG::Geometry Core can have children, which are used just as all other OSG::Node's children. Geometry has to be flexible, to accommodate the needs of the application and to provide input data for complex shaders. Providing support for various data types that define the geometry is useful, as well as different indexing capabilities to reuse data as much as possible. On the other hand, it also has to be efficient to render. Flexibility and performance don't always go well together, thus, there are some simplifications to make.

Properties (Attributes)

OpenSG geometry is modeled closely following OpenGL. The data that make up the geometry are stored in separate arrays. Positions, Colors, Normals and Texture Coordinates all have their own arrays, (or OSG::MField, to stay in OpenSG terminology). As OpenGL can handle many different formats of data, some of which might be more appropriate due to speed and memory consumption than others, depending on the application, OpenSG features different versions of this data, allowing pretty much all the variants that OpenGL can handle. To allow this with type safety and without having a separate geometry class for every possible combination, the data fields are stored in separate field containers, the so called GeoProperties.

There are separate OSG::GeoProperty types for different attributes, and variants for different data types for each kind of OSG::GeoProperty. The naming scheme for OSG::GeoProperty is OSG::Geo<StoredType>Property. The most commonly used types are:

Note: The OpenSG 1.x names directly associated a OSG::GeoProperty with its role in the fixed function OpenGL pipeline. Since the introduction of vertex shaders there role of a property is not cast in stone, but instead determined through its use inside the shader. Therefore the new naming scheme focuses more on the type of data stored in a OSG::GeoProperty and not on its function.

Class Structure

The basic idea of the OSG::GeoProperty class structure is very simple. The actual implementation might appear a little complex at fist, but allows for simple extensions with a great deal of code reuse. Once the ideas behind the structure are understood, the actual code does not look so bad any more.

All OSG::GeoProperty are derived from OSG::StateChunk, since geometric data is also part of the OpenGL state and this allows for a natural way to take advantage of vertex buffer objects.

There are three primary types of OSG::GeoProperty: OSG::GeoIntegralProperty, OSG::GeoVectorProperty and OSG::GeoMultiProperty. The first two are the generic base classes for the typed OSG::TypedGeoIntegralProperty and OSG::TypedGeoVectorProperty that hold the actual data. OSG::GeoMultiProperty (*XXX one sentence missing here XXX* -- cneumann).

OSG::GeoIntegralProperty types

OSG::GeoIntegralProperty is used for scalar data, most notably to specify OpenGL primitive types, the number of vertices to use for a primitve or as indices into other properties. However you can not create instances of this class as it is abstract and only offers an interface that allows access to the data through any scalar type, independent of the data actually stored in the property. This can be very convienent, but the abstraction comes at a cost, so for performance critical operations you need the exact type information and access to data through the derived OSG::TypedGeoIntegralProperty - the interfaces are described in more detail below.

The predefined typedefs of OSG::GeoIntegralProperty include:

There are also aliases for the OpenSG 1.x names including: OSG::GeoPTypesUI8, OSG::GeoPTypesUI16, OSG::GeoPTypesUI32 and OSG::GeoPLengthsUI8, OSG::GeoPLengthsUI16, OSG::GeoPLengthsUI32 and OSG::GeoIndicesUI8, OSG::GeoIndicesUI16, OSG::GeoIndicesUI32.

OSG::GeoVectorProperty types

OSG::GeoVectorProperty is used for vector like data, i.e. for properties that hold point, vector or color information, which may of course be of any type OpenSG offers for these purposes. Again this class is abstract and provides an interface that is independent of the actual data type.

There are predefined typedefs for all combinations of OSG::Geo(Pnt/Vec/Color?)(1/2/3/4)(ub/b/us/s/f/fx/d)Property that makes sense in combination. For example the most commonly used types are:

OSG::GeoMultiProperty types

OSG::GeoMultiProperty (*XXX paragraph here XXX* -- cneumann).

Accessing the data

Access to the data is available at two levels that offer different performance convenience tradeoffs. The generic level (available through OSG::GeoIntegralProperty and OSG::GeoVectorProperty does not require knowledge of the exact data types stored in a property and performs the necessary conversions for the user. The typed level (available through OSG::TypedGeoIntegralProperty and OSG::TypedGeoVectorProperty on the other hand does require that knowledge, but compensates this by offering better performance.

The generic level contains member template functions that convert the data to the user requested type after retrieving it through an internal interface, thus requiring two type conversions (from the stored type to the type used in the internal interface to the user requested type). The type used in the internal interface is exported as OSG::GeoIntegralProperty::MaxTypeT and OSG::GeoVectorProperty::MaxTypeT respectively.

template <class ExternalType>

ExternalType 

Geo*Property::getValue(const UInt32 index) const;



template <class ExternalType>

void

Geo*Property::getValue(ExternalType &eval, const UInt32 index) const;



template <class ExternalType>

void

Geo*Property::setValue(const ExternalType &eval, const UInt32 index);



template <class ExternalType>

void

Geo*Property::addValue(const ExternalType &eval);



template <class ExternalType>

void

Geo*Property::push_back(const ExternalType &eval)

At the typed level the same template member functions are available as well, but they only require one conversion from the stored type to that requested by the user. Additionally there are overloads that use parameters of the stored type that incur no conversion penalty at all. Another alternative is to gain access to the OSG::MField that holds the data and modify it directly.

      TypedGeo*Property::StoredFieldType *editFieldPtr(void);

const TypedGeo*Property::StoredFieldType *getFieldPtr (void) const;



      TypedGeo*Property::StoredFieldType &editField(void);

const TypedGeo*Property::StoredFieldType &getField (void) const;

Finally, OSG::GeoProperty features an interface for OpenGL vertex arrays, giving access to the data and the types involved, which is used for rendering, however this read only interface should really only used for internal purposes.

Hint!

Some rules of thumb:

  • Everywhere you actually want to create a new property you have to use the typed versions like OSG::GeoPnt3fProperty, as they are the only ones that actually contain data.
  • To write functions that can handle arbitrary types of data, use abstract property pointers and the generic interface to access the data.
#!cpp

GeoVectorPropertyPtr colProp = geo->getColors();

if(col == NullFC)

{

   FWARNING(("No colors available!\n"));

   return;

}

// this works independent of the real data type used for colors.

colProp->push_back(Color3f(1, 0, 1));

...

  • If you know that all the geometry your function has to work on has been created by yourself using a single type of property you can just downcast to that type and use the interface of the OSG::MField that holds the data directly. For safety reasons you should make sure the downcast succeeded. This is the most efficient way to access the data.
#!cpp

GeoColor3ubPropertyPtr colProp = GeoColor3ubPropertyPtr::dcast(geo->getColors());

if(colProp == NullFC)

{

   FWARNING(("Downcast failed!\n"));

   return;

}

MFColor3ub *colField = colProp->getFieldPtr();

// here only Color3ub works!

colField->push_back(Color3ub(255, 0, 255));

...

Indexing

Using these properties it is possible to define geometry. Note that OpenSG inherits the constraints and specifications that concern geometry from OpenGL. Vertex orientation is counterclockwise when seen from the outside, and concave polygons are not supported.


Non-Indexed Geometry

One additional advantage of separating properties from Geometry is the ability to share properties between geometry osg::NodeCore? s. As geometries can only have one material right now that's useful for simplifying the handling of objects with multiple materials.

This simple geometry has one problem: there is no way to reuse vertex data. When a vertex is to be used multiple times, it has to be replicated, which can increase the amount of memory needed significantly. Thus, some sort of indexing to reuse vertices is needed. You can guess what's coming? Right, another property.

Indices are stored in the osg::GeoIndices? property, which only exists in the osg::GeoIndicesUI32 variant right now. When indices are present the given lengths define how many indices are used to define the primitive, while that actual data is indexed by the indices.


Indexed Geometry

Indexed geometry is very close to OpenGL, and probably the most often used type of geometry. It doesn't handle all the cases, though.

Sometimes vertices need different additional attributes, even though they have the same position. One example are discontinuities in texture coordinates, e.g. when texturing a simple cube. The edges of the cube don't necessarily use the same texture coordinate. To support that a single indexed geometry has to replicate the vertices.

To get around that you need multiple indices per vertex to index the different attributes. Adding an index for every attribute would blow up the geometry significantly and not necessarily make it easier to use. We decided to use another way: interleaved indices.


Multi-Indexed Geometry

Interleaved indices require every vertex to hold multiple indices. Which index is used for what attribute is defined by a separate indexMapping field. The indexMapping field is a osg::UInt32 osg::MField. The possible values are bitwise combinations of the available attribute masks: osg::Geometry::MapPosition?, osg::Geometry::MapNormal? etc. The length of the indexMapping defines how many indices are used per vertex. If it's not set a single index for all available properties is used (or none at all).

In addition to the properties geometry keeps a osg::MaterialPtr? to define the material that's used for rendering the geometry (see Materials) and a flag that activates caching the geometry in OpenGL display lists. As geometry rendering is not optimized very much right now that's the best way to get decent performance. Display lists are turned on by default.

Geometry Iterators

The osg::Geometry setup is very nice and flexible to define: you can mix different kinds of primitives in an object, you can have properties and different kinds and the indexing allows the reuse of some or all of the data.

From the other side of the fence things look different: if you want to walk over all triangles of a geometry to calculate the average triangle size or the surface area, or for calculating face normals or for whatever reason you have to take care of all the flexibility and be prepared for lots of different ways to define geometry.

To simplify that the concept of a geometry iterator has been introduced. A geometry iterator allows to iterate over a given geometry primitive by primitive, face by face (a face being a triangle or quad), or triangle by triangle.

All of them are used like STL iterators: the osg::Geometry has methods to return the first or last+1th iterator, and to step from one element to the next. They can also unify the different indexing variants: when using an iterator you can access the index value for each attribute of each vertex of the iterator separately. Or you can directly access the data that's behind the index in its generic form, which is probably the easiest way of accessing the data of the osg::Geometry.

Example: The following loop prints all the vertices and normals of all the triangles of a geometry:

#!cpp

for(it = geo->beginTriangles(); it != geo->endTriangles(); ++it)

{

    std::cout << "Triangle " << it.getIndex() << ":" << std::endl;

    std::cout << it.getPosition(0) << " " << it.getNormal(0) << std::endl;

    std::cout << it.getPosition(1) << " " << it.getNormal(1) << std::endl;

    std::cout << it.getPosition(2) << " " << it.getNormal(2) << std::endl;

}

If you're used to having a separate Face object that keeps all the data for a face, the Iterators pretty much mimic that behavior. The one thing you can't do using iterators is changing the data. To do that you have to use the Indices the Iterators give you and access the Properties directly. Be aware that the Iterators hide all data sharing, so manipulating data for a face the iterator gives you can influence an arbitrary set of other faces.

Primitive Iterator

The osg::PrimitiveIterator? is the basic iterator that just iterates through the osg::GeoPTypes property and gives access to the primitive's data. It is useful to solve the index mapping complications and to get access to the generic data, but it's primarily a base class for the following two iterator types.

Face Iterator

The osg::FaceIterator? only iterates over polygonal geometry and ignores points, lines and polygonal primitives with less than three vertices. It also splits the geometry into triangles or quads.

Triangle Iterator

The osg::TriangleIterator? behaves like the osg::FaceIterator?, but it also splits Quads into two triangles, thus it does an implicit triangulation. As OpenSG just like OpenGL doesn't support concave geometry that's not as hard as it sounds.

Line Iterator

The osg::LineIterator? only iterates over line geometry and ignores points, polygonal primitives and line primitives with less than two vertices. It splits line strips and loops into single lines.

Edge Iterator

The osg::EdgeIterator? (currently) only iterates over line geometry and ignores points, polygonal primitives and line primitives with less than two vertices like the osg::LineIterator? does, but it leaves line strips and loops as they are. This iterator will make more sense in a future version, where it returns the edges of the other primitives as single lines or line loops as well.

Dev: For all polygonal primitives probably except GL_POLYGON itself the edges could be returned as single lines conceptually similar to the implicit triangulation of the osg::TriangleIterator?. For a polygon it seems reasonable to me to reinterpret it as line loop. Shouldn't we overload getType() to return the interpretation?

The iterators can also be used to indicate a specific primitive/face/triangle/line. Each of these has an associated index that the iterator keeps and that can be accessed using getIndex(). A new iterator can be used to seek() a given primitive/face/triangle again and work on it. This is used for example in the osg::IntersectAction?.

Simple Geometry

OpenSG does not have NodeCores? for geometric primitives like spheres, cones, cylinders etc. Instead there are a number of utility functions that can create these objects. They can be created as a ready-to-use node and as a naked node core. In most cases we tried to mimic the VRML primitive semantics, so if you're familiar with VRML you will feel right at home.

Plane::

osg::makePlane creates a single subdivided quad.

Box::

osg::makeBox creates a box around the origin with subdivided sides.

Cone::

osg::makeCone create a cone at the origin.

Cylinder::

osg::makeCylinder create a cylinder at the origin.

Torus::

osg::makeTorus create a torus at the origin.

ConicalFrustum::

osg::makeConicalFrustum creates a truncated cone at the origin.

Sphere::

There are two ways to create a sphere. osg::makeSphere uses a icosahedron as a base and subdivides it. This gives a sphere with equilateral triangles, but they do not correspond to latitude or longitude, which makes it hard to get good texture mapping on it. As every subdivision step quadruples the number of triangles, it is also hard to control the complexity of these kinds of spheres.

osg::makeLatLongSphere on the other hand creates a sphere by simply usign a regular subdivision of latitude and longitude. This creates very small polygons near the poles, but is more amendable to texture mapping and gives finer control of the resoltuion of the sphere.

Extrusion Geometry

osg::makeExtrusion creates a pretty general extruded geometry. It works by sweeping a given cross section, which can be given clockwise or counterclockwise, across a spine. For every spine point an orientation and a scale factor are specified. The beginning and the end of the object can be closed by caps, but for the capping to work the cross section has to be convex. The resulting geometry can be refined as a subdivision surface (no idea which subdivision scheme is applied, anyone care who knows care to take a look?). Optionally normals and texture coordinates can be generated

Helper Functions

A number of helper functions can be used in conjunction with manipulating and optimizing geometry.

Normal Calculation

A common problem for self-created geometry or for geometry loaded from simple file formats are missing normals. Normals are needed for proper lighting, without them objects will either be black or uniformly colored.

Normals can be calculated either for every face or for every vertex.

Face normals, as calculated by osg::calcFaceNormals, are only unique for a given triangle or quad. The resulting object will look faceted, whcih may or may not be the desired effect. This will also work for striped or fanned models, as OpenSG doesn't have a per-face binding and uses multi-indexed per-vertex normals for this.

Vertex normals are calculated for every vertex and allow a shape to look smooth, as the lighting calculation is done using the vertex normals and interpolated across the surface. They can be calculated using two different methods.

osg::calcVertexNormals(GeometryPtr? geo) will just average all the normals of the faces touching a vertex. It does not unify the vertices, i.e. it does not check if a vertex with given coordinates appears in the position property multiple times, the geometry has to be created correctly or be run thrugh osg::createSharedIndex.

The disadvantage of osg::calcVertexNormals(GeometryPtr? geo) is its indiscriminative nature, it will average out all the edges in the object. The alternative is osg::calcVertexNormals(GeometryPtr? geo, Real32 creaseAngle), which uses a crease angle criterion to define which edges to keep. Edges that have an angle larger than creaseAngle will not be averaged out. It won't always work for striped geometry. It will process it, but if a stripe point needs to be split because it has two normals, that won't be done. The same sharing caveat as given above applies.

Calculating vertex normals with a crease angle sounds simpler than it is, especially if the calculation should be independent of the triangulation of the object. Thus the algorithm is relatively expensive and should be avoided in a per-frame loop. There are some ideas to do the expesive calculations once and quickly reaverage the normals when needed. These have not been realized, if you need this or even better want to implement it, notify us at info@….

Geometry Creation

Setting up all the objects needed to fully specify an OpenSG Geometry can be a bit tedious. So to simplify the process there are some functions that take data in other formats and create the corresponding OpenSG Geometry data.

Right now there is only one function to help with this, osg::setIndexFromVRMLData. It takes separate indices for the different attributes, as given in the VRML97 specification, together with the flags that influence the interpretation of these indices, and sets up the indices, lengths and types properties of the given geometry.

Geometry Optimization

OpenSG's Geometry structure is very flexible and pretty closed modeled on OpenGL. But not all of the Geometry data specification variants are similarly efficient to render. The functions in this group help optimizie different aspects of the Geometry.

osg::createOptimizedPrimitives takes a Geometry and tries to change it so that it can be rendered using the minimum number of vertex transformations. To do that it connects triangles to strips and fans (optionally). It does not change the actual property values, it just creates new indices, types and lengths. The algorithm realized here does not try a high-level optimization, instead it is optimized for speed. Due to its pseudo-random nature it can be run multiple times in the same time a more complex algorithm needs, allowing it to try different variants and keeping the best one found. Or, if execution time is a problem, it can be run only once and create a very quick result that is good, but not optimal.

osg::createSharedIndex tries to find identical elements in the Geometries Properties and remove the copies. It will not actually change the Property data, it will just change the indexing to only use one version of the data. This is a necessary preparation step to allow osg::createOptimizedPrimitives to identify the triangles it can connect to form stripes and fans.

osg::createSingleIndex resorts the Geometry's Property values to allow using a single index (in contrast to interleaved multi-indices) to represent the Geometry. To do that it might have to remap and copy Property values, as well as index values. While multi-indexing can be very efficient datawise, as as much of possible is shared, for rendering it is problematic. OpenGL doesn't know multi-indexing, thus for multi-indexed Geometry the more efficient OpenGL geometry specifiers like VertexArrays? can't be used, which can have a significant impact on performance, especially for dynamic objects.

UInt32 calcPrimitiveCount ( GeometryPtr? geo, UInt32 &triangle, UInt32 &line, UInt32 &point );

Normal Visualisation

For debugging it can be useful to actually see the normals of an object, as that allows making sure that the normals point in the expected direction and that normals are really identical and not just pretty close. Every normal is represented by a line of a user-defined length.

As OpenSG doesn't have an explicit face/vertex binding mode there are two different functions to create an object representing the vertex or face normals. The application should know whether face or vertex normals are used in the given geometry. In general it is safe to assume vertex normals are used.

osg::calcVertexNormalsGeo creates an object that shows the vertex normals of the given geometry, while osg::calcFaceNormalsGeo creates an object that shows the face normals.

Dev: We should add something more general here. Forcing the app to know is not nice. calcVertexNormalsGeo can already calc normals at the vertices and at the centers of the tris, but the decision when to use which and which normal to use is not clear.

Last modified 8 years ago Last modified on 01/17/10 01:11:44

Attachments (3)

Download all attachments as: .zip