|
/** |
|
* Individual rigid body |
|
* |
|
*/ |
|
class RigidNode : public MPxLocatorNode { |
|
public: |
|
RigidNode() {}; |
|
~RigidNode() override; |
|
|
|
MStatus compute(const MPlug&, MDataBlock&) override; |
|
MStatus computeStartState(const MPlug&, MDataBlock&); |
|
MStatus computeCurrentState(const MPlug&, MDataBlock&); |
|
MStatus computeOutputTranslate(const MPlug&, MDataBlock&); |
|
MStatus computeOutputWorldMatrix(const MPlug&, MDataBlock&); |
|
|
|
MStatus updateStartState(const MPlug&, MDataBlock&); |
|
MStatus updateCurrentState(const MPlug&, MDataBlock&); |
|
|
|
static void* creator() { return new RigidNode(); } |
|
static MStatus initialize(); |
|
void postConstructor(); |
|
void reset(MDataBlock&); |
|
int uid() { return _uid; } |
|
bool getInternalValue(const MPlug&, MDataHandle&) override; |
|
bool isPassiveOutput(const MPlug&) const override; |
|
|
|
// Does nothing |
|
MStatus preEvaluation(const MDGContext&, const MEvaluationNode&) override; |
|
MStatus setDependentsDirty(const MPlug&, MPlugArray&) override; |
|
|
|
public: |
|
static MString name; |
|
static MTypeId id; |
|
static const MString drawDbClassification; |
|
static const MString drawRegistrantId; |
|
|
|
// Attributes |
|
static MObject aUid; |
|
static MObject aDamping; |
|
static MObject aActive; |
|
static MObject aCurrentTime; |
|
static MObject aCachedWorldMatrix; |
|
static MObject aInputWorldMatrix; |
|
static MObject aInputParentInverseMatrix; |
|
static MObject aOutputWorldMatrix; |
|
static MObject aOutputTranslate; |
|
static MObject aOutputTranslateX; |
|
static MObject aOutputTranslateY; |
|
static MObject aOutputTranslateZ; |
|
|
|
// <-- Pull on this to fetch latest goods from the solver |
|
static MObject aNextState; |
|
|
|
// --> Deliver initial state of this rigid to the solver |
|
static MObject aStartState; |
|
|
|
// --> Deliver animated values, like damping, to the solver |
|
static MObject aCurrentState; |
|
|
|
private: |
|
int _uid { -1 }; |
|
MCallbackIdArray _callbackIds; |
|
}; |
|
|
|
|
|
MString RigidNode::name("rigidNode"); |
|
MTypeId RigidNode::id(0x80012); |
|
const MString RigidNode::drawDbClassification("drawdb/geometry/RigidNode"); |
|
const MString RigidNode::drawRegistrantId("RigidNodePlugin"); |
|
MObject RigidNode::aUid; |
|
MObject RigidNode::aDamping; |
|
MObject RigidNode::aActive; |
|
MObject RigidNode::aCachedWorldMatrix; |
|
MObject RigidNode::aInputWorldMatrix; |
|
MObject RigidNode::aInputParentInverseMatrix; |
|
MObject RigidNode::aOutputWorldMatrix; |
|
MObject RigidNode::aOutputTranslate; |
|
MObject RigidNode::aOutputTranslateX; |
|
MObject RigidNode::aOutputTranslateY; |
|
MObject RigidNode::aOutputTranslateZ; |
|
MObject RigidNode::aCurrentTime; |
|
MObject RigidNode::aNextState; |
|
MObject RigidNode::aStartState; |
|
MObject RigidNode::aCurrentState; |
|
|
|
|
|
|
|
RigidNode::~RigidNode() { |
|
// Any node created will have an entry in this map |
|
gRigids.erase(_uid); |
|
|
|
for (unsigned int ii=0; ii<_callbackIds.length(); ii++) |
|
MMessage::removeCallback(_callbackIds[ii]); |
|
|
|
_callbackIds.clear(); |
|
} |
|
|
|
|
|
MStatus RigidNode::initialize() { |
|
MStatus status; |
|
MFnNumericAttribute numFn; |
|
MFnMatrixAttribute matFn; |
|
MFnUnitAttribute uniFn; |
|
|
|
aUid = numFn.create("uid", "uid", MFnNumericData::kInt, -1, &status); |
|
numFn.setWritable(false); |
|
numFn.setStorable(false); |
|
numFn.setInternal(true); // We'll manage this ourselves |
|
|
|
aNextState = numFn.create("nextState", "nest", MFnNumericData::kInt, -1, &status); |
|
numFn.setStorable(false); |
|
numFn.setReadable(false); |
|
|
|
aStartState = numFn.create("startState", "stst", MFnNumericData::kInt, -1, &status); |
|
numFn.setStorable(false); |
|
|
|
aCurrentState = numFn.create("currentState", "cust", MFnNumericData::kInt, -1, &status); |
|
numFn.setStorable(false); |
|
|
|
aDamping = numFn.create("damping", "damp", MFnNumericData::kFloat, 0.5f, &status); |
|
numFn.setKeyable(true); |
|
|
|
aActive = numFn.create("active", "acti", MFnNumericData::kBoolean, true, &status); |
|
numFn.setKeyable(true); |
|
|
|
aCurrentTime = uniFn.create("currentTime", "cuti", MFnUnitAttribute::kTime, 0.0, &status); |
|
uniFn.setReadable(false); |
|
uniFn.setStorable(false); |
|
uniFn.setHidden(true); |
|
|
|
aInputParentInverseMatrix = matFn.create("inParentInverseMatrix", "ipim"); |
|
matFn.setStorable(false); |
|
matFn.setReadable(false); |
|
|
|
aInputWorldMatrix = matFn.create("inWorldMatrix", "inma"); |
|
matFn.setStorable(false); |
|
matFn.setReadable(false); |
|
|
|
aCachedWorldMatrix = matFn.create("inCachedMatrix", "incm"); |
|
matFn.setStorable(true); |
|
matFn.setReadable(false); |
|
matFn.setConnectable(false); |
|
matFn.setHidden(true); |
|
|
|
aOutputWorldMatrix = matFn.create("outWorldMatrix", "ouma"); |
|
matFn.setWritable(false); |
|
|
|
aOutputTranslateX = uniFn.create("outputTranslateX", "outx", MFnUnitAttribute::kDistance, 0.0, &status); |
|
uniFn.setWritable(false); |
|
uniFn.setStorable(false); |
|
uniFn.setChannelBox(true); |
|
aOutputTranslateY = uniFn.create("outputTranslateY", "outy", MFnUnitAttribute::kDistance, 0.0, &status); |
|
uniFn.setWritable(false); |
|
uniFn.setStorable(false); |
|
uniFn.setChannelBox(true); |
|
aOutputTranslateZ = uniFn.create("outputTranslateZ", "outz", MFnUnitAttribute::kDistance, 0.0, &status); |
|
uniFn.setWritable(false); |
|
uniFn.setStorable(false); |
|
uniFn.setChannelBox(true); |
|
aOutputTranslate = numFn.create("outputTranslate", "outr", aOutputTranslateX, aOutputTranslateY, aOutputTranslateZ); |
|
|
|
AddAttribute(aUid); |
|
AddAttribute(aActive); |
|
AddAttribute(aDamping); |
|
AddAttribute(aNextState); |
|
AddAttribute(aStartState); |
|
AddAttribute(aCurrentState); |
|
AddAttribute(aCurrentTime); |
|
AddAttribute(aCachedWorldMatrix); |
|
AddAttribute(aInputWorldMatrix); |
|
AddAttribute(aInputParentInverseMatrix); |
|
AddAttribute(aOutputWorldMatrix); |
|
AddAttribute(aOutputTranslate); |
|
|
|
AttributeAffects(aInputParentInverseMatrix, aOutputWorldMatrix); |
|
AttributeAffects(aNextState, aOutputWorldMatrix); |
|
AttributeAffects(aDamping, aOutputWorldMatrix); |
|
AttributeAffects(aActive, aOutputWorldMatrix); |
|
AttributeAffects(aCurrentTime, aOutputWorldMatrix); |
|
|
|
AttributeAffects(aCurrentTime, aOutputTranslate); |
|
AttributeAffects(aOutputWorldMatrix, aOutputTranslate); |
|
|
|
return status; |
|
} |
|
|
|
|
|
bool RigidNode::isPassiveOutput(const MPlug& plug) const { |
|
/** |
|
* Let anything connected to these still be editable |
|
* |
|
*/ |
|
return plug == aOutputTranslateX || |
|
plug == aOutputTranslateY || |
|
plug == aOutputTranslateZ; |
|
} |
|
|
|
|
|
MStatus RigidNode::preEvaluation(const MDGContext& context, const MEvaluationNode& evaluationNode) { |
|
return MS::kSuccess; |
|
} |
|
|
|
|
|
MStatus RigidNode::setDependentsDirty(const MPlug& plugBeingDirtied, MPlugArray& affectedPlugs) { |
|
return MPxLocatorNode::setDependentsDirty(plugBeingDirtied, affectedPlugs); |
|
} |
|
|
|
|
|
/** |
|
* Respond to changes in time |
|
* |
|
* When time has changed, but before any evaluation takes place, |
|
* read and record the world matrix of this rigid as the new initial state. |
|
* |
|
*/ |
|
static void onTimeChanged(MTime&, void* clientData) { |
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidNode* node = static_cast<RigidNode*>(clientData); |
|
RigidBody& rigid = gRigids.at(node->uid()); |
|
|
|
if (rigid.updateInitialState) { |
|
MPlug worldMatrixPlug { node->thisMObject(), RigidNode::aInputWorldMatrix }; |
|
MObject obj = worldMatrixPlug.asMObject(&status); |
|
assert(status == MS::kSuccess); |
|
|
|
MMatrix mat = MFnMatrixData(obj).matrix(&status); |
|
assert(status == MS::kSuccess); |
|
|
|
rigid.restMatrix = mat; |
|
rigid.worldMatrix = mat; |
|
rigid.worldMatrixChanged = true; |
|
|
|
// Cache value for when time rewinds and file save |
|
MPlug restMatrixPlug { node->thisMObject(), RigidNode::aCachedWorldMatrix }; |
|
obj = MFnMatrixData().create(mat); |
|
restMatrixPlug.setMObject(obj); |
|
|
|
Print() << "(+) Successfully updated initial state\n"; |
|
rigid.updateInitialState = false; |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Respond to newly connected inWorldMatrix |
|
* |
|
* In order to preserve the position of the connected Maya transform, |
|
* we'll read and store whatever the world matrix was at the time of connecting. |
|
* |
|
* If we don't do this, then the `outputTranslate` would be zero once |
|
* evaluation starts. |
|
* |
|
*/ |
|
void onAttributeChanged(MNodeMessage::AttributeMessage msg, MPlug& plug, MPlug& otherPlug, void*) { |
|
MStatus status { MS::kSuccess }; |
|
|
|
if (msg & MNodeMessage::kConnectionMade) { |
|
if (plug == RigidNode::aInputWorldMatrix) { |
|
MObject obj = plug.asMObject(&status); |
|
assert(status == MS::kSuccess); |
|
|
|
MMatrix mat = MFnMatrixData(obj).matrix(&status); |
|
assert(status == MS::kSuccess); |
|
|
|
MPlug restMatrixPlug { plug.node(), RigidNode::aCachedWorldMatrix }; |
|
obj = MFnMatrixData().create(mat); |
|
restMatrixPlug.setMObject(obj); |
|
|
|
Print() << "(+) Successfully cached newly connected inWorldMatrix\n"; |
|
} |
|
} |
|
} |
|
|
|
|
|
void RigidNode::postConstructor() { |
|
MStatus status { MS::kSuccess }; |
|
|
|
// Find ourselves amongst rigids hosted by the solver |
|
_uid = ++gUniqueIdCounter; |
|
|
|
// Initialise our data, in a place accessible to our solver |
|
gRigids[_uid] = { _uid }; |
|
|
|
MObject tempobj { thisMObject() }; |
|
|
|
_callbackIds.append(MDGMessage::addForceUpdateCallback(onTimeChanged, this, &status)); |
|
assert(status == MS::kSuccess); |
|
|
|
_callbackIds.append(MNodeMessage::addAttributeChangedCallback(tempobj, onAttributeChanged, this, &status)); |
|
assert(status == MS::kSuccess); |
|
} |
|
|
|
|
|
bool RigidNode::getInternalValue(const MPlug& plug, MDataHandle& datahandle) { |
|
if (plug == aUid) { |
|
datahandle.setInt(_uid); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
/** |
|
* Restore initial state |
|
* |
|
*/ |
|
MStatus RigidNode::computeStartState(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode:: Rigid[" << _uid << "].computeStartState() ===============\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
OutputValue(datablock, aStartState).set(_uid); |
|
|
|
return status; |
|
} |
|
|
|
|
|
/** |
|
* Read values that change over time |
|
* |
|
* We don't really have any in this example, but for example linearDamping |
|
* |
|
*/ |
|
MStatus RigidNode::computeCurrentState(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode:: Rigid[" << _uid << "].computeCurrentState() ===============\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
OutputValue(datablock, aCurrentState).set(_uid); |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::updateStartState(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode:: Rigid[" << _uid << "].updateStartState() ===============\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
|
|
// Restore initial state, do not evaluate anything |
|
MMatrix worldMatrix = InputValue(datablock, aCachedWorldMatrix).asMatrix(); |
|
rigid.restMatrix = worldMatrix; |
|
rigid.worldMatrix = rigid.restMatrix; |
|
rigid.active = InputValue(datablock, aActive).asBool(); |
|
rigid.damping = InputValue(datablock, aDamping).asFloat(); |
|
|
|
// Tell the next frame we'd like for *this* to be |
|
// our new initial state, whatever it may be. |
|
rigid.updateInitialState = true; |
|
|
|
MFnDagNode fn { thisMObject() }; |
|
rigid.name = fn.name(); |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::updateCurrentState(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode:: Rigid[" << _uid << "].updateCurrentState() ===============\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
rigid.active = InputValue(datablock, aActive).asBool(); |
|
rigid.damping = InputValue(datablock, aDamping).asFloat(); |
|
|
|
if (!rigid.active) { |
|
rigid.updateInitialState = true; |
|
} |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::computeOutputWorldMatrix(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode::computeOutputWorldMatrix()\n"; |
|
|
|
MStatus status { MS::kSuccess }; |
|
RigidBody& rigid = gRigids.at(_uid); |
|
|
|
const int simulatedFrame = InputValue(datablock, aNextState).asInt(); |
|
|
|
if (simulatedFrame <= 0) { |
|
updateStartState(plug, datablock); |
|
} |
|
else { |
|
updateCurrentState(plug, datablock); |
|
} |
|
|
|
// Preserve hierarchy when manipulating an object during standstill |
|
// Try removing this and rotate the parent of a cube, you will find that |
|
// children remain stationary as the parent moves around. That's bad. |
|
if (rigid.worldMatrixChanged) { |
|
rigid.parentInverseMatrix = InputValue(datablock, aInputParentInverseMatrix).asMatrix(); |
|
rigid.worldMatrixChanged = false; |
|
} |
|
|
|
MDataHandle outputHandle = OutputValue(datablock, aOutputWorldMatrix); |
|
outputHandle.setMMatrix(rigid.worldMatrix); |
|
|
|
datablock.setClean(plug); |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::computeOutputTranslate(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode::computeOutputTranslate() -------------------------------\n"; |
|
|
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
|
|
MMatrix worldMatrix = InputValue(datablock, aOutputWorldMatrix).asMatrix(); |
|
MMatrix localMatrix = worldMatrix * rigid.parentInverseMatrix; |
|
|
|
MTransformationMatrix tm { localMatrix }; |
|
MVector translate = tm.translation(MSpace::kTransform); |
|
MDataHandle tx = OutputValue(datablock, aOutputTranslateX), |
|
ty = OutputValue(datablock, aOutputTranslateY), |
|
tz = OutputValue(datablock, aOutputTranslateZ); |
|
|
|
tx.set(translate.x); |
|
ty.set(translate.y); |
|
tz.set(translate.z); |
|
|
|
datablock.setClean(plug); |
|
|
|
return status; |
|
} |
|
|
|
|
|
|
|
MStatus RigidNode::compute(const MPlug& plug, MDataBlock& datablock) { |
|
MStatus status { MS::kUnknownParameter }; |
|
|
|
// Print() << "RigidNode::compute() - " << plug.name() << "\n"; |
|
|
|
// For some reason, we need to pull on time, even though we don't need it |
|
// in order for its dependents to continue to be dirtied when it changes. |
|
// The alternative is having it drawn in the channel box or AE. |
|
InputValue(datablock, aCurrentTime).asTime(); |
|
|
|
if (plug == aStartState) { |
|
status = computeStartState(plug, datablock); |
|
} |
|
|
|
else if (plug == aCurrentState) { |
|
status = computeCurrentState(plug, datablock); |
|
} |
|
|
|
else if (plug == aOutputWorldMatrix) { |
|
status = computeOutputWorldMatrix(plug, datablock); |
|
} |
|
|
|
else if (plug == aOutputTranslate || |
|
plug == aOutputTranslateX || |
|
plug == aOutputTranslateY || |
|
plug == aOutputTranslateZ) { |
|
status = computeOutputTranslate(plug, datablock); |
|
} |
|
|
|
return status; |
|
} |