hairCollisionSolver/hairCollisionSolver.cpp

hairCollisionSolver/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 );
MIntArray logIdxs;
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.
//
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" );
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 );
return( MS::kSuccess );
}