Main Page | Namespace List | Class Hierarchy | Alphabetical List | Class List | File List | Namespace Members | Class Members | File Members | Related Pages

physics/OBBCollisionModel.cpp

Go to the documentation of this file.
00001 /****************************************************************************
00002   Copyright (C)1996 David Jung <opensim@pobox.com>
00003 
00004   This program/file is free software; you can redistribute it and/or modify
00005   it under the terms of the GNU General Public License as published by
00006   the Free Software Foundation; either version 2 of the License, or
00007   (at your option) any later version.
00008   
00009   This program is distributed in the hope that it will be useful,
00010   but WITHOUT ANY WARRANTY; without even the implied warranty of
00011   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00012   GNU General Public License for more details. (http://www.gnu.org)
00013   
00014   You should have received a copy of the GNU General Public License
00015   along with this program; if not, write to the Free Software
00016   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00017   
00018   $Id: OBBCollisionModel.cpp 1031 2004-02-11 20:46:36Z jungd $
00019   $Revision: 1.6 $
00020   $Date: 2004-02-11 15:46:36 -0500 (Wed, 11 Feb 2004) $
00021   $Author: jungd $
00022  
00023 ****************************************************************************/
00024 
00025 #include <physics/OBBCollisionModel>
00026 
00027 #include <physics/Box>
00028 #include <gfx/TriangleContainer>
00029 #include <gfx/TriangleIterator>
00030 #include <gfx/Triangle3>
00031 #include <gfx/Color4>
00032 
00033 #include <osg/Node>
00034 #include <osg/Group>
00035 #include <osg/MatrixTransform>
00036 #include <osg/StateSet>
00037 #include <osg/Material>
00038 #include <osg/Vec4>
00039 #include <osg/PolygonMode>
00040 
00041 
00042 
00043 using physics::OBBCollisionModel;
00044 using physics::Box;
00045 
00046 using gfx::Triangle3;
00047 using gfx::Color4;
00048 using gfx::TriangleContainer;
00049 using gfx::TriangleIterator;
00050 
00051 using base::Matrix3;
00052 using base::transpose;
00053 using base::cross;
00054 using base::array;
00055 
00056 using osg::Vec4;
00057 using osg::StateSet;
00058 
00059 
00060 OBBCollisionModel::OBBCollisionModel(const gfx::TriangleContainer& triangles)
00061   : b(*new base::array<OBB>()), tris(*new base::array<Triangle3>())
00062 {
00063   // Copy all the triangles into an array (tris)
00064   Int id=0;
00065   
00066   TriangleContainer::const_iterator t = triangles.begin();
00067   TriangleContainer::const_iterator end = triangles.end();
00068   while (t != end) {
00069     tris.at(id++) = (*t);
00070     ++t;
00071   }
00072   tris.trim();
00073 
00074   if (!tris.empty()) {
00075     buildHierarchy(); // Build the model
00076   }
00077 
00078 }
00079 
00080 OBBCollisionModel::OBBCollisionModel(const OBBCollisionModel& cm)
00081   : b(*new base::array<OBB>(cm.b)),
00082     tris(*new base::array<Triangle3>(cm.tris))
00083 {
00084 }
00085 
00086 OBBCollisionModel::~OBBCollisionModel()
00087 {
00088   delete &tris;
00089   delete &b;
00090 }
00091 
00092 
00093 void OBBCollisionModel::buildHierarchy()
00094 {
00095   array<Moment> moment(tris.size());
00096   array<OBB>& boxes(b);
00097   Int OBBsInited = 1; 
00098   boxes.resize(tris.size()*2);
00099   
00100   // Determine initial orientation, mean point, and splitting axis.
00101   Moment  M;
00102   Matrix3 C;
00103   
00104   Moment::computeMoment(moment, tris, 0, tris.size());
00105 
00106   M.clear();
00107   for(Int i=0; i<tris.size(); i++) {
00108     M.accumulate(moment[i]);
00109   }
00110 
00111   b[0].pT = M.meanFromAccum();
00112   C = M.covariance();
00113   
00114   eigenAndSort1(b[0].pR, C);
00115   
00116   // create the index list
00117   Int* t = new Int[tris.size()];
00118   for(Int i=0; i<tris.size(); i++) t[i]=i;
00119   
00120   // do the build
00121   OBBsInited = b[0].splitRecurse(tris,moment,boxes,OBBsInited,t,0,tris.size());
00122   
00123   delete[] t;
00124 }
00125 
00126 
00127 Int OBBCollisionModel::eigenAndSort1(Matrix3& evecs, const Matrix3& cov, Int maxIterations)
00128 {
00129   Vector3 evals;
00130   
00131   Int n = cov.eigenJacobi(evecs, evals, maxIterations);
00132 
00133   // not a full sort -- just makes column 1 the largest
00134   if (evals.z > evals.x) {
00135     if (evals.z > evals.y) {
00136       // 3 is largest, swap with column 1
00137       evecs.swapColumns(3,1);
00138     }
00139     else {
00140       // 2 is largest, swap with column 1
00141       evecs.swapColumns(2,1);
00142     }
00143   }
00144   else {
00145     if (evals.x > evals.y) {
00146       // 1 is largest, do nothing
00147     }
00148     else {
00149       // 2 is largest
00150       evecs.swapColumns(1,2);
00151     }
00152   }
00153   
00154   // we are returning the number of iterations eigenJacobi took.
00155   // too many iterations means our chosen orientation is bad.
00156   return n; 
00157 }
00158 
00159 
00160 
00161 
00162 
00163 
00164 
00165 
00166 
00167 // OBB implementation
00168 
00169 void OBBCollisionModel::OBB::reaccumMoments(Moment& A, base::array<Moment>& moment,
00170                                             Int t[], Int f, Int n)
00171 {
00172   A.clear();
00173   for(Int i=0;i<n;i++) 
00174     A.accumulate(moment[t[f+i]]);
00175 }
00176 
00177 
00178 Int OBBCollisionModel::OBB::splitRecurse(base::array<gfx::Triangle3>& tri, 
00179                                          base::array<Moment>& moment, 
00180                                          base::array<OBB>& boxes,
00181                                          Int OBBsInited, Int t[], Int f, Int n)
00182 {
00183   // The orientation for the parent box is already assigned to this->pR.
00184   // The axis along which to split will be column 0 of this->pR.
00185   // The mean point is passed in on this->pT.
00186   
00187   // When this routine completes, the position and orientation in model
00188   // space will be established, as well as its dimensions.  Child boxes
00189   // will be constructed and placed in the parent's CS.
00190   
00191   if (n == 1)
00192     return splitRecurse(tri,moment,boxes,OBBsInited,t,f);
00193   
00194   
00195   // walk along the tris for the box, and do the following:
00196   //   1. collect the max and min of the vertices along the axes of <or>.
00197   //   2. decide which group the triangle goes in, performing appropriate swap.
00198   //   3. accumulate the mean point and covariance data for that triangle.
00199   
00200   Moment M1, M2;
00201   Matrix3 C;
00202   Vector3 c;
00203   Vector3 minval, maxval;
00204   
00205   Int in;
00206   Int i;
00207   Real axdmp;
00208   Int n1 = 0;  // The number of tris in group 1.  
00209   // Group 2 will have n - n1 tris.
00210         
00211   // project approximate mean point onto splitting axis, and get coord.
00212   axdmp = (pR.e(1,1) * pT.x + pR.e(2,1) * pT.y + pR.e(3,1) * pT.z);
00213   
00214   M1.clear();
00215   M2.clear();
00216 
00217   Matrix3 pRT = transpose(pR);
00218   
00219   c = pRT * tri[t[f]].p1();
00220   
00221   minval = c;
00222   maxval = c;
00223 
00224   for(i=0; i<n; i++) {
00225     in = t[f+i];
00226     Triangle3& ptr(tri[in]);
00227     
00228     c = pRT*ptr.p1();
00229     minmax(minval, maxval, c);
00230     
00231     c = pRT*ptr.p2();
00232     minmax(minval, maxval, c);
00233     
00234     c = pRT*ptr.p3();
00235     minmax(minval, maxval, c);
00236     
00237     // grab the mean point of the in'th triangle, project
00238     // it onto the splitting axis (1st column of pR) and
00239     // see where it lies with respect to axdmp.
00240     c = moment[in].meanFromMoment();
00241     
00242     if ( (((pR.e(1,1)*c.x + pR.e(2,1)*c.y + pR.e(3,1)*c.z) < axdmp)
00243           && (n!=2)) || ((n==2) && (i==0)) ) {
00244       // accumulate first and second order moments for group 1
00245       M1.accumulate(moment[in]);
00246       
00247       // put it in group 1 by swapping t[i] with t[n1]
00248       Int temp = t[f+i];
00249       t[f+i] = t[f+n1];
00250       t[f+n1] = temp;
00251       n1++;
00252     }
00253     else {
00254       // accumulate first and second order moments for group 2
00255       M2.accumulate(moment[in]);
00256       
00257       // leave it in group 2
00258       // do nothing...it happens by default
00259     }
00260   }
00261   
00262   // done using this.pT as a mean point.
00263   
00264   // error check!
00265   if ((n1 == 0) || (n1 == n)) {
00266     // our partitioning has failed: all the triangles fell into just
00267     // one of the groups.  So, we arbitrarily partition them into
00268     // equal parts, and proceed.
00269     //Debugln("Warning: Triangle partitioning failed. Partitioning equally.");    
00270     n1 = n/2;
00271     
00272     // now recompute accumulated stuff
00273     reaccumMoments(M1, moment, t, f, n1);
00274     reaccumMoments(M2, moment, t, f + n1, n - n1);
00275   }
00276   
00277   // With the max and min data, determine the center point and dimensions
00278   // of the parent box.
00279   
00280   c = (minval+maxval)/2.0;
00281   
00282   pT = pR*c; // equiv of above?? !!!
00283   d = (maxval-minval)/2.0;
00284   
00285   // allocate new boxes
00286   P = &boxes[OBBsInited++];
00287   N = &boxes[OBBsInited++];
00288         
00289   // Compute the orienations for the child boxes (eigenvectors of
00290   // covariance matrix).  Select the direction of maximum spread to be
00291   // the split axis for each child.
00292   
00293   Matrix3 tR;
00294   
00295   // do the P child
00296   if (n1 > 1) {
00297     P->pT = M1.meanFromAccum();
00298     C = M1.covariance();
00299     
00300     if (OBBCollisionModel::eigenAndSort1(tR, C, 30) >= 30) {
00301       // unable to find an orientation.  We'll just pick identity.
00302       tR.setIdentity();
00303     }
00304     
00305     P->pR = tR;
00306     OBBsInited = P->splitRecurse(tri, moment, boxes, OBBsInited, t, f, n1);
00307   }
00308   else {
00309     OBBsInited = P->splitRecurse(tri, moment, boxes, OBBsInited, t, f);
00310   }
00311   C = P->pR; P->pR = transpose(pR)*C;  // and F1 
00312   c = P->pT - pT; P->pT = transpose(pR)*c;
00313   
00314   // do the N child
00315   if ((n-n1) > 1) {
00316     N->pT = M2.meanFromAccum();
00317     C = M2.covariance();
00318     
00319     if (OBBCollisionModel::eigenAndSort1(tR, C) > 30) {
00320       // unable to find an orientation.  We'll just pick identity.
00321       tR.setIdentity();
00322     }
00323     
00324     N->pR = tR;
00325     OBBsInited = N->splitRecurse(tri, moment, boxes, OBBsInited, t, f + n1, n - n1);
00326   }
00327   else {
00328     OBBsInited = N->splitRecurse(tri, moment, boxes, OBBsInited, t, f+n1);
00329   }
00330   C = N->pR; N->pR = transpose(pR)*C;
00331   c = N->pT-pT; N->pT = transpose(pR)*c;
00332   
00333   return OBBsInited;
00334 }
00335 
00336 
00337 // specialized for leaf nodes
00338 Int OBBCollisionModel::OBB::splitRecurse(base::array<gfx::Triangle3>& tri, 
00339                                          base::array<Moment>& moment, 
00340                                          base::array<OBB>& boxes,
00341                                          Int OBBsInited, Int t[], Int f)
00342 {
00343   // For a single triangle, orientation is easily determined.
00344   // The major axis is parallel to the longest edge.
00345   // The minor axis is normal to the triangle.
00346   // The in-between axis is determine by these two.
00347   
00348   // this.pR, this.d, and this.pT are set herein.
00349   
00350   P = N = 0;
00351   Triangle3& ptr(tri[t[f]]);
00352   
00353   // Find the major axis: parallel to the longest edge.
00354   Vector3 u12, u23, u31;
00355   
00356   // First compute the squared-lengths of each edge
00357   u12 = ptr.p1()-ptr.p2();
00358   Real d12 = u12.norm();
00359   u23 = ptr.p2()-ptr.p3();  
00360   Real d23 = u23.norm();
00361   u31 = ptr.p3()-ptr.p1();  
00362   Real d31 = u31.norm();
00363   
00364   // Find the edge of longest squared-length, normalize it to
00365   // unit length, and put result into a0.
00366   Vector3 a0;
00367   if (d12 > d23) {
00368     if (d12 > d31) 
00369       a0 = u12 / base::sqrt(d12);
00370     else 
00371       a0 = u31 / base::sqrt(d31);
00372   }
00373   else {
00374     if (d23 > d31) 
00375       a0 = u23 / base::sqrt(d23);
00376     else 
00377       a0 = u31 / base::sqrt(d31);
00378   }
00379   
00380   // Now compute unit normal to triangle, and put into a2.
00381   Vector3 a2;
00382   a2 = cross(u12,u23);
00383   a2.normalize();
00384   
00385   // a1 is a2 cross a0.
00386   Vector3 a1;
00387   a1 = cross(a2,a0);
00388   
00389   // Now make the columns of this.pR the vectors a0, a1, and a2.
00390   pR.e(1,1) = a0.x;  pR.e(1,2) = a1.x;  pR.e(1,3) = a2.x;
00391   pR.e(2,1) = a0.y;  pR.e(2,2) = a1.y;  pR.e(2,3) = a2.y;
00392   pR.e(3,1) = a0.z;  pR.e(3,2) = a1.z;  pR.e(3,3) = a2.z;
00393   
00394   // Now compute the maximum and minimum extents of each vertex 
00395   // along each of the box axes.  From this we will compute the 
00396   // box center and box dimensions.
00397   Vector3 minval, maxval;
00398   Vector3 c;
00399   
00400   c = transpose(pR)*ptr.p1();
00401   minval=c; maxval=c;
00402   
00403   c = transpose(pR)*ptr.p2();
00404   minmax(minval, maxval, c);
00405   
00406   c = transpose(pR)*ptr.p3();
00407   minmax(minval, maxval, c);
00408   
00409   // With the max and min data, determine the center point and dimensions
00410   // of the box
00411   c = (minval+maxval)/2.0;
00412   
00413   pT = pR*c; // !!! check equiv of above        
00414   d = (maxval-minval)/2.0;
00415   
00416   // Assign the one triangle to this box
00417   tr = ptr;
00418   
00419   return OBBsInited;
00420 }
00421 
00422 
00423 void OBBCollisionModel::OBB::minmax(gfx::Vector3& min, gfx::Vector3& max, 
00424                                     const gfx::Vector3& v)
00425 {
00426   if (v.x < min.x) 
00427     min.x = v.x;
00428   else
00429     if (v.x > max.x)
00430       max.x = v.x;
00431   if (v.y < min.y) 
00432     min.y = v.y;
00433   else
00434     if (v.y > max.y)
00435       max.y = v.y;
00436   if (v.z < min.z) 
00437     min.z = v.z;
00438   else
00439     if (v.z > max.z)
00440       max.z = v.z;
00441 }
00442 
00443 
00444 
00445 // Moment implementation
00446 
00447 void OBBCollisionModel::Moment::clear()
00448 {
00449   A = 0;
00450   m.setZero();
00451   s.setZero();
00452 }
00453 
00454 
00455 void OBBCollisionModel::Moment::accumulate(const Moment& b)
00456 {
00457   m += b.m*b.A;
00458   s += b.s;
00459   A += b.A;
00460 }
00461 
00462 
00463 base::Matrix3 OBBCollisionModel::Moment::covariance() const 
00464 {
00465   base::Matrix3 C = s;
00466   C.e(1,1) -= m.x*m.x/A;
00467   C.e(1,2) -= m.x*m.y/A;
00468   C.e(1,3) -= m.x*m.z/A;
00469   C.e(2,1) -= m.y*m.x/A;
00470   C.e(2,2) -= m.y*m.y/A;
00471   C.e(2,3) -= m.y*m.z/A;
00472   C.e(3,1) -= m.z*m.x/A;
00473   C.e(3,2) -= m.z*m.y/A;
00474   C.e(3,3) -= m.z*m.z/A;
00475   return C;
00476 }
00477 
00478 
00479 void OBBCollisionModel::Moment::computeMoment(Moment& M, const base::Vector3& p,
00480                                               const base::Vector3& q, const base::Vector3& r)
00481 {
00482   Vector3 u,v,w;
00483   
00484   // compute the area of the triangle
00485   u = q-p;
00486   v = r-p;
00487   w = cross(u,v);
00488   M.A = 0.5*w.length();
00489   
00490   if (M.A == 0.0) {
00491     // This triangle has zero area.  The second order components
00492     // would be eliminated with the usual formula, so, for the 
00493     // sake of robustness we use an alternative form.  These are the 
00494     // centroid and second-order components of the triangle's vertices.
00495     
00496     // centroid
00497     M.m = (p+q+r)/3.0;
00498     
00499     // second-order components
00500     M.s.e(1,1) = (p.x*p.x + q.x*q.x + r.x*r.x);
00501     M.s.e(1,2) = (p.x*p.y + q.x*q.y + r.x*r.y);
00502     M.s.e(1,3) = (p.x*p.z + q.x*q.z + r.x*r.z);
00503     M.s.e(2,2) = (p.y*p.y + q.y*q.y + r.y*r.y);
00504     M.s.e(2,3) = (p.y*p.z + q.y*q.z + r.y*r.z);
00505     M.s.e(3,3) = (p.z*p.z + q.z*q.z + r.z*r.z);      
00506     M.s.e(3,2) = M.s.e(2,3);
00507     M.s.e(2,1) = M.s.e(1,2);
00508     M.s.e(3,1) = M.s.e(1,3);
00509     
00510     return;
00511   }
00512   
00513   // get the centroid
00514   M.m = (p+q+r)/3.0;
00515   
00516   // get the second order components -- note the weighting by the area
00517   M.s.e(1,1) = M.A*(9.0*M.m.x*M.m.x+p.x*p.x+q.x*q.x+r.x*r.x)/12.0;
00518   M.s.e(1,2) = M.A*(9.0*M.m.x*M.m.y+p.x*p.y+q.x*q.y+r.x*r.y)/12.0;
00519   M.s.e(1,3) = M.A*(9.0*M.m.x*M.m.z+p.x*p.z+q.x*q.z+r.x*r.z)/12.0;
00520   M.s.e(2,2) = M.A*(9.0*M.m.y*M.m.y+p.y*p.y+q.y*q.y+r.y*r.y)/12.0;
00521   M.s.e(2,3) = M.A*(9.0*M.m.y*M.m.z+p.y*p.z+q.y*q.z+r.y*r.z)/12.0;
00522   M.s.e(3,3) = M.A*(9.0*M.m.z*M.m.z+p.z*p.z+q.z*q.z+r.z*r.z)/12.0;
00523   M.s.e(3,2) = M.s.e(2,3);
00524   M.s.e(2,1) = M.s.e(1,2);
00525   M.s.e(3,1) = M.s.e(1,3);
00526 }
00527 
00528 
00529 void OBBCollisionModel::Moment::computeMoment(base::array<Moment>& M,
00530                                               base::array<gfx::Triangle3>& tris,
00531                                               Int firstTri, Int numTris)
00532 {
00533   // check implementation. (tri *tris, int num_tris)
00534   
00535   // first collect all the moments, and obtain the area of the 
00536   // smallest nonzero area triangle.
00537   Real Amin = 0.0;
00538   bool zero=false;
00539 
00540   Triangle3 tri;
00541   Moment m;
00542   for(Int i=firstTri; i<firstTri+numTris; i++) {
00543     Moment& m(M[i]);
00544     tri = tris[i];
00545     computeMoment(m, tri.p1(), tri.p2(), tri.p3());
00546 
00547     if (m.A == 0.0)
00548       zero=true;
00549     else {
00550       if (Amin == 0.0) 
00551         Amin = m.A;
00552       else 
00553         if (m.A < Amin)
00554           Amin = m.A;
00555     }
00556     
00557   }
00558   
00559   if (zero) {
00560     Logfln("Warning: Some triangles have zero area.");
00561 
00562     // if there are any zero area triangles, go back and set their area
00563     // if ALL the triangles have zero area, then set the area thingy
00564     // to some arbitrary value.
00565     if (Amin == 0.0) Amin = 1.0;
00566     
00567     for(Int i=firstTri; i<firstTri+numTris; i++) {
00568       Moment& m(M[i]);
00569       if ( m.A == 0.0) 
00570         m.A = Amin;
00571     }
00572   }
00573 }
00574 
00575 
00576 
00577 osg::Node* OBBCollisionModel::createOSGVisual(Visual::Attributes visualAttributes) const
00578 {
00579   if (!(visualAttributes & ShowCollisionModel)
00580       || tris.empty() ) 
00581     return new osg::Node();
00582   
00583   osg::Node* node = createOBBVisualRecurse(b[0], 0);
00584   node->setName("debug");
00585 
00586   // Set state to be transparent, random colour
00587   StateSet* state = new osg::StateSet();
00588   osg::Material* mat = new osg::Material();
00589   Vec4 col( Math::random(), Math::random(), Math::random(), 1.0);
00590   mat->setEmission( osg::Material::FRONT_AND_BACK, Vec4(0,0,0,0) );
00591   mat->setAmbient( osg::Material::FRONT_AND_BACK, col );
00592   mat->setDiffuse( osg::Material::FRONT_AND_BACK, col );
00593   mat->setSpecular( osg::Material::FRONT_AND_BACK, Vec4(1,1,1,0) );
00594   mat->setShininess( osg::Material::FRONT_AND_BACK, 0.3);
00595   state->setAttribute( mat );
00596   state->setMode(GL_CULL_FACE,osg::StateAttribute::OFF);
00597 
00598   osg::PolygonMode* polyMode = new osg::PolygonMode;
00599   polyMode->setMode( osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE );
00600   state->setAttributeAndModes(polyMode,osg::StateAttribute::ON);
00601 
00602   node->setStateSet(state);
00603   
00604   return node;
00605 }
00606 
00607 osg::Node* OBBCollisionModel::createOBBVisualRecurse(OBB& obb, Int level) const
00608 {
00609   const Real epsOffset = 0.01;
00610 
00611   osg::MatrixTransform* tn = new osg::MatrixTransform();
00612   tn->setName("OBB");
00613   //  tn->setType(osg::Transform::STATIC); // is there an equiv??? !!!
00614   Matrix4 m(obb.pR);
00615   m.setTranslationComponent(obb.pT);
00616   tn->setMatrix(m);
00617   
00618   if (!onlyOneLevel || (onlyLevel==level)) {
00619     ref<Box> box(NewObj Box(2*obb.d.x+epsOffset,2*obb.d.y+epsOffset,2*obb.d.z+epsOffset));
00620     Debugln(Physics,"box leaf " << level);
00621     StateSet* state = new osg::StateSet();
00622     osg::Material* mat = new osg::Material();
00623     Vec4 col( Math::random(), Math::random(), Math::random(), 1.0);
00624     mat->setAmbient( osg::Material::FRONT_AND_BACK, col );
00625     mat->setDiffuse( osg::Material::FRONT_AND_BACK, col );
00626     state->setAttribute( mat );
00627 
00628     osg::PolygonMode* polyMode = new osg::PolygonMode;
00629     polyMode->setMode( osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE );
00630     state->setAttributeAndModes(polyMode,osg::StateAttribute::ON);
00631 
00632     osg::Node* boxNode = box->createOSGVisual();
00633     boxNode->setStateSet(state);
00634     tn->addChild(boxNode);
00635   }
00636   
00637   if (level < maxLevels) {
00638     if (obb.N != 0) {
00639       tn->addChild( createOBBVisualRecurse(*obb.N, level+1) );
00640     }
00641     if (obb.P != 0) {
00642       tn->addChild( createOBBVisualRecurse(*obb.P, level+1) );
00643     }
00644   }
00645   
00646   return tn;
00647 }

Generated on Thu Jul 29 15:56:25 2004 for OpenSim by doxygen 1.3.6