hairCollisionSolver.cpp

//-
// ==========================================================================
// Copyright 1995,2006,2008 Autodesk, Inc. All rights reserved.
//
// Use of this software is subject to the terms of the Autodesk
// license agreement provided at the time of installation or download,
// or which otherwise accompanies this software in either electronic
// or hard copy form.
// ==========================================================================
//+

#include <maya/MIOStream.h>
#include <maya/MStatus.h>
#include <maya/MDagPath.h>
#include <maya/MFloatPointArray.h>
#include <maya/MObject.h>
#include <maya/MPlugArray.h>
#include <maya/MFnDependencyNode.h>
#include <maya/MFnDagNode.h>
#include <maya/MFnMesh.h>
#include <maya/MHairSystem.h>
#include <maya/MFnPlugin.h>

#include <math.h> 

#define kPluginName     "hairCollisionSolver"

//
//
// OVERVIEW:
// This plug-in implements a custom collision solver for Maya's dynamic
// hair system. This allows users to override the following aspects of
// Maya's dynamic hair systems:
//
//      o Override the Maya dynamic hair system's object-to-hair
//        collision algorithm with a user-defined algorithm.
//      o Optionally perform global filtering on hair, such as freeze,
//        smoothing, etc.
//
// It should be noted that Maya's dynamic hair system involves four
// arears of collision detection, and this plug-in is specific to the
// Hair-to-Object aspect only. The four areas collision which the Maya
// dynamic hair system involves are:
//
//      1) Hair to object collision.
//      2) Hair to implicit object collision.
//      3) Hair to ground plane collision.
//      4) Self collision between hairs.
//
// This plug-in illustrates the first case, overriding Maya's internal
// Hair-to-Object collision solver. There is currently no API for over-
// riding the other three collision solvers.
//
//
// RATIONALE:
// There are several reasons why are user may wish to override Maya's
// internal hair to object collision algorithm. These are:
//
//      1) The internal algorithm may not be accurate enough. After all,
//         it is a simulation of real-life physics and there are tradeoffs
//         taken to provide reasonable performance. Note that there are
//         means of increasing the accuracy without writing a custom
//         implementation, such as decreasing the dynamics time step, or
//         increasing the hair width.
//      2) The built-in algorithm might be too accurate. If you only want
//         simplified collisions, such as against a bounding box repre-
//         sentation instead of the internal algorithm's exhasutive test-
//         ing against each surface of the object, you could write a
//         lighter-weight implementation.
//      3) You might have a desire to process the hairs, such as smooth
//         them out.
//
//
// STRATEGY:
// The basic idea is to implement a custom callback which is registered
// via MHairSystem::registerCollisionSolverCollide(). Your callback will
// then be invoked in place of Maya's internal collision solver. By simply
// registering a collision solver, you can completely implement a custom
// hair-to-object solution.
//
// However, since the collision solver is called once per hair times the
// number of solver iterations, it is wise to pre-process the data if
// possible to speed up the collision tests. For this reason, you can
// assign a pre-frame callback. One approach is to create a pre-processed
// representation of your object pre-frame (e.g. an octree representation)
// and then access this representation during the collision testing.
//
// For the purposes of our simple demo plug-in, our private data will
// consist of a COLLISION_INFO which contains an array of COLLISION_OBJ
// structures, each one holding the bounding box of the object in world
// space.
//
// One issue with pre-processing the data involves managing the private
// data. A collision object could be deleted or turned off during a
// simulation. One way to cleanly manage such data is to store your
// private data on a typed attribute which is added to the node. You
// would build your private data once at the start of simulation in your
// pre-frame callback (keep track of the current time, and if the curTime
// passed in is less than what you store locally, assume the playback
// has restarted from the beginning -- or the user is being silly and
// trying to play the simulation backwards :-) Since it is relatively
// expensive to look up a dynamic attribute value, and the collide()
// callback can get triggered 1000's of times per frame, for efficiency
// you can pass back your private data as a pointer from your pre-frame
// routine, and this pointer is then passed directly into your collide()
// callback.
// 
//
typedef struct {
    int             numVerts;   // Number of vertices in object.
    double          minx;       // Bounding box minimal extrema.
    double          miny;       // Bounding box minimal extrema.
    double          minz;       // Bounding box minimal extrema.
    double          maxx;       // Bounding box maximal extrema.
    double          maxy;       // Bounding box maximal extrema.
    double          maxz;       // Bounding box maximal extrema.
} COLLISION_OBJ ;

typedef struct {
    int             numObjs;    // Number of collision objects.
    COLLISION_OBJ   *objs;      // Array of per-object info.
} COLLISION_INFO ;

//
// Synopsis:
//      bool    preFrame( hairSystem, curTime, privateData )
//
// Description:
//      This callback is invoked once at the start of each frame, allowing
//  the user to perform any pre-processing as they see fit, such as build-
//  ing or updating private collision-detection structures.
//
//      Note: it is possible for collision objects to change between
//  frames during a simulation (for example, the user could delete a col-
//  lision object), so if you choose to store your pre-processed data, it
//  is critical to track any edits or deletions to the collision object
//  to keep your pre-processed data valid.
//
//      There are lots of hints for writing an effective pre-frame call-
//  back in the STRATEGY section listed earlier in this file.
//
// Parameters:
//      MObject hairSystem  : (in)  The hair system shape node.
//      double  curTime     : (in)  Current time in seconds.
//      void    **privateData:(out) Allows the user to return a private
//                                  data pointer to be passed into their
//                                  collision solver. If you store your
//                                  pre-processed data in data structure
//                                  which is difficult to access, such as
//                                  on a typed attribute, this provides
//                                  an easy way to provide the pointer.
//
// Returns:
//      bool    true        : Successfully performed any needed initial-
//                            isation for the hair simulation this frame.
//      bool    false       : An error was detected.
//


bool        preFrame(
                const MObject       hairSystem,
                const double        curTime,
                void                **privateData )
{
    MStatus status;

    // If you need want to perform any preprocessing on your collision
    // objects pre-frame, do it here. One option for storing the pre-
    // processed data is on a typed attribute on the hairSystem node.
    // That data could be fetched and updated here.
    //
    // In our example, we'll just compute a bounding box here and NOT use
    // attribute storage. That is an exercise for the reader.
    //
    MFnDependencyNode fnHairSystem( hairSystem, &status );
    CHECK_MSTATUS_AND_RETURN( status, false );
    fprintf( stderr,
            "preFrame: calling hairSystem node=`%s', time=%g\n",
            fnHairSystem.name().asChar(), curTime );
    MObjectArray    cols;
    MIntArray       logIdxs;
    CHECK_MSTATUS_AND_RETURN( MHairSystem::getCollisionObject( hairSystem,
            cols, logIdxs ), false );
    int nobj = cols.length();

    // Allocate private data.
    // This allows us to pre-process data on a pre-frame basis to avoid
    // calculating it per hair inside the collide() call. As noted earlier
    // we could allocate this in preFrame() and hang it off the hairSystem
    // node via a dynamic attribute.
    // Instead we'll allocate it here.
    //
    COLLISION_INFO *collisionInfo = (COLLISION_INFO *) malloc(
            sizeof( COLLISION_INFO ) );
    collisionInfo->objs = (COLLISION_OBJ *) malloc(
            nobj * sizeof( COLLISION_OBJ ) );
    collisionInfo->numObjs = nobj;

    // Create the private data that we'll make available to the collide
    // method. The data should actually be stored in a way that it can
    // be cleaned up (such as storing the pointer on the hairSystem node
    // using a dynamic attribute). As it stands right now, there is a
    // memory leak with this plug-in because the memory we're allocating
    // for the private data is never cleaned up.
    //
    // Note that when using the dynamic attribute approach, it is still
    // wise to set *privateData because this avoids the need to look up
    // the plug inside the collide() routine which is a high-traffic
    // method.
    //
    *privateData = (void *) collisionInfo;

    // Loop through the collision objects and pre-process, storing the
    // results in the collisionInfo structure.
    //
    int    obj;
    for ( obj = 0; obj < nobj; ++obj ) {
        // Get the ith collision geometry we are connected to.
        //
        MObject colObj = cols[obj];

        // Get the DAG path for the collision object so we can transform
        // the vertices to world space.
        //
        MFnDagNode fnDagNode( colObj, &status );
        CHECK_MSTATUS_AND_RETURN( status, false );
        MDagPath path;
        status = fnDagNode.getPath( path );
        CHECK_MSTATUS_AND_RETURN( status, false );
        MFnMesh fnMesh( path, &status );
        if ( MS::kSuccess != status ) {
            fprintf( stderr,
                    "%s:%d: collide was not passed a valid mesh shape\n",
                    __FILE__, __LINE__ );
            return( false );
        }

        // Get the vertices of the object transformed to world space.
        //
        MFloatPointArray    verts;
        status = fnMesh.getPoints( verts, MSpace::kWorld );
        CHECK_MSTATUS_AND_RETURN( status, false );

        // Compute the bounding box for the collision object.
        // As this is a quick and dirty demo, we'll just support collisions
        // between hair and the bbox.
        //
        double minx, miny, minz, maxx, maxy, maxz, x, y, z;
        minx = maxx = verts[0].x;
        miny = maxy = verts[0].y;
        minz = maxz = verts[0].z;
        int nv = verts.length();
        int i;
        for ( i = 1; i < nv; ++i ) {
            x = verts[i].x;
            y = verts[i].y;
            z = verts[i].z;

            if ( x < minx ) {
                minx = x;
            }
            if ( y < miny ) {
                miny = y;
            }
            if ( z < minz ) {
                minz = z;
            }

            if ( x > maxx ) {
                maxx = x;
            }
            if ( y > maxy ) {
                maxy = y;
            }
            if ( z > maxz ) {
                maxz = z;
            }
        }

        // Store this precomputed informantion into our private data
        // structure.
        //
        collisionInfo->objs[obj].numVerts = nv;
        collisionInfo->objs[obj].minx = minx;
        collisionInfo->objs[obj].miny = miny;
        collisionInfo->objs[obj].minz = minz;
        collisionInfo->objs[obj].maxx = maxx;
        collisionInfo->objs[obj].maxy = maxy;
        collisionInfo->objs[obj].maxz = maxz;
        fprintf( stderr, "Inside preFrameInit, bbox=%g %g %g %g %g %g\n",
                minx,miny,minz,maxx,maxy,maxz);
    }

    return( true );
}

//
// Synopsis:
//      bool    belowCollisionPlane( co, pnt )
//
// Description:
//      Test if `pnt' is directly below the collision plane of `co'.
//
// Parameters:
//      COLLISION_OBJ   *co : (in)  The collision object to test against.
//      MVector         &pnt: (in)  Point to test.
//
// Returns:
//      bool    true        : The `pnt' is directly below the collision
//                            plane specified by `co'.
//      bool    false       : The `pnt' is not directly below the collis-
//                            ion plane specified by `co'.
//


bool    belowCollisionPlane( const COLLISION_OBJ *co, const MVector &pnt )
{
    return(    pnt.x > co->minx && pnt.x < co->maxx
                                && pnt.y < co->maxy
            && pnt.z > co->minz && pnt.z < co->maxz );
}

//
// Synopsis:
//      MStatus collide( hairSystem, follicleIndex, hairPositions,
//                      hairPositionsLast, hairWidths, startIndex,
//                      endIndex, curTime, friction, privateData )
//
// Description:
//      This callback is invoked by Maya to test if a collision exists be-
//  tween the follicle (defined by `hairPositions', `hairPositionsLast'
//  and `hairWidths') and the collision objects associated with the
//  hairSystem. If a collision is detected, this routine should adjust
//  `hairPositions' to compensate. The `hairPositionsLast' can also be
//  modified to adjust the velocity, but this should only be a dampening
//  effect as the hair solver expects collisions to be dampened,
//
//      This method is invoked often (actually its once per hair times the
//  hairSystem shape's iterations factor). Thus with 10,000 follicles it
//  would be called 80,000 times per frame (Note: as of Maya 7.0, the
//  iterations factor is multipled by 2, so at its default value of 4, you
//  get 8x calls. However, if you set iterations=0, it clamps to 1x calls).
//
// Parameters:
//      MObject hairSystem  : (in)  The hair system shape node.
//      int     follicleIndex:(in)  Which follicle we are processing.
//                                  You can get the follicle node if you
//                                  wish via MHairSystem::getFollicle().
//      MVectorArray &hairPositions:
//                            (mod) Array of locations in world space
//                                  where the hair system is trying to
//                                  move the follicle. The first entry
//                                  corresponds to the root of the hair
//                                  and the last entry to the tip. If a
//                                  collision is detected, these values
//                                  should be updated appropriately.
//                                  Note that hairPositions can change
//                                  from iteration to iteration on the
//                                  same hair and same frame. You can set
//                                  a position, and then find the hair has
//                                  moved a bit the next iteration. There
//                                  are two reasons for this phenomenom:
//                                    1) Other collisions could occur,
//                                       including self collision.
//                                    2) Stiffness is actually applied PER
//                                       ITERATION.
//      MVectorArray &hairPositionsLast:
//                            (mod) Array of the position at the previous
//                                  time for each entry in `hairPositions'.
//      MDoubleArray &hairWidths:
//                            (in)  Array of widths, representing the
//                                  width of the follcle at each entry in
//                                  `hairPositions'.
//      int     startIndex  : (in)  First index in `hairPositions' we
//                                  can move. This will be 0 unless the
//                                  root is locked in which case it will
//                                  be 1.
//      int     endIndex    : (in)  Last index in `hairPositions' we can
//                                  move. Will be the full array (N-1)
//                                  unless the tip is locked.
//      double  curTime     : (in)  Start of current time interval.
//      double  friction    : (in)  Frictional coefficient.
//      void    *privateData: (in)  If a privateData record was returned
//                                  from preFrame() it will be passed in
//                                  here. This is an optimisation to save
//                                  expensive lookups of your private data
//                                  if stored on the node.
//
// Returns:
//      bool    true        : Successfully performed collision testing and
//                            adjustment of the hair array data.
//      bool    false       : An error occurred.
//


#define EPSILON 0.0001

bool    collide(
                const MObject       hairSystem,
                const int           follicleIndex,
                MVectorArray        &hairPositions,
                MVectorArray        &hairPositionsLast,
                const MDoubleArray  &hairWidths,
                const int           startIndex,
                const int           endIndex,
                const double        curTime,
                const double        friction,
                const void          *privateData )
{
    // Get the private data for the collision objects which was returned
    // from preFrame().
    //
    COLLISION_INFO *ci = (COLLISION_INFO *) privateData;
    if ( !ci ) {
        fprintf( stderr,"%s:%d: collide() privateData pointer is NULL\n",
                __FILE__, __LINE__ );
        return( false );
    }

    // If object has no vertices, or hair has no segments, then there is
    // nothing to collide. In our example, we'll return here, but if you
    // want to implement your own hair processing such as smoothing or
    // freeze on the hair, you could proceed and let the processing happen
    // after the object loop so that the data gets processed even if no
    // collisions occur.
    //
    if ( ci->numObjs <= 0 || hairPositions.length() <= 0 ) {
        return( true );
    }

    int     obj;
    for ( obj = 0; obj < ci->numObjs; ++obj ) {
        COLLISION_OBJ *co = &ci->objs[obj];

        // For our plug-in example, we just collide the segments of the
        // hair against the top of the bounding box for the geometry.
        // In an implementation where you only care about hair falling
        // downwards onto flat objects, this might be OK. However, in the
        // most deluxe of implementation, you should do the following:
        //
        //      o  Determine the motion of each face of your collision
        //         object during the time range.
        //      o  Step through the follicle, and consider each segment
        //         to be a moving cylinder where the start and end radii
        //         can differ. The radii come from `hairWidths' and the
        //         motion comes from the difference between `hairPositions'
        //         and `hairPositionsLast'.
        //      o  Intersect each moving element (e.g. moving triangle
        //         if you have a mesh obect) with each hair segment
        //         e.g. moving cylinder). This intersection may occur
        //         at a point within the frame. (Remember that the
        //         hairPositions[] array holds the DESIRED location where
        //         the hair system wants the hair to go.
        //      o  There can be multiple collisions along a hair segment.
        //         Use the first location found and the maximal velocity.
        //
        // If a collision is detected, the `hairPositions' array should be
        // updated. `hairPositionsLast' may also be updated to provide a
        // dampening effect only.
        //

        // Loop through the follicle, starting at the root and advancing
        // toward the tip.
        //
        int     numSegments = hairPositions.length() - 1;
        int     seg;
        for ( seg = 0; seg < numSegments; ++seg ) {
            // Get the desired position of the segment (i.e. where the
            // solver is trying to put the hair for the current frame)
            // and the velocity required to get to that desired position.
            //
            // Thus,
            //      P = hairPositions       // Desired pos'n at cur frame.
            //      L = hairPositionsLast   // Position at prev frame.
            //      V = P - L               // Desired velocity of hair.
            //
            MVector pStart = hairPositions[seg];
            MVector pEnd = hairPositions[seg + 1];

            MVector vStart = pStart - hairPositionsLast[seg];
            MVector vEnd = pEnd - hairPositionsLast[seg + 1];

            // The proper way to time sample is to intersect the moving
            // segment of width `hairWidths[seg] to hairWidths[seg + 1]'
            // with the moving collision object. For the purposes of our
            // demo plug-in, we will simply assume the collision object is
            // static, the hair segment has zero width, and instead of
            // intersecting continuously in time, we will sample discrete
            // steps along the segment.
            //
            #define STEPS 4
            int     step;
            for ( step = 0; step < STEPS; ++step ) {
                // Compute the point for this step and its corresponding
                // velocity. This is a "time swept" point:
                //  p1 = desired position at current time
                //  p0 = position at previous time to achieve desired pos
                //
                MVector pCur, pPrev, v;
                double fracAlongSeg = step / ( (double) STEPS );
                v = vStart * ( 1.0 - fracAlongSeg ) + vEnd * fracAlongSeg;
                MVector p1 = pStart * ( 1.0 - fracAlongSeg )
                        + pEnd * fracAlongSeg;
                MVector p0 = p1 - v;

                // See if BOTH endpoints are outside of the bounding box
                // on the same side. If so, then the segment cannot
                // intersect the bounding box. Note that we assume the
                // bounding box is static.
                //
                if (       p0.x < co->minx && p1.x < co->minx
                        || p0.y < co->miny && p1.y < co->miny
                        || p0.z < co->minz && p1.z < co->minz
                        || p0.x > co->maxx && p1.x > co->maxx
                        || p0.y > co->maxy && p1.y > co->maxy
                        || p0.z > co->maxz && p1.z > co->maxz ) {
                    continue;
                }

                // For the purposes of this example plug-in, we'll assume
                // the hair always moves downwards (due to gravity and
                // thus in the negative Y direction). As such, we only
                // need to test for collisions with the TOP of the
                // bounding box. Expanding the example to all 6 sides is
                // left as an exercise for the reader.
                //
                // Remember that p1 is the point at current time, and
                // p0 is the point at the previous time. Since we assume
                // the bounding box to be static, this simplifies things.
                //
                MVector where(-100000,-100000,-100000); // Loc'n of collision
                double  fracTime;   // Time at which collision happens 0..1

                if ( fabs( v.y ) < EPSILON      // velocity is zero
                        && fabs( p1.y - co->maxy ) < EPSILON ) {    // right on the bbox
                    // Velocity is zero and the desired location (p1) is
                    // right on top of the bounding box.
                    //
                    where = p1;
                    fracTime = 1.0;     // Collides right at end;
                } else {
                    fracTime = ( co->maxy -  p0.y ) / v.y;
                    if ( fracTime >= 0.0 && fracTime <= 1.0 ) {
                        // Compute the collision of the swept point with the
                        // plane defined by the top of the bounding box.
                        //
                        where = p0 + v * fracTime;
                    }
                }

                // If `seg' lies between startIndex and endIndex
                // we can move it. If its <= startIndex, the root is
                // locked and if >= endIndex the tip is locked.
                //
                if ( seg >= startIndex && seg <= endIndex ) {
                    // Since we are just colliding with the top of the
                    // bounding box, the normal where we hit is (0,1,0).
                    // For the object velocity, we SHOULD measure the
                    // relative motion of the object during the time
                    // interval, but for our example, we'll assume its
                    // not moving (0,0,0).
                    //
                    bool segCollides = false;
                    MVector normal(0.0,1.0,0.0);    // normal is always up
                    MVector objectVel(0.0,0.0,0.0); // assume bbox is static

                    // If we get the this point, then the intersection
                    // point `where' is on the plane of the bounding box
                    // top. See if it lies within the actual bounds of the
                    // boxtop, and if so compute the position and velocity
                    // information and apply to the hair segment.
                    //
                    if ( where.x >= co->minx && where.x <= co->maxx
                            && where.z >= co->minz && where.z <= co->maxz
                            && fracTime >= 0.0 && fracTime <= 0.0 ) {
                        // We have a collision at `where' with the plane.
                        // Compute the new velocity for the hair at the
                        // point of collision.
                        //
                        MVector objVelAlongNormal = (objectVel * normal)
                                * normal;
                        MVector objVelAlongTangent = objectVel
                                - objVelAlongNormal;
                        MVector pntVelAlongTangent = v - ( v * normal )
                                * normal;
                        MVector reflectedPntVelAlongTangent
                                = pntVelAlongTangent * ( 1.0 - friction )
                                + objVelAlongTangent * friction;
                        MVector newVel = objVelAlongNormal
                                + reflectedPntVelAlongTangent;

                        // Update the hair position. It actually looks
                        // more stable from a simulation standpoint to
                        // just move the closest segment endpoint, but
                        // you are free to experiment. What we'll do in
                        // our example is move the closest segment end-
                        // point by the amount the collided point (where)
                        // has to move to have velocity `newVel' yet still
                        // pass through `where' at time `fracTime'.
                        //
                        if ( fracAlongSeg > 0.5 ) {
                            MVector deltaPos = -newVel * fracTime;
                            hairPositionsLast[seg + 1] += deltaPos;
                            hairPositions[seg + 1] = hairPositionsLast[seg]
                                    + newVel;
                        } else {
                            MVector deltaPos = newVel * fracTime;
                            hairPositionsLast[seg] += deltaPos;
                            hairPositions[seg] = hairPositionsLast[seg]
                                    + newVel;
                        }
                        segCollides = true;
                    }

                    // Check for segment endpoints that may still be
                    // inside. Note that segments which started out being
                    // totally inside will never collide using the
                    // algorithm we use above, so we'll simply clamp them
                    // here. One might expect an inside-the-bounding-box
                    // test instead. However, this does not work if the
                    // collision object is a very thin object.
                    //
                    bool inside = false;
                    if ( belowCollisionPlane( co, hairPositions[seg] ) ) {
                        hairPositions[seg].y = co->maxy + EPSILON;
                        hairPositionsLast[seg] = hairPositions[seg] - objectVel;
                        inside = true;
                    }
                    if ( belowCollisionPlane( co, hairPositions[seg+1] ) ) {
                        hairPositions[seg+1].y = co->maxy + EPSILON;
                        hairPositionsLast[seg+1] = hairPositions[seg+1] - objectVel;
                        inside = true;
                    }

                    // If we collided, go onto the next segment.
                    //
                    if ( segCollides || inside ) {
                        goto nextSeg;
                    }
                }
            }
nextSeg:;
        }
    }

    // You could perform any global filtering that you want on the hair
    // right here. For example: smoothing. This code is independent of
    // whether or not a collision occurred, and note that collide() is
    // called even with zero collision objects, so you will be guaranteed
    // of reaching here once per hair per iteration.
    //

    return( true );
}

//
// Synopsis:
//      MStatus initializePlugin( MObject obj )
//
// Description:
//      Invoked upon plug-in load to register the plug-in and initialise.
//
// Parameters:
//      MObject obj         : (in)  Plug-in object being loaded.
//
// Returns:
//      MStatus::kSuccess   : Successfully performed any needed initial-
//                            isation for the plug-in.
//      MStatus statusCode  : Error was detected.
//


MStatus initializePlugin( MObject obj )
{
    MFnPlugin   plugin( obj, "Autodesk", "8.0", "Any" );

    CHECK_MSTATUS( MHairSystem::registerCollisionSolverCollide( collide ) );
    CHECK_MSTATUS( MHairSystem::registerCollisionSolverPreFrame( preFrame ) );

    return( MS::kSuccess );
}

//
// Synopsis:
//      MStatus uninitializePlugin( MObject obj )
//
// Description:
//      Invoked upon plug-in unload to deregister the plug-in and clean up.
//
// Parameters:
//      MObject obj         : (in)  Plug-in object being unloaded.
//
// Returns:
//      MStatus::kSuccess   : Successfully performed any needed cleanup.
//      MStatus statusCode  : Error was detected.
//


MStatus uninitializePlugin( MObject obj )
{
    MFnPlugin   plugin( obj );

    CHECK_MSTATUS( MHairSystem::unregisterCollisionSolverCollide() );
    CHECK_MSTATUS( MHairSystem::unregisterCollisionSolverPreFrame() );

    return( MS::kSuccess );
}