Manual: Collision Detection

From ODE Wiki
Jump to: navigation, search
ODETutorial.png
This article is part
of the ODE Manual
Chapters ...

Introduction
Install and Use
Concepts
Data Types and Conventions
World
Rigid Body Functions
Joint Types and Functions
Support Functions
Collision Detection

ODE has two main components: a dynamics simulation engine and a collision detection engine. The collision engine is given information about the shape of each body. At each time step it figures out which bodies touch each other and passes the resulting contact point information to the user. The user in turn creates contact joints between bodies.

Using ODE's collision detection is optional -- an alternative collision detection system can be used as long as it can supply the right kinds of contact information.

Collision tests supported

Here is a table of what primitives collide with which other, ordered by object complexity (information taken from collision_kernel.cpp->initColliders() function).

Ray Plane Sphere Box Capsule Cylinder Trimesh Convex Heightfield
Ray No2 Yes Yes Yes Yes Yes Yes Yes Yes
Plane - No2 Yes Yes Yes Yes Yes Yes No2
Sphere - - Yes Yes Yes Yes Yes Yes Yes
Box - - - Yes Yes Yes Yes Yes3 Yes
Capsule - - - - Yes Yes3 Yes Yes3 Yes
Cylinder - - - - - Yes3 Yes Yes3 Yes
Trimesh - - - - - - Yes No Yes
Convex - - - - - - - Yes Yes
Heightfield - - - - - - - - No2

Note 1: This feature exists only in a recent SVN revision and has not yet been added to an official release.

Note 2: Not planned to be implemented, since this collision combination doesn't make much sense in a physics simulation.

Note 3: By enabling the libccd colliders.

It's possible to override the collision handler for a certain pair of geom classes through dSetColliderOverride():

void dSetColliderOverride (int class1, int class2, dColliderFn *fn);

dColliderFn is defined as:

typedef int dColliderFn (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

Contact points

If two bodies touch, or if a body touches a static feature in its environment, the contact is represented by one or more "contact points". Each contact point has a corresponding dContactGeom structure:

struct dContactGeom {
    dVector3 pos;    // contact position
    dVector3 normal; // normal vector
    dReal depth;     // penetration depth
    dGeomID g1,g2;   // the colliding geoms
};
  • pos records the contact position, in global coordinates.
  • depth is the depth to which the two bodies inter-penetrate each other. If the depth is zero then the two bodies have a grazing contact, i.e. they "only just" touch. However, this is rare - the simulation is not perfectly accurate and will often step the bodies too far so that the depth is nonzero.
  • normal is a unit length vector that is, generally speaking, perpendicular to the contact surface.
  • g1 and g2 are the geometry objects that collided.

The convention is that if body 1 is moved along the normal vector by a distance depth (or equivalently if body 2 is moved the same distance in the opposite direction) then the contact depth will be reduced to zero. This means that the normal vector points "in" to body 1.

In real life, contact between two bodies is a complex thing. Representing contacts by contact points is only an approximation. Contact "patches" or "surfaces" might be more physically accurate, but representing these things in high speed simulation software is a challenge.

Each extra contact point added to the simulation will slow it down some more, so sometimes we are forced to ignore contact points in the interests of speed. For example, when two boxes collide many contact points may be needed to properly represent the geometry of the situation, but we may choose to keep only the best three. Thus we are piling approximation on top of approximation.

Contact points can then be used, for example, to generate contact joints.

Geoms

Geometry objects (or "geoms" for short) are the fundamental objects in the collision system. A geom can represents a single rigid shape (such as a sphere or box), or it can represents a group of other geoms - this is a special kind of geom called a "space".

Any geom can be collided against any other geom to yield zero or more contact points. Spaces have the extra capability of being able to collide their contained geoms together to yield internal contact points.

Geoms can be placeable or non-placeable. A placeable geom has a position vector and a 3*3 rotation matrix, just like a rigid body, that can be changed during the simulation. A non-placeable geom does not have this capability - for example, it may represent some static feature of the environment that can not be moved. Spaces are non-placeable geoms, because each contained geom may have its own position and orientation but it does not make sense for the space itself to have a position and orientation.

To use the collision engine in a rigid body simulation, placeable geoms are associated with rigid body objects. This allows the collision engine to get the position and orientation of the geoms from the bodies. Note that geoms are distinct from rigid bodies in that a geom has geometrical properties (size, shape, position and orientation) but no dynamical properties (such as velocity or mass). A body and a geom together represent all the properties of the simulated object.

Every geom is an instance of a class, such as sphere, plane, or box. There are a number of built-in classes, described below, and you can define your own classes as well.

The point of reference of a placeable geoms is the point that is controlled by its position vector. The point of reference for the standard classes usually corresponds to the geom's center of mass. This feature allows the standard classes to be easily connected to dynamics bodies. If other points of reference are required, a transformation object can be used to encapsulate a geom.

The concepts and functions that apply to all geoms will be described below, followed by the various geometry classes and the functions that manipulate them.

Spaces

A space is a non-placeable geom that can contain other geoms. It is similar to the rigid body concept of the "world", except that it applies to collision instead of dynamics.

Space objects exist to make collision detection go faster. Without spaces, you might generate contacts in your simulation by calling dCollide to get contact points for every single pair of geoms. For N geoms this is O(N2) tests, which is too computationally expensive if your environment has many objects.

A better approach is to insert the geoms into a space and call dSpaceCollide. The space will then perform collision culling, which means that it will quickly identify which pairs of geoms are potentially intersecting. Those pairs will be passed to a callback function, which can in turn call dCollide on them. This saves a lot of time that would have been spent in useless dCollide tests, because the number of pairs passed to the callback function will be a small fraction of every possible object-object pair.

Spaces can contain other spaces. This is useful for dividing a collision environment into several hierarchies to further optimize collision detection speed. This will be described in more detail below.

General geom functions

The following functions can be applied to any geom.

void dGeomDestroy (dGeomID);

Destroy a geom, removing it from any space it is in first. This one function destroys a geom of any type, but to create a geom you must call a creation function for that type.

When a space is destroyed, if its cleanup mode is 1 (the default) then all the geoms in that space are automatically destroyed as well.

void dGeomSetData (dGeomID, void *);
void * dGeomGetData (dGeomID);

These functions set and get the user-defined data pointer stored in the geom.

void dGeomSetBody (dGeomID, dBodyID);
dBodyID dGeomGetBody (dGeomID);

These functions set and get the body associated with a placeable geom. Setting a body on a geom automatically combines the position vector and rotation matrix of the body and geom, so that setting the position or orientation of one will set the value for both objects.

Setting a body ID of zero gives the geom its own position and rotation, independent from any body. If the geom was previously connected to a body then its new independent position/rotation is set to the current position/rotation of the body.

Calling these functions on a non-placeable geom results in a runtime error in the debug build of ODE.

void dGeomSetPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetRotation (dGeomID, const dMatrix3 R);
void dGeomSetQuaternion (dGeomID, const dQuaternion);

Set the position vector, rotation matrix or quaternion of a placeable geom. These functions are analogous to dBodySetPosition, dBodySetRotation and dBodySetQuaternion. If the geom is attached to a body, the body's position / rotation / quaternion will also be changed.

Calling these functions on a non-placeable geom results in a runtime error in the debug build of ODE.

const dReal * dGeomGetPosition (dGeomID);
const dReal * dGeomGetRotation (dGeomID);
void dGeomGetQuaternion (dGeomID, dQuaternion result);

The first two return pointers to the geom's position vector and rotation matrix. The returned values are pointers to internal data structures, so the vectors are valid until any changes are made to the geom. If the geom is attached to a body, the body's position / rotation pointers will be returned, i.e. the result will be identical to calling dBodyGetPosition or dBodyGetRotation.

dGeomGetQuaternion copies the geom's quaternion into the space provided. If the geom is attached to a body, the body's quaternion will be returned, i.e. the resulting quaternion will be the same as the result of calling dBodyGetQuaternion.

Calling these functions on a non-placeable geom results in a runtime error in the debug build of ODE.

void dGeomSetOffsetPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetOffsetRotation (dGeomID, const dMatrix3 R);
void dGeomSetOffsetQuaternion (dGeomID, const dQuaternion Q);

Set the offset position, rotation or quaternion of a geom. The geom must be attached to a body. If the geom did not have an offset, it is automatically created. This sets up an additional (local) transformation for the geom, since geoms attached to a body share their global position and rotation. To disable the offset call dGeomClearOffset.

void dGeomSetOffsetWorldPosition (dGeomID, dReal x, dReal y, dReal z);
void dGeomSetOffsetWorldRotation (dGeomID, const dMatrix3 R);
void dGeomSetOffsetWorldQuaternion (dGeomID, const dQuaternion Q);

Set the offset world position, rotation or quaternion of a geom. The new local offset is the difference of the current body transformation, so that the geom is placed and oriented in the world as specified, without changing the body's transformation. See also the previous three offset functions.

const dReal * dGeomGetOffsetPosition (dGeomID);
const dReal * dGeomGetOffsetRotation (dGeomID);
void dGeomGetOffsetQuaternion (dGeomID, dQuaternion result);

Get the offset position, rotation or quaternion of a geom. The returned value of the first two functions are pointers to the geom's internal data structure. They are valid until any changes are made to the geom. If the geom has no offset the zero vector is returned, in case of dGeomGetOffsetQuaternion the identity quaternion is returned.

void dGeomClearOffset (dGeomID);

Disable the geom's offset. The geom will be repositioned / oriented at the body's position / orientation. If the geom has no offset, this function does nothing. Note, that this will eliminate the offset and is more efficient than setting the offset to the identity transformation.

void dGeomGetAABB (dGeomID, dReal aabb[6]);

Return in aabb an axis aligned bounding box that surrounds the given geom. The aabb array has elements (minx, maxx, miny, maxy, minz, maxz). If the geom is a space, a bounding box that surrounds all contained geoms is returned.

This function may return a pre-computed cached bounding box, if it can determine that the geom has not moved since the last time the bounding box was computed.

int dGeomIsSpace (dGeomID);

Return 1 if the given geom is a space, or 0 if not.

dSpaceID dGeomGetSpace (dGeomID);

Return the space that the given geometry is contained in, or return 0 if it is not contained in any space.

int dGeomGetClass (dGeomID);

Given a geom, this returns its class number. The standard class numbers are:

dSphereClass = 0 Sphere
dBoxClass Box
dCapsuleClass Capsule (i.e. cylinder with half-sphere caps at its ends)
dCylinderClass Regular flag ended Cylinder
dPlaneClass Infinite plane (non-placeable)
dRayClass Ray
dConvexClass
dGeomTransformClass Geometry transform
dTriMeshClass Triangle mesh
dHeightfieldClass
dFirstSpaceClass
dSimpleSpaceClass = dFirstSpaceClass Simple space
dHashSpaceClass Hash table based space
dQuadTreeSpaceClass Quad tree based space
dLastSpaceClass = dQuadTreeSpaceClass
dFirstUserClass
dLastUserClass = dFirstUserClass + dMaxUserClasses - 1,

The class value is equal to value of the previous row + 1, if there is no equal sign.

User defined classes will return their own numbers.

void dGeomSetCategoryBits (dGeomID, unsigned long bits);
void dGeomSetCollideBits (dGeomID, unsigned long bits);
unsigned long dGeomGetCategoryBits (dGeomID);
unsigned long dGeomGetCollideBits (dGeomID);

Set and get the "category" and "collide" bitfields for the given geom. These bitfields are use by spaces to govern which geoms will interact with each other. The bit fields are guaranteed to be at least 32 bits wide. The default category and collide values for newly created geoms have all bits set.

void dGeomEnable (dGeomID);
void dGeomDisable (dGeomID);
int dGeomIsEnabled (dGeomID);

Enable and disable a geom. Disabled geoms are completely ignored by dSpaceCollide and dSpaceCollide2, although they can still be members of a space.

dGeomIsEnabled() returns 1 if a geom is enabled or 0 if it is disabled. New geoms are created in the enabled state.

Collision detection

A collision detection "world" is created by making a space and then adding geoms to that space. At every time step we want to generate a list of contacts for all the geoms that intersect each other. Three functions are used to do this:

  • dCollide intersects two geoms and generates contact points.
  • dSpaceCollide determines which pairs of geoms in a space may potentially intersect, and calls a callback function with each candidate pair. This does not generate contact points directly, because the user may want to handle some pairs specially - for example by ignoring them or using different contact generating strategies. Such decisions are made in the callback function, which can choose whether or not to call dCollide for each pair.
  • dSpaceCollide2 determines which geoms from one space may potentially intersect with geoms from another space, and calls a callback function with each candidate pair. It can also test a single non-space geom against a space or treat a space of lower inclusion sublevel as a geom against a space of higher level. This function is useful when there is a collision hierarchy, i.e. when there are spaces that contain other spaces.

The collision system has been designed to give the user maximum flexibility to decide which objects will be tested against each other. This is why there are three collision functions instead of, for example, one function that just generates all the contact points.

Spaces may contain other spaces. These sub-spaces will typically represent a collection of geoms (or other spaces) that are located near each other. This is useful for gaining extra collision performance by dividing the collision world into hierarchies. Here is an example of where this is useful:

Suppose you have two cars driving over some terrain. Each car is made up of many geoms. If all these geoms were inserted into the same space, the collision computation time between the two cars would always be proportional to the total number of geoms (or even to the square of this number, depending on which space type is used).

To speed up collision a separate space is created to represent each car. The car geoms are inserted into the car-spaces, and the car-spaces are inserted into the top level space. At each time step dSpaceCollide is called for the top level space. This will do a single intersection test between the car-spaces (actually between their bounding boxes) and call the callback if they touch. The callback can then test the geoms in the car-spaces against each other using dSpaceCollide2. If the cars are not near each other then the callback is not called and no time is wasted performing unnecessary tests.

If space hierarchies are being used then the callback function may be called recursively, e.g. if dSpaceCollide calls the callback which in turn calls dSpaceCollide with the same callback function. In this case the user must make sure that the callback function is properly reentrant.

Here is a sample callback function that traverses through all spaces and sub-spaces, generating all possible contact points for all intersecting geoms:

void nearCallback (void *data, dGeomID o1, dGeomID o2)
{
    if (dGeomIsSpace (o1) || dGeomIsSpace (o2)) { 

        // colliding a space with something :
        dSpaceCollide2 (o1, o2, data,&nearCallback); 
   
        // collide all geoms internal to the space(s)
        if (dGeomIsSpace (o1))
            dSpaceCollide ((dSpaceID)o1, data, &nearCallback);
        if (dGeomIsSpace (o2))
            dSpaceCollide ((dSpaceID)o2, data, &nearCallback);

    } else {

        // colliding two non-space geoms, so generate contact
        // points between o1 and o2
        int num_contact = dCollide (o1, o2, max_contacts,contact_array, skip);
        // add these contact points to the simulation ...

    }
}

    ... // collide all objects together
    dSpaceCollide (top_level_space,0,&nearCallback);

A space callback function is not allowed to modify a space while that space is being processed with dSpaceCollide or dSpaceCollide2. For example, you can not add or remove geoms from a space, and you can not reposition the geoms within a space. Doing so will trigger a runtime error in the debug build of ODE.

Category and Collide Bitfields

Each geom has a "category" and "collide" bitfield that can be used to assist the space algorithms in determining which geoms should interact and which should not. Use of this feature is optional - by default geoms are considered to be capable of colliding with any other geom.

Each bit position in the bitfield represents a different category of object. The actual meaning of these categories (if any) is user defined. The category bitfield indicates which categories a geom is a member of. The collide bitfield indicates which categories the geom will collide with during collision detection.

A pair of geoms will be considered by dSpaceCollide and dSpaceCollide2 for passing to the callback only if one of them has a collide bit set that corresponds to a category bit in the other. The exact test is as follows:

// test if geom o1 and geom o2 can collide
cat1 = dGeomGetCategoryBits (o1);
cat2 = dGeomGetCategoryBits (o2);
col1 = dGeomGetCollideBits (o1);
col2 = dGeomGetCollideBits (o2);
if ((cat1 & col2) || (cat2 & col1)) {
    // call the callback with o1 and o2
} else {
    // do nothing, o1 and o2 do not collide
}

Note that only dSpaceCollide and dSpaceCollide2 use these bitfields, they are ignored by dCollide.

Typically a geom will belong only to a single category, so only one bit will be set in the category bitfield. The bitfields are guaranteed to be at least 32 bits wide, so the user is able to specify an arbitrary pattern of interactions for up to 32 objects. If there are more than 32 objects then some of them will obviously have to have the same category.

Sometimes the category field will contain multiple bits set, e.g. if the geom is a space them you may want to set the category to the union of all the geom categories that are contained.

Design note: Why don't we just have a single category bitfield and use the test (cat1 & cat2) ? This is simpler, but a single field requires more bits to represent some patterns of interaction. For example, if 32 geoms have an interaction pattern that is a 5 dimensional hypercube, 80 bit are required in the simpler scheme. The simpler scheme also makes it harder to determine what the categories should be for some situations.

Collision Detection Functions

int dCollide (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

Given two geoms o1 and o2 that potentially intersect, generate contact information for them. Internally, this just calls the correct class-specific collision functions for o1 and o2.

flags specifies how contacts should be generated if the geoms touch.
The lower 16 bits of flags is an integer that specifies the maximum number of contact points to generate. The number of contacts requested can't be zero.
The higher 16 bits of flags can contain any combination of the following flags:

  • CONTACTS_UNIMPORTANT - just generate any contacts (skip any contact improvements and return any contacts found as soon as possible).

All other bits in flags must be zeroed. In the future the other bits may be used to select from different contact generation strategies.

contact points to an array of dContactGeom structures. The array must be able to hold at least the maximum number of contacts. These dContactGeom structures may be embedded within larger structures in the array - the skip parameter is the byte offset from one dContactGeom to the next in the array. If skip is sizeof(dContactGeom) then contact points to a normal (C-style) array. It is an error for skip to be smaller than sizeof(dContactGeom).

If the geoms intersect, this function returns the number of contact points generated (and updates the contact array), otherwise it returns 0 (and the contact array is not touched).

If a space is passed as o1 or o2 then this function will collide all objects contained in o1 with all objects contained in o2, and return the resulting contact points. This method for colliding spaces with geoms (or spaces with spaces) provides no user control over the individual collisions. To get that control, use dSpaceCollide or dSpaceCollide2 instead.

If o1 and o2 are the same geom then this function will do nothing and return 0. Technically speaking an object intersects with itself, but it is not useful to find contact points in this case.

This function does not care if o1 and o2 are in the same space or not (or indeed if they are in any space at all).

void dSpaceCollide (dSpaceID space, void *data, dNearCallback *callback);

This determines which pairs of geoms in a space may potentially intersect, and calls the callback function with each candidate pair. The callback function is of type dNearCallback, which is defined as:

typedef void dNearCallback (void *data, dGeomID o1, dGeomID o2);

The data argument is passed from dSpaceCollide directly to the callback function. Its meaning is user defined. The o1 and o2 arguments are the geoms that may be near each other.

The callback function can call dCollide on o1 and o2 to generate contact points between each pair. Then these contact points may be added to the simulation as contact joints. The user's callback function can of course chose not to call dCollide for any pair, e.g. if the user decides that those pairs should not interact.

Other spaces that are contained within the colliding space are not treated specially, i.e. they are not recursed into. The callback function may be passed these contained spaces as one or both geom arguments.

dSpaceCollide() is guaranteed to pass all intersecting geom pairs to the callback function, but it may also make mistakes and pass non-intersecting pairs. The number of mistaken calls depends on the internal algorithms used by the space. Thus you should not expect that dCollide will return contacts for every pair passed to the callback.

void dSpaceCollide2 (dGeomID o1, dGeomID o2, void *data, dNearCallback *callback);

This function is similar to dSpaceCollide, except that it is passed two geoms (or spaces) as arguments. It calls the callback for all potentially intersecting pairs that contain one geom from o1 and one geom from o2.

The exact behavior depends on the types of o1 and o2:

  • If one argument is a non-space geom and the other is a space, the callback is called with all potential intersections between the geom and the objects in the space.
  • If both arguments are spaces and their sublevel values differ, the space with lower sublevel is treated as a geom against the space of higher sublevel (sublevel value can be assigned to a space with dSpaceSetSublevel call - the value is not tracked automatically!).
  • If both o1 and o2 are spaces of the same sublevel then this calls the callback for all potentially intersecting pairs that contain one geom from o1 and one geom from o2. The algorithm that is used depends on what kinds of spaces are being collided. If no optimized algorithm can be selected then this function will resort to one of the following two strategies:
    1. All the geoms in o1 are tested one-by-one against o2.
    2. All the geoms in o2 are tested one-by-one against o1.
    The strategy used may depend on a number of rules, but in general the space with less objects has its geoms examined one-by-one.
  • If both arguments are the same space, this is equivalent to calling dSpaceCollide on that space.
  • If both arguments are non-space geoms, this simply calls the callback once with these arguments. If this function is given a space and an geom X in that same space, this case is not treated specially. In this case the callback will always be called with the pair (X,X), because an objects always intersects with itself. The user may either test for this case and ignore it, or just pass the pair (X,X) to dCollide (which will be guaranteed to return 0).

Space functions

There are several kinds of spaces. Each kind uses different internal data structures to store the geoms, and different algorithms to perform the collision culling:

  • Simple space. This does not do any collision culling - it simply checks every possible pair of geoms for intersection, and reports the pairs whose AABBs overlap. The time required to do intersection testing for n objects is O(n2). This should not be used for large numbers of objects, but it can be the preferred algorithm for a small number of objects. This is also useful for debugging potential problems with the collision system.
  • Multi-resolution hash table space. This uses an internal data structure that records how each geom overlaps cells in one of several three dimensional grids. Each grid has cubical cells of side lengths 2i, where i is an integer that ranges from a minimum to a maximum value. The time required to do intersection testing for n objects is O(n) (as long as those objects are not clustered together too closely), as each object can be quickly paired with the objects around it.
  • Quadtree space. This uses a pre-allocated hierarchical grid-based AABB tree to quickly cull collision checks. It's exceptionally quick for large amounts of objects in landscape-shaped worlds. The amount of memory used is 4^depth * 32 bytes. Currently dSpaceGetGeom is not implemented for the quadtree space.

Here are the functions used for spaces:

dSpaceID dSimpleSpaceCreate (dSpaceID space);
dSpaceID dHashSpaceCreate (dSpaceID space);

Create a space, either of the simple or multi-resolution hash table kind. If space is nonzero, insert the new space into that space.

dSpaceID dQuadTreeSpaceCreate (dSpaceID space, dVector3 Center, dVector3 Extents, int Depth);

Creates a quadtree space. center and extents define the size of the root block. depth sets the depth of the tree - the number of blocks that are created is 4^depth.

void dSpaceDestroy (dSpaceID);

This destroys a space. It functions exactly like dGeomDestroy except that it takes a dSpaceID argument. When a space is destroyed, if its cleanup mode is 1 (the default) then all the geoms in that space are automatically destroyed as well.

void dHashSpaceSetLevels (dSpaceID space, int minlevel, int maxlevel);
void dHashSpaceGetLevels (dSpaceID space, int *minlevel, int *maxlevel);

Sets and get some parameters for a multi-resolution hash table space. The smallest and largest cell sizes used in the hash table will be 2^minlevel and 2^maxlevel respectively. minlevel must be less than or equal to maxlevel.

In dHashSpaceGetLevels the minimum and maximum levels are returned through pointers. If a pointer is zero then it is ignored and no argument is returned.

void dSpaceSetCleanup (dSpaceID space, int mode);
int dSpaceGetCleanup (dSpaceID space);

Set and get the clean-up mode of the space. If the clean-up mode is 1, then the contained geoms will be destroyed when the space is destroyed. If the clean-up mode is 0 this does not happen. The default clean-up mode for new spaces is 1.

void dSpaceSetSublevel (dSpaceID space, int sublevel);
int dSpaceGetSublevel (dSpaceID space);

Set and get sublevel value for the space. Sublevel affects how the space is handled in dSpaceCollide2 when it is collided with another space. If sublevels of both spaces match, the function iterates geometries of both spaces and collides them with each other. If sublevel of one space is greater than the sublevel of another one, only the geometries of the space with greater sublevel are iterated, another space is passed into collision callback as a geometry itself. By default all the spaces are assigned zero sublevel.

Note! The space sublevel IS NOT automatically updated when one space is inserted into another or removed from one. It is a client's responsibility to update sublevel value if necessary.

void dSpaceAdd (dSpaceID, dGeomID);

Add a geom to a space. This function can be called automatically if a space argument is given to a geom creation function.

void dSpaceRemove (dSpaceID, dGeomID);

Remove a geom from a space. This does nothing if the geom is not actually in the space. This function is called automatically by dGeomDestroy if the geom is in a space.

int dSpaceQuery (dSpaceID, dGeomID);

Return 1 if the given geom is in the given space, or return 0 if it is not.

int dSpaceGetNumGeoms (dSpaceID);

Return the number of geoms contained within a space.

dGeomID dSpaceGetGeom (dSpaceID, int i);

Return the i'th geom contained within the space. i must range from 0 to dSpaceGetNumGeoms()-1.

If any change is made to the space (including adding and deleting geoms) then no guarantee can be made about how the index number of any particular geom will change. Thus no space changes should be made while enumerating the geoms.

This function is guaranteed to be fastest when the geoms are accessed in the order 0,1,2,etc. Other non-sequential orders may result in slower access, depending on the internal implementation.

Geometry Classes

Sphere Class

dGeomID dCreateSphere (dSpaceID space, dReal radius);

Create a sphere geom of the given radius, and return its ID. If space is nonzero, insert it into that space. The point of reference for a sphere is its center.

void dGeomSphereSetRadius (dGeomID sphere, dReal radius);

Set the radius of the given sphere.

dReal dGeomSphereGetRadius (dGeomID sphere);

Return the radius of the given sphere.

dReal dGeomSpherePointDepth (dGeomID sphere, dReal x, dReal y, dReal z);

Return the depth of the point (x,y,z) in the given sphere. Points inside the geom will have positive depth, points outside it will have negative depth, and points on the surface will have zero depth.

Box Class

dGeomID dCreateBox (dSpaceID space, dReal lx, dReal ly, dReal lz);

Create a box geom of the given x/y/z side lengths (lx,ly,lz), and return its ID. If space is nonzero, insert it into that space. The point of reference for a box is its center.

void dGeomBoxSetLengths (dGeomID box, dReal lx, dReal ly, dReal lz);

Set the side lengths of the given box.

void dGeomBoxGetLengths (dGeomID box, dVector3 result);

Return in result the side lengths of the given box.

dReal dGeomBoxPointDepth (dGeomID box, dReal x, dReal y, dReal z);

Return the depth of the point (x,y,z) in the given box. Points inside the geom will have positive depth, points outside it will have negative depth, and points on the surface will have zero depth.

Plane Class

dGeomID dCreatePlane (dSpaceID space, dReal a, dReal b, dReal c, dReal d);
Create a plane geom of the given parameters, and return its ID. If space is nonzero, insert it into that space. The plane equation is
a*x+b*y+c*z = d The plane's normal vector is (a,b,c), and it must have length 1. Planes are non-placeable geoms. This means that, unlike placeable geoms, planes do not have an assigned position and rotation. This means that the parameters (a,b,c,d) are always in global coordinates. In other words it is assumed that the plane is always part of the static environment and not tied to any movable object.
void dGeomPlaneSetParams (dGeomID plane, dReal a, dReal b, dReal c, dReal d);

Set the parameters of the given plane.

void dGeomPlaneGetParams (dGeomID plane, dVector4 result);

Return in result the parameters of the given plane.

dReal dGeomPlanePointDepth (dGeomID plane, dReal x, dReal y, dReal z);

Return the depth of the point (x,y,z) in the given plane. Points inside the geom will have positive depth, points outside it will have negative depth, and points on the surface will have zero depth.

Note that planes in ODE are in fact not really planes: they are half-spaces. Anything that is moving inside the half-space will be ejected out from it. This means that planes are only planes from the perspective of one side. If you want your planes to be reversed, multiply the whole plane equation by -1.

Capsule Class

N.B. Capsule were called Capped Cylinder (CCylinder) before version 0.6

dGeomID dCreateCapsule (dSpaceID space, dReal radius, dReal length);

Create a capsule geom of the given parameters, and return its ID. If space is nonzero, insert it into that space.

A capsule is like a normal cylinder except it has half-sphere caps at its ends. This feature makes the internal collision detection code particularly fast and accurate. The cylinder's length, not counting the caps, is given by length. The cylinder is aligned along the geom's local Z axis. The radius of the caps, and of the cylinder itself, is given by radius.

void dGeomCapsuleSetParams (dGeomID capsule, dReal radius, dReal length);

Set the parameters of the given capsule.

void dGeomCapsuleGetParams (dGeomID capsule, dReal *radius, dReal *length);

Return in radius and length the parameters of the given capsule.

dReal dGeomCapsulePointDepth (dGeomID capsule, dReal x, dReal y, dReal z);

Return the depth of the point (x,y,z) in the given capsule. Points inside the geom will have positive depth, points outside it will have negative depth, and points on the surface will have zero depth.

Cylinder Class

dGeomID dCreateCylinder (dSpaceID space, dReal radius, dReal length);

Create a cylinder geom of the given parameters, and return its ID. If space is nonzero, insert it into that space.

void dGeomCylinderSetParams (dGeomID cylinder, dReal radius, dReal length);

Set the parameters of the given cylinder.

void dGeomCylinderGetParams (dGeomID cylinder, dReal *radius, dReal *length);

Return in radius and length the parameters of the given cylinder.

Ray Class

A ray is different from all the other geom classes in that it does not represent a solid object. It is an infinitely thin line that starts from the geom's position and extends in the direction of the geom's local Z-axis.

Calling dCollide between a ray and another geom will result in at most one contact point. Rays have their own conventions for the contact information in the dContactGeom structure (thus it is not useful to create contact joints from this information):

  • pos - This is the point at which the ray intersects the surface of the other geom, regardless of whether the ray starts from inside or outside the geom.
  • normal - This is the surface normal of the other geom at the contact point. if dCollide is passed the ray as its first geom then the normal will be oriented correctly for ray reflection from that surface (otherwise it will have the opposite sign).
  • depth - This is the distance from the start of the ray to the contact point.

Rays are useful for things like visibility testing, determining the path of projectiles or light rays, and for object placement.

dGeomID dCreateRay (dSpaceID space, dReal length);

Create a ray geom of the given length, and return its ID. If space is nonzero, insert it into that space.

void dGeomRaySetLength (dGeomID ray, dReal length);

Set the length of the given ray.

dReal dGeomRayGetLength (dGeomID ray);

Get the length of the given ray.

void dGeomRaySet (dGeomID ray, dReal px, dReal py, dReal pz, dReal dx, dReal dy, dReal dz);

Set the starting position (px,py,pz) and direction (dx,dy,dz) of the given ray. The ray's rotation matrix will be adjusted so that the local Z-axis is aligned with the direction. Note that this does not adjust the ray's length.

void dGeomRayGet (dGeomID ray, dVector3 start, dVector3 dir);

Get the starting position (start) and direction (dir) of the ray. The returned direction will be a unit length vector.

void dGeomRaySetParams( dGeomID ray, int FirstContact, int BackfaceCull );
void dGeomRayGetParams( dGeomID ray, int *FirstContact, int *BackfaceCull );
void dGeomRaySetClosestHit( dGeomID ray, int ClosestHit );
int  dGeomRayGetClosestHit( dGeomID ray );

Set or Get parameters for the ray which determine which hit between the ray geom and a trimesh geom is returned from dCollide.

FirstContact determines if dCollide returns the first collision detected between the ray geom and a trimesh geom, even if that collision is not the nearest to the ray start position. BackfaceCull determines if dCollide returns a collision between the ray geom and a trimesh geom when the collision is between the ray and a backfacing triangle. Default values are FirstContact = 0, BackfaceCull = 0 (both false).

ClosestHit determines if dCollide returns the closest hit between the ray and a trimesh geom. If ClosestHit is false, the hit returned by dCollide may not be the nearest collision to the ray position. This parameter is ignored if FirstContact is set to true in dGeomRaySetParams(). If ClosestHit is set to true and BackfaceCull is set to false, the hit returned by dCollide may be between the ray and a backfacing triangle. The default value is ClosestHit = 0 (false).

Convex Class

dGeomID dCreateConvex (dSpaceID space, dReal *planes, unsigned planecount, 
                       dReal *points, unsigned pointcount, unsigned *polygons);

void dGeomSetConvex (dGeomID g, dReal *planes, unsigned planecount,
                     dReal *points, unsigned pointcount, unsigned *polygons);

Triangle Mesh Class

A triangle mesh (TriMesh) represents an arbitrary collection of triangles. The triangle mesh collision system has the following features:

  • Any triangle "soup" can be represented --- i.e. the triangles are not required to have any particular strip, fan or grid structure.
  • Triangle meshes can interact with spheres, boxes, rays and other triangle meshes.
  • It works well for relatively large triangles.
  • It uses temporal coherence to speed up collision tests. When a geom has its collision checked with a trimesh once, data is stored inside the trimesh. This data can be cleared with the dGeomTriMeshClearTCCache function. In the future it will be possible to disable this functionality.

Trimesh/Trimesh collisions, perform quite well, but there are three minor caveats:

  • The stepsize you use will, in general, have to be reduced for accurate collision resolution. Non-convex shape collision is much more dependent on the collision geometry than primitive collisions. Further, the local contact geometry will change more rapidly (and in a more complex fashion) for non-convex polytopes than it does for simple, convex polytopes such as spheres and cubes.
  • In order to efficiently resolve collisions, dCollideTTL needs the positions of the colliding trimeshes in the previous timestep. This is used to calculate an estimated velocity of each colliding triangle, which is used to find the direction of impact, contact normals, etc. This requires the user to update these variables at every timestep. This update is performed outside of ODE, so it is not included in ODE itself. The code to do this looks something like this:
  const double *DoubleArrayPtr = Bodies(BodyIndex).TransformationMatrix->GetArray();
  dGeomTriMeshDataSet( TriMeshData, TRIMESH_LAST_TRANSFORMATION, (void *) DoubleArrayPtr );

The transformation matrix is the standard 4x4 homogeneous transform matrix, and the "DoubleArray" is the standard flattened array of the 16 matrix values.

NOTE: The triangle mesh class is not final, so in the future API changes might be expected.

NOTE: dInitODE() must have been called in order to successfully use Trimesh.

dTriMeshDataID dGeomTriMeshDataCreate();
void dGeomTriMeshDataDestroy (dTriMeshDataID g);

Creates and destroys a dTriMeshData object which is used to store mesh data.

void dGeomTriMeshDataBuild (dTriMeshDataID g,
                            const void* Vertices, int VertexStride, int VertexCount,
                            const void* Indices, int IndexCount, int TriStride,
                            const void* Normals);

NOTE: The argument order is non-intuitive here: stride,count for vertex data, but count,stride for index data.

Used for filling a dTriMeshData object with data. No data is copied here, so the pointers passed into this function must remain valid. This is how the strided data works:

struct StridedVertex {
    dVector3 Vertex; // 4th component can be left out, reducing memory usage
    // Userdata
};
 
int VertexStride = sizeof (StridedVertex);
struct StridedTri {
    int Indices(3);
    // Userdata
};
int TriStride = sizeof (StridedTri);

The Normals argument is optional: the normals of the faces of each trimesh object. For example,

dTriMeshDataID TriMeshData;
TriMeshData = dGeomTriMeshGetTriMeshDataID ( Bodies(BodyIndex).GeomID); // as long as dReal == floats
dGeomTriMeshDataBuildSingle (TriMeshData,
                             Bodies(BodyIndex).VertexPositions,  3*sizeof(dReal), (int) numVertices, // Vertices
                             Bodies(BodyIndex).TriangleIndices, 3*((int) NumTriangles), 3*sizeof(unsigned int), // Faces
                             Bodies(BodyIndex).FaceNormals); //  Normals

This pre-calculation saves some time during evaluation of the contacts, but isn't necessary. If you don't want to calculate the face normals before construction (or if you have enormous trimeshes and know that only very few faces will be touching and want to save time), just pass a "NULL" for the Normals argument, and dCollideTTL will take care of the normal calculations itself.

void dGeomTriMeshDataBuildSimple (dTriMeshDataID g,
                                  const dVector3*Vertices, int VertexCount,
                                  const int* Indices, int IndexCount);

Simple build function provided for convenience.

typedef int dTriCallback (dGeomID TriMesh, dGeomID RefObject, int TriangleIndex);
void dGeomTriMeshSetCallback (dGeomID g, dTriCallback *Callback);
dTriCallback* dGeomTriMeshGetCallback (dGeomID g);

Optional per triangle callback. Allows the user to say if collision with a particular triangle is wanted. If the return value is zero no contact will be generated.

typedef void dTriArrayCallback (dGeomID TriMesh, dGeomID RefObject, const int* TriIndices, int TriCount);
void dGeomTriMeshSetArrayCallback (dGeomID g, dTriArrayCallback* ArrayCallback);
dTriArrayCallback *dGeomTriMeshGetArrayCallback (dGeomID g);

Optional per geom callback. Allows the user to get the list of all intersecting triangles in one shot.

typedef int dTriRayCallback (dGeomID TriMesh, dGeomID Ray, int TriangleIndex, dReal u, dReal v);
void dGeomTriMeshSetRayCallback (dGeomID g, dTriRayCallback* Callback);
dTriRayCallback *dGeomTriMeshGetRayCallback (dGeomID g);

Optional Ray callback. Allows the user to determine if a ray collides with a triangle based on the barycentric coordinates of an intersection. The user can for example sample a bitmap to determine if a collision should occur.

dGeomID dCreateTriMesh (dSpaceID space, dTriMeshDataID Data,
                        dTriCallback *Callback,
                        dTriArrayCallback *ArrayCallback,
                        dTriRayCallback *RayCallback);

Constructor. The Data member defines the vertex data the newly created triangle mesh will use.

void dGeomTriMeshSetData (dGeomID g, dTriMeshDataID Data);

Replaces the current data.

void dGeomTriMeshClearTCCache (dGeomID g);

Clears the internal temporal coherence caches.

void dGeomTriMeshGetTriangle (dGeomID g, int Index, dVector3 *v0, dVector3 *v1, dVector3 *v2);

Retrieves a triangle in world space. The v0, v1 and v2 arguments are optional.

void dGeomTriMeshGetPoint (dGeomID g, int Index, dReal u, dReal v, dVector3 Out);

Retrieves a position in world space based on the incoming data.

void dGeomTriMeshEnableTC(dGeomID g, int geomClass, int enable);
int dGeomTriMeshIsTCEnabled(dGeomID g, int geomClass);

These functions can be used to enable/disable the use of temporal coherence during tri-mesh collision checks. Temporal coherence can be enabled/disabled per tri-mesh instance/geom class pair, currently it works for spheres and boxes. The default for spheres and boxes is 'false'.

The 'enable' param should be 1 for true, 0 for false.

Temporal coherence is optional because allowing it can cause subtle efficiency problems in situations where a tri-mesh may collide with many different geoms during its lifespan. If you enable temporal coherence on a tri-mesh then these problems can be eased by intermittently calling dGeomTriMeshClearTCCache for it.

typedef int dTriTriMergeCallback(dGeomID TriMesh, int FirstTriangleIndex, int SecondTriangleIndex);
void dGeomTriMeshSetTriMergeCallback(dGeomID g, dTriTriMergeCallback* Callback);
dTriTriMergeCallback* dGeomTriMeshGetTriMergeCallback(dGeomID g);

Allows the user to generate a fake triangle index for a new contact generated from merging of two other contacts. That index could later be used by the user to determine attributes of original triangles used as sources for a merged contact. The callback is currently used within OPCODE trimesh-sphere and OPCODE new trimesh-trimesh collisions.

If the callback is not assigned (the default) -1 is generated as triangle index for merged contacts.

NOTE: Before this API was introduced, the index was always set to the first triangle index.

Heightfield Class

dHeightfield is a regular grid heightfield collider. It can be used for heightmap terrains, but also for deformable animated water surfaces.

Heightfield Data

The dHeightfieldData is a storage class, similar to the dTrimeshData class, that holds all geom properties and optionally height sample data.

dHeightfieldDataID dGeomHeightfieldDataCreate ();
void dGeomHeightfieldDataDestroy (dHeightfieldDataID d)

Allocate and destroy dHeightfieldDataID objects. You must call dGeomHeightfieldDataDestroy to destroy it after the geom has been removed. The dHeightfieldDataID value is used when specifying a data format type.

Building from existing data

There are four functions to easily build a heightfield from an array of height values of different data types. They all have the same parameters, except the data type of the pHeightData pointer is different:

void dGeomHeightfieldDataBuildByte   (dHeightfieldDataID d,
                                      const unsigned char *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap);

void dGeomHeightfieldDataBuildShort  (dHeightfieldDataID d,
                                      const short *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap);

void dGeomHeightfieldDataBuildSingle (dHeightfieldDataID d,
                                      const float *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap);

void dGeomHeightfieldDataBuildDouble (dHeightfieldDataID d,
                                      const double *pHeightData,
                                      int bCopyHeightData,
                                      dReal width, dReal depth,
                                      int widthSamples, int depthSamples,
                                      dReal scale, dReal offset, dReal thickness, int bWrap);

Loads heightfield sample data into the HeightfieldData structure. Before a dHeightfieldDataID can be used by a geom it must be configured to specify the format of the height data. These calls all take the same parameters as listed below; the only difference is the data type pointed to by pHeightData.

  • pHeightData is a pointer to the height data;
  • bCopyHeightData specifies whether the height data should be copied to a local store. If zero, the data is accessed by reference and so must persist throughout the lifetime of the heightfield;
  • width, height are the world space heightfield dimensions on the geom's local X and Z axes;
  • widthSamples, depthSamples specifies the number of vertices to sample along the width and depth of the heightfield. Naturally this value must be at least two or more;
  • scale is the vertical sample height multiplier, a uniform scale applied to all raw height data;
  • offset is the vertical sample offset, added to the scaled height data;
  • thickness is the thickness of AABB which is added below the lowest point, to prevent objects from falling through very thin heightfields;
  • bWrap is 0 if the heightfield should be finite, 1 if should tile infinitely.
Retrieving data from a callback
typedef dReal (*dHeightfieldGetHeight) (void *userdata, int x, int z);

void dGeomHeightfieldDataBuildCallback (dHeightfieldDataID d,
                                        void *pUserData,
                                        dHeightfieldGetHeight *pCallback,
                                        dReal width, dReal depth,
                                        int widthSamples, int depthSamples,
                                        dReal scale, dReal offset, dReal thickness, int bWrap);

This call specifies that the heightfield data is computed by the user and it should use the given callback when determining the height of a given element of its shape. The callback function is called while the simulation runs, and returns the value of the heightmap ("y" value) at a given (x,z) position.

  • pUserData is a pointer for arbitrary user-defined data to pass to the callback
  • pCallback is a pointer to the callback function
  • The other arguments are the same as the other dGeomHeightfieldDataBuild* functions.
Setting terrain bounds
void dGeomHeightfieldDataSetBounds (dHeightfieldDataID d, dReal min_height, dReal max_height)

Sets the minimum and maximum height sample bounds in sample space. ODE does not automatically detect the sample data minimum and maximum height bounds, this must be done manually (for added flexibility and allows the user to control the process).

The default vertical sample bounds are infinite, so when the sample bounds are known, call this function for improved performance. Also, if the geom (built from this data) is rotated, its AABB will end up with NaNs, and break the collision detection; so always set the heightfield data's bounds to finite values if you are going to rotate the geom.

Heightfield Geom

dGeomID dCreateHeightfield(dSpaceID space, dHeightfieldDataID data, int bPlaceable);

Uses the information in the given dHeightfieldDataID to construct a geom representing a heightfield in a collision space.

  • dHeightfieldDataID is the heightfield data object
  • bPlaceable defines whether this geom can be transformed in the world using the usual functions such as dGeomSetPosition and dGeomSetRotation. If the geom is not set as placeable, then it uses a fixed orientation where the global Y axis represents the 'height' of the heightfield.
void dGeomHeightfieldSetHeightfieldData(dGeomID g, dHeightfieldDataID Data);
dHeightfieldDataID dGeomHeightfieldGetHeightfieldData(dGeomID g);

Set and retrieve the heightfield data object of this geom.


Geometry Transform Class

The geom transform classes are deprecated. Use geom offsets instead.

dGeomID dCreateGeomTransform (dSpaceID space);
void dGeomTransformSetGeom (dGeomID g, dGeomID obj);
dGeomID dGeomTransformGetGeom (dGeomID g);
void dGeomTransformSetCleanup (dGeomID g, int mode);
int dGeomTransformGetCleanup (dGeomID g);
void dGeomTransformSetInfo (dGeomID g, int mode);
int dGeomTransformGetInfo (dGeomID g);

If your code uses geom transforms, update it to use geom offsets instead, as soon as possible. The geom transform functions will be removed from the next release.

User defined classes

ODE's geometry classes are implemented internally as C++ classes. If you want to define your own geometry classes you can do this in two ways:

  • Use the C functions in this section. This has the advantage of providing a clean separation between your code and ODE.
  • Add the classes directly to ODE's source code. This has the advantage that you can use C++ so the implementation will potentially be a bit cleaner. This is also the preferred method if your collision class is generally useful and you want to contribute it to the public source base.

What follows is the C API for user defined geometry classes.

Every user defined geometry class has a unique integer number. A new geometry class (call it 'X') must provide the following to ODE:

  • Functions that will handle collision detection and contact generation between X and one or more other classes. These functions must be of type dColliderFn, which is defined as:
typedef int dColliderFn (dGeomID o1, dGeomID o2, int flags, dContactGeom *contact, int skip);

This has exactly the same interface as dCollide. Each function will handle a specific collision case, where o1 has type X and o2 has some other known type.

  • A "selector" function, of type dGetColliderFnFn, which is defined as:
typedef dColliderFn * dGetColliderFnFn (int num);

This function takes a class number (num), and returns the collider function that can handle colliding X with class num. It should return 0 if X does not know how to collide with class num. Note that if classes X and Y are to collide, only one needs to provide a function to collide with the other.

This function is called infrequently - the return values are cached and reused.

  • A function that will compute the axis aligned bounding box (AABB) of instances of this class. This function must be of type dGetAABBFn, which is defined as:
typedef void dGetAABBFn (dGeomID g, dReal aabb[6]);

This function is given g, which has type X, and returns the axis-aligned bounding box for g. The aabb array has elements (minx, maxx, miny, maxy, minz, maxz). If you don't want to compute tight bounds for the AABB, you can just supply a pointer to dInfiniteAABB, which returns +/- infinity in each direction.

  • The number of bytes of "class data" that instances of this class need. For example a sphere stores its radius in the class data area, and a box stores its side lengths there.

The following things are optional for a geometry class:

  • A function that will destroy the class data. Most classes will not need this function, but some will want to deallocate heap memory or release other resources. This function must be of type dGeomDtorFn, which is defined as:
typedef void dGeomDtorFn (dGeomID o);

The argument o has type X.

  • A function that will test whether a given AABB intersects with an instance of X. This is used as an early-exit test in the space collision functions. This function must be of type dAABBTestFn, which is defined as:
typedef int dAABBTestFn (dGeomID o1, dGeomID o2, dReal aabb2[6]);

The argument o1 has type X. If this function is provided it is called by dSpaceCollide when o1 intersects geom o2, which has an AABB given by aabb2. It returns 1 if aabb2 intersects o1, or 0 if it does not.

This is useful, for example, for large terrains. Terrains typically have very large AABBs, which are not very useful to test intersections with other objects. This function can test another object's AABB against the terrain without going to the computational trouble of calling the specific collision function. This has an especially big savings when testing against GeomGroup objects.

Here are the functions used to manage custom classes:

int dCreateGeomClass (const dGeomClass *classptr);

Register a new geometry class, defined by classptr. The number of the new class is returned. The convention used in ODE is to assign the class number to a global variable with the name dXxxClass where Xxx is the class name (e.g. dSphereClass).

Here is the definition of the dGeomClass structure:

struct dGeomClass {
    int bytes; // bytes of custom data needed
    dGetColliderFnFn *collider; // collider function
    dGetAABBFn *aabb; // bounding box function
    dAABBTestFn *aabb_test; // aabb tester, can be 0 for none
    dGeomDtorFn *dtor; // destructor, can be 0 for none
};
void * dGeomGetClassData (dGeomID);

Given a geom, return a pointer to the class's custom data (this will be a block of the required number of bytes).

dGeomID dCreateGeom (int classnum);

Create a geom of the given class number. The custom data block will initially be set to 0. This object can be added to a space using dSpaceAdd.

When you implement a new class you will usually write a function that does the following:

  • If the class has not yet been created, create it. You should be careful to only ever create the class once.
  • Call dCreateGeom to make an instance of the class.
  • Set up the custom data area.

Composite objects

Consider the following objects:

  • A table that is made out of a box for the top and a box for each leg.
  • A branch of a tree that is modeled from several cylinders joined together.
  • A molecule that has spheres representing each atom.

If these objects are meant to be rigid then it is necessary to use a single rigid body to represent each of them. But it might seem that performing collision detection is a problem, because there is no single geometry class that can represent a complex shape like a table or a molecule. The solution is to use a composite collision object that is a combination of several geoms.

No extra functions are needed to manage composite objects: simply create each component geom and attach it to the same body. To move and rotate the separate geoms with respect to each other in the same object, geometry offsets can be used. That's all there is to it!

However there is one caveat: You should never create a composite object that will result in collision points being generated very close together. For example, consider a table that is made up of a box for the top and four boxes for the legs. If the legs are flush with the top, and the table is lying on the ground on its side, then the contact points generated for the boxes may coincide where the legs join to the top. ODE does not currently optimize away coincident contact points, so this situation can lead to numerical errors and strange behavior.

In this example the table geometry should be adjusted so that the legs are not flush with the sides, making it much more unlikely that coincident contact points will be generated. In general, avoid having different contact surfaces that overlap, or that line up along their edges.

Utility functions

void dClosestLineSegmentPoints (const dVector3 a1, const dVector3 a2,
                                const dVector3 b1, const dVector3 b2,
                                dVector3 cp1, dVector3 cp2);

Given two line segments A and B with endpoints a1-a2 and b1-b2, return the points on A and B that are closest to each other (in cp1 and cp2). In the case of parallel lines where there are multiple solutions, a solution involving the endpoint of at least one line will be returned. This will work correctly for zero length lines, e.g. if a1==a2 and/or b1==b2.

int dBoxTouchesBox (const dVector3 p1, const dMatrix3 R1, const dVector3 side1,
                    const dVector3 p2, const dMatrix3 R2, const dVector3 side2);

Given boxes (p1,R1,side1) and (p2,R2,side2), return 1 if they intersect or 0 if not. p is the center of the box, R is the rotation matrix for the box, and side is a vector of x/y/z side lengths.

void dInfiniteAABB (dGeomID geom, dReal aabb[6]);

This function can be used as the AABB-getting function in a geometry class, if you don't want to compute tight bounds for the AABB. It returns +/- infinity in each direction.

Implementation notes

Large Environments

Often the collision world will contain many objects that are part of the static environment, that are not associated with rigid bodies. ODE's collision detection is optimized to detect geoms that do not move and to precompute as much information as possible about these objects to save time. For example, bounding boxes and internal collision data structures are precomputed.

Using a Different Collision Library

Using ODE's collision detection is optional - an alternative collision library can be used as long as it can supply dContactGeom structures to initialize contact joints.

The dynamics core of ODE is mostly independent of the collision library that is used, except for four points:

  • The dGeomID type must be defined, as each body can store a pointer to the first geometry object that it is associated with.
  • The dGeomMoved() function must be defined, with the following prototype:
void dGeomMoved (dGeomID);

This function is called by the dynamics code whenever a body moves: it indicates that the geometry object associated with the body is now in a new position.

  • The dGeomGetBodyNext() function must be defined, with the following prototype:
dGeomID dGeomGetBodyNext (dGeomID);

This function is called by the dynamics code to traverse the list of geoms that are associated with each body. Given a geom attached to a body, it returns the next geom attached to that body, or 0 if there are no more geoms.

  • The dGeomSetBody() function must be defined, with the following prototype:
void dGeomSetBody (dGeomID, dBodyID);

This function is called in the body destructor code (with the second argument set to 0) to remove all references from the geom to the body.

If you want an alternative collision library to get body-movement notifications from ODE, you should define these types and functions appropriately.