Game Development Community

Swapable Colmeshes

by Azaezel · in Torque 3D Professional · 07/19/2013 (11:02 pm) · 9 replies

OK, so. lil background: This particular set of experiments came about trying to debug a fairly simple issue:

For our present project, we set a rigidshape animation into a destroyed state which caused the visible model to go from really tall, to really short. Issue was, the collision mesh stayed the same size.

While we found a simple workaround (in our case, changing the datablock in order to change the model), that really did feel like settling for a second-best approach, at which point I started poking around the ShapeBaseData::preload code, looking to cook up a more flexible solution that'd be useful in more situations.

The following is protypical, hackery, wip, ect ect:

shapebase.h

#define ShapeStates 1024

struct ShapeBaseData : public GameBaseData {
...
   S32 mColSets;        //track how many collision-# a given model's got.
   S32 mColSetReport;   //strictly for reporting via game editor. does nothing else
   Vector<S32>   collisionDetails[ShapeStates];  ///< Detail level used to collide with.
...
}

class ShapeBase : public GameBase, public ISceneLight
{
...
   bool              mTrigger[MaxTriggerKeys];  ///< What triggers are set, if any.
   S32 mActiveCollisionset;
...
   void setMeshHidden( const char *meshName, bool forceHidden ); 
   void setActiveCollision(S32 _ActiveCollisionset);
   S32 getActiveCollision(){ return mActiveCollisionset;}
...
}
shapebase.cpp

ShapeBaseData::ShapeBaseData()
...
   debrisDetail( -1 ),
   mColSets(-1)
{...}

bool ShapeBaseData::preload(bool server, String &errorStr)
{
...
      // Resolve details and camera node indexes.
      static const String sCollisionStr( "collision-" );
      mColSets = -1;
      for (i = 0; i < mShape->details.size(); i++)
      {
         const String &name = mShape->names[mShape->details[i].nameIndex];

         if (name.compare( sCollisionStr, sCollisionStr.length(), String::NoCase ) == 0)
         {
             mColSets++;
             if (mColSets>ShapeStates-1) mColSets = ShapeStates-1;
            collisionDetails[mColSets].push_back(i);
            collisionBounds.increment();

            mShape->computeBounds(collisionDetails[mColSets].last(), collisionBounds.last());
            mShape->getAccelerator(collisionDetails[mColSets].last());

            if (!mShape->bounds.isContained(collisionBounds.last()))
            {
               Con::warnf("Warning: shape %s collision detail %d (Collision-%d) bounds exceed that of shape.", shapeName, collisionDetails[mColSets].size() - 1, collisionDetails[mColSets].last());
               collisionBounds.last() = mShape->bounds;
            }
            else if (collisionBounds.last().isValidBox() == false)
            {
               Con::errorf("Error: shape %s-collision detail %d (Collision-%d) bounds box invalid!", shapeName, collisionDetails[mColSets].size() - 1, collisionDetails[mColSets].last());
               collisionBounds.last() = mShape->bounds;
            }

            // The way LOS works is that it will check to see if there is a LOS detail that matches
            // the the collision detail + 1 + MaxCollisionShapes (this variable name should change in
            // the future). If it can't find a matching LOS it will simply use the collision instead.
            // We check for any "unmatched" LOS's further down
            LOSDetails.increment();

            String   buff = String::ToString("LOS-%d", i + 1 + MaxCollisionShapes);
            U32 los = mShape->findDetail(buff);
            if (los == -1)
               LOSDetails.last() = i;
            else
               LOSDetails.last() = los;
         }
      }
      mColSetReport = mColSets;
...
}

void ShapeBaseData::initPersistFields()
{
...
      addField( "ColSetCount", TypeS32, Offset(mColSetReport, ShapeBaseData),
         "how many collisionsets do we have for this model?" );
...
}

#endif // #ifndef TORQUE_SHIPPING

void ShapeBase::setActiveCollision(S32 _ActiveCollisionset)
{
    if ((_ActiveCollisionset > 0)&&(_ActiveCollisionset <= mDataBlock->mColSets))
        mActiveCollisionset = _ActiveCollisionset;
    else
        mActiveCollisionset = 0;
}

DefineEngineMethod( ShapeBase, setActiveCollision, void, ( S32 activeColset ),,
   "@brief Set the active collision detail chainnn")
{
   object->setActiveCollision(activeColset);
}

bool ShapeBase::buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F &, const SphereF &)
{
...
   else
   {
      bool ret = false;
      for (U32 i = 0; i < mDataBlock->collisionDetails[mActiveCollisionset].size(); i++)
      {
         mShapeInstance->buildPolyList(polyList,mDataBlock->collisionDetails[mActiveCollisionset][i]);
         ret = true;
      }

      return ret;
   }
...
}

void ShapeBase::buildConvex(const Box3F& box, Convex* convex)
{
...
   if (realBox.isOverlapped(getObjBox()) == false)
      return;

   for (U32 i = 0; i < mDataBlock->collisionDetails[mActiveCollisionset].size(); i++)
   {
...
}

void ShapeBaseConvex::findNodeTransform()
{
    S32 dl = pShapeBase->mDataBlock->collisionDetails[pShapeBase->getActiveCollision()][hullId];
...
}

Point3F ShapeBaseConvex::support(const VectorF& v) const
{
   TSShape::ConvexHullAccelerator* pAccel =
      pShapeBase->mShapeInstance->getShape()->getAccelerator(pShapeBase->mDataBlock->collisionDetails[pShapeBase->getActiveCollision()][hullId]);
...
}

void ShapeBaseConvex::getFeatures(const MatrixF& mat, const VectorF& n, ConvexFeature* cf)
{
   cf->material = 0;
   cf->object = mObject;

   TSShape::ConvexHullAccelerator* pAccel =
      pShapeBase->mShapeInstance->getShape()->getAccelerator(pShapeBase->mDataBlock->collisionDetails[pShapeBase->getActiveCollision()][hullId]);
...
}

void ShapeBaseConvex::getPolyList(AbstractPolyList* list)
{
   list->setTransform(&pShapeBase->getTransform(), pShapeBase->getScale());
   list->setObject(pShapeBase);

   pShapeBase->mShapeInstance->animate(pShapeBase->mDataBlock->collisionDetails[pShapeBase->getActiveCollision()][hullId]);
   pShapeBase->mShapeInstance->buildPolyList(list,pShapeBase->mDataBlock->collisionDetails[pShapeBase->getActiveCollision()][hullId]);
}

various cpp files
bool FlyingVehicleData::preload(bool server, String &errorStr)
{
...
   // Extract collision planes from shape collision detail level
   if (collisionDetails[0][0] != -1)
   {
      MatrixF imat(1);
      PlaneExtractorPolyList polyList;
      polyList.mPlaneList = &rigidBody.mPlaneList;
      polyList.setTransform(&imat, Point3F(1,1,1));
      si->animate(collisionDetails[0][0]);
      si->buildPolyList(&polyList,collisionDetails[0][0]);
   }
...
}

bool WheeledVehicleData::preload(bool server, String &errorStr)
{
...
   // Extract collision planes from shape collision detail level
   if (collisionDetails[0][0] != -1) {
      MatrixF imat(1);
      SphereF sphere;
      sphere.center = mShape->center;
      sphere.radius = mShape->radius;
      PlaneExtractorPolyList polyList;
      polyList.mPlaneList = &rigidBody.mPlaneList;
      polyList.setTransform(&imat, Point3F(1,1,1));
      si->buildPolyList(&polyList,collisionDetails[0][0]);
   }
...
}

bool RigidShapeData::preload(bool server, String &errorStr)
{
   if (!Parent::preload(server, errorStr))
      return false;

   // RigidShape objects must define a collision detail
   if (!collisionDetails[0].size() || collisionDetails[0][0] == -1)
   {
      Con::errorf("RigidShapeData::preload failed: Rigid shapes must define a collision-1 detail");
      return false;
   }
...
}
bool RigidShape::onAdd()
{
...
AssertFatal(mDataBlock->collisionDetails[0][0] != -1, "Error, a rigid shape must have a collision-1 detail!");
...
}

#1
07/19/2013 (11:14 pm)
The end result is being able to throw in:
%obj.setActiveCollision(4);
To say, the cheetah, since it was built having seperate collision-# entries per colmesh, and collide with only the rear wheelhousing. As previously stated, the posted methods and results are most assuredly early-prototype phase, but I figured I might as well throw it out there early to get folks thoughts on different ways they'd want to see this type of thing branch out into a proper pull request if it get's that far.
#2
07/21/2013 (2:11 am)
Second verse, same as the first: For bullets, we need to support raycasts. While were at it, for consistency, lets knock out collisionBounds. Thing about collisions for shapebase derivatives to remember, is that if it can't find a LOS, it'll borrow the colmesh ones, so:

struct ShapeBaseData : public GameBaseData {
{...
   Vector<Box3F> collisionBounds[ShapeStates];   ///< Detail level bounding boxes.

   Vector<S32>   LOSDetails[ShapeStates];   ///< Detail level used to perform line-of-sight queries against.
...}

bool ShapeBaseData::preload(bool server, String &errorStr)
{...
      // Resolve details and camera node indexes.
      static const String sCollisionStr( "collision-" );
      mColSets = -1;
      for (i = 0; i < mShape->details.size(); i++)
      {
         const String &name = mShape->names[mShape->details[i].nameIndex];

         if (name.compare( sCollisionStr, sCollisionStr.length(), String::NoCase ) == 0)
         {
             mColSets++;
             if (mColSets>ShapeStates-1) mColSets = ShapeStates-1;
            collisionDetails[mColSets].push_back(i);
            collisionBounds[mColSets].increment();

            mShape->computeBounds(collisionDetails[mColSets].last(), collisionBounds[mColSets].last());
            mShape->getAccelerator(collisionDetails[mColSets].last());

            if (!mShape->bounds.isContained(collisionBounds[mColSets].last()))
            {
               Con::warnf("Warning: shape %s collision detail %d (Collision-%d) bounds exceed that of shape.", shapeName, collisionDetails[mColSets].size() - 1, collisionDetails[mColSets].last());
               collisionBounds[mColSets].last() = mShape->bounds;
            }
            else if (collisionBounds[mColSets].last().isValidBox() == false)
            {
               Con::errorf("Error: shape %s-collision detail %d (Collision-%d) bounds box invalid!", shapeName, collisionDetails[mColSets].size() - 1, collisionDetails[mColSets].last());
               collisionBounds[mColSets].last() = mShape->bounds;
            }

            // The way LOS works is that it will check to see if there is a LOS detail that matches
            // the the collision detail + 1 + MaxCollisionShapes (this variable name should change in
            // the future). If it can't find a matching LOS it will simply use the collision instead.
            // We check for any "unmatched" LOS's further down
            LOSDetails[mColSets].increment();

            String   buff = String::ToString("LOS-%d", i + 1 + MaxCollisionShapes);
            U32 los = mShape->findDetail(buff);
            if (los == -1)
               LOSDetails[mColSets].last() = i;
            else
               LOSDetails[mColSets].last() = los;
         }
      }
      mColSetReport = mColSets;

      // Snag any "unmatched" LOS details
      static const String sLOSStr( "LOS-" );

      for (i = 0; i < mShape->details.size(); i++)
      {
         const String &name = mShape->names[mShape->details[i].nameIndex];

         if (name.compare( sLOSStr, sLOSStr.length(), String::NoCase ) == 0)
         {
            // See if we already have this LOS
            bool found = false;
            for (U32 j = 0; j < LOSDetails[mColSets].size(); j++)
            {
               if (LOSDetails[mColSets][j] == i)
               {
                     found = true;
                     break;
               }
            }

            if (!found)
               LOSDetails[mColSets].push_back(i);
         }
      }
...}
#3
07/21/2013 (2:12 am)
aaand the detail work (no pun intended):
void ShapeBase::buildConvex(const Box3F& box, Convex* convex)
{...

   for (U32 i = 0; i < mDataBlock->collisionDetails[mActiveCollisionset].size(); i++)
   {
         Box3F newbox = mDataBlock->collisionBounds[mActiveCollisionset][i];
         newbox.minExtents.convolve(mObjScale);
         newbox.maxExtents.convolve(mObjScale);
         mObjToWorld.mul(newbox);
         if (box.isOverlapped(newbox) == false)
            continue;

         // See if this hull exists in the working set already...
         Convex* cc = 0;
         CollisionWorkingList& wl = convex->getWorkingList();
         for (CollisionWorkingList* itr = wl.wLink.mNext; itr != &wl; itr = itr->wLink.mNext) {
            if (itr->mConvex->getType() == ShapeBaseConvexType &&
                (static_cast<ShapeBaseConvex*>(itr->mConvex)->pShapeBase == this &&
                 static_cast<ShapeBaseConvex*>(itr->mConvex)->hullId     == i)) {
               cc = itr->mConvex;
               break;
            }
         }
         if (cc)
            continue;

         // Create a new convex.
         ShapeBaseConvex* cp = new ShapeBaseConvex;
         mConvexList->registerObject(cp);
         convex->addToWorkingList(cp);
         cp->mObject    = this;
         cp->pShapeBase = this;
         cp->hullId     = i;
         cp->box        = mDataBlock->collisionBounds[mActiveCollisionset][i];
         cp->transform = 0;
         cp->findNodeTransform();
   }
}

bool ShapeBase::castRay(const Point3F &start, const Point3F &end, RayInfo* info)
{...

      info->object = NULL;
      for (U32 i = 0; i < mDataBlock->LOSDetails[mActiveCollisionset].size(); i++)
      {
         mShapeInstance->animate(mDataBlock->LOSDetails[mActiveCollisionset][i]);
         if (mShapeInstance->castRay(start, end, info, mDataBlock->LOSDetails[mActiveCollisionset][i]))
         {
            info->object = this;
...}
#4
07/22/2013 (2:38 pm)
Last bit for the basic prototyping phase:

better debugging feedback, and networking via riding on the coattails of the mesh hiding flag
void ShapeBase::setActiveCollision(S32 _ActiveCollisionset)
{
    if ((_ActiveCollisionset >= 0)&&(_ActiveCollisionset <= mDataBlock->mColSets))
        mActiveCollisionset = _ActiveCollisionset;
    else
    {
        Con::errorf("Attempting to set an active collision lod for object %s outside of range. Tried: %i, Max: %i",mDataBlock->getName(), _ActiveCollisionset,mDataBlock->mColSets);
        mActiveCollisionset = 0;
    }
   setMaskBits( MeshHiddenMask );
}

U32 ShapeBase::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{...
      if ( stream->writeFlag( mask & MeshHiddenMask ) )
      {
         stream->writeBits( mMeshHidden );
         stream->write(mActiveCollisionset);
      }
...)

void ShapeBase::unpackUpdate(NetConnection *con, BitStream *stream)
{...
      if ( stream->readFlag() ) // MeshHiddenMask
      {
         stream->readBits( &mMeshHidden );
         _updateHiddenMeshes();
         stream->read(&mActiveCollisionset);
      }
...}

Still mulling over how to handle the cheetah, or whether to further complicate the code to work around it.
#5
07/25/2013 (11:07 pm)
https://github.com/Azaezel/Torque3D collision_lods branch. Figure give it a week or two for community feedback (read here, try and break it bad) in addition to personal usage before I consider turning it into a pull request.
#6
07/26/2013 (5:31 am)
Cool stuff!
#7
07/26/2013 (9:44 am)
uhm, sorry been a late night but, wouldn't your model... using more than 1 collision mesh, fix this issue? I know I have done it with 'destroyable' trees, fences, etc. in some of my levels. Then again, could be I just need some sleep and I am not reading things correctly.

Ron
#8
07/26/2013 (11:16 am)
@Ron: One of the first things I tried, actually. Then I looked into the code and found that at this point, the live engine is taking every mesh in every collision-N, and shoving them in one vector for that series of classes, at least, effectively ignoring the existence of that trailing number entirely.

What the revised set does is take up to 64 (probably should turn that into a vector of vectors at some point) collision-1, collision-2 ect, and set them as separate collections to test physics against, with a method to set which one you're using at the time.

The provided controllable_bridge, for instance, has:
collsion-1
^-colboxA-1
^-colboxB-1
collsion-2
^-colboxA-2
^-colboxB-2

calling %obj.setActiveCollision(0) tells that object-instance to use collision-1, %obj.setActiveCollision(1) to use collsion-2, ect (matching activecollsion to trailing number should probably be another one of those refinements for that matter.).

In retrospect, probably shoulda titled it swappable collsion lods.
#9
07/28/2013 (11:50 am)
updated with:

adds a UseCollisonLods flag (true/false) to object datablocks so that if the swapping system isn't used, it pretty much acts just like the old system.

@Ron again: Went on another hunt to verify. Are you referring perhaps to PhysicsShape? If so, that one's a separate inheritance tree entirely, as well as being hard-coded to swap between a normal and destroyedShape PhysicsShapeData.

The intent for this one is to resolve the single case with a more broadly applicable solution. (For instance, in the case of the bridge, we could now have an up, down, and destroyed collision series simply by having a collsion-1, collsion-2, and collsion-3 group, then swapping to whatever relevant one is applicable at the time.