|
/** |
|
* Individual rigid body |
|
* |
|
*/ |
|
class RigidNode : public MPxLocatorNode { |
|
public: |
|
RigidNode() {}; |
|
~RigidNode() override; |
|
|
|
MStatus compute(const MPlug&, MDataBlock&) override; |
|
MStatus computeUid(const MPlug&, MDataBlock&); |
|
MStatus computeStartState(const MPlug&, MDataBlock&); |
|
MStatus computeCurrentState(const MPlug&, MDataBlock&); |
|
MStatus computeOutputTranslate(const MPlug&, MDataBlock&); |
|
|
|
static void* creator() { return new RigidNode(); } |
|
static MStatus initialize(); |
|
void postConstructor(); |
|
void reset(MDataBlock&); |
|
int uid() { return _uid; } |
|
|
|
MStatus setDependentsDirty(const MPlug&, MPlugArray&) override; |
|
bool isPassiveOutput(const MPlug&) const override; |
|
|
|
public: |
|
static MString name; |
|
static MTypeId id; |
|
static const MString drawDbClassification; |
|
static const MString drawRegistrantId; |
|
|
|
// Attributes |
|
static MObject aUid; |
|
static MObject aCurrentTime; |
|
static MObject aInputMatrix; |
|
static MObject aInputParentInverseMatrix; |
|
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 }; |
|
int _lastFrame { -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::aInputMatrix; |
|
MObject RigidNode::aInputParentInverseMatrix; |
|
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); |
|
} |
|
|
|
|
|
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.setKeyable(true); |
|
|
|
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); |
|
numFn.setWritable(false); |
|
|
|
aCurrentState = numFn.create("currentState", "cust", MFnNumericData::kInt, -1, &status); |
|
numFn.setStorable(false); |
|
numFn.setWritable(false); |
|
|
|
aCurrentTime = uniFn.create("currentTime", "cuti", MFnUnitAttribute::kTime, 0.0, &status); |
|
uniFn.setKeyable(true); |
|
|
|
aInputMatrix = matFn.create("inMatrix", "inma"); |
|
matFn.setStorable(false); |
|
matFn.setReadable(false); |
|
aInputParentInverseMatrix = matFn.create("inParentInverseMatrix", "ipim"); |
|
matFn.setStorable(false); |
|
matFn.setReadable(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(aNextState); |
|
AddAttribute(aStartState); |
|
AddAttribute(aCurrentState); |
|
AddAttribute(aCurrentTime); |
|
AddAttribute(aInputMatrix); |
|
AddAttribute(aInputParentInverseMatrix); |
|
AddAttribute(aOutputTranslate); |
|
|
|
// Affects initial state |
|
AttributeAffects(aInputMatrix, aStartState); |
|
AttributeAffects(aInputParentInverseMatrix, aOutputTranslate); |
|
|
|
// Affects simulation |
|
AttributeAffects(aCurrentTime, aStartState); |
|
AttributeAffects(aCurrentTime, aCurrentState); |
|
AttributeAffects(aCurrentTime, aOutputTranslate); |
|
AttributeAffects(aNextState, 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::setDependentsDirty(const MPlug& plugBeingDirtied, MPlugArray& affectedPlugs) { |
|
return MPxLocatorNode::setDependentsDirty(plugBeingDirtied, affectedPlugs); |
|
} |
|
|
|
|
|
static void onWorldMatrixModified(MObject& transformNode, |
|
MDagMessage::MatrixModifiedFlags& modified, |
|
void* clientData) { |
|
|
|
// `transformNode` isn't necessarily our plug-in, it would most |
|
// likely be a parent since our plug-in is a shape. But, not even |
|
// the immediate parent, it could also be a grandparent or beyond. |
|
|
|
RigidNode* node = static_cast<RigidNode*>(clientData); |
|
RigidBody& rigid = gRigids[node->uid()]; |
|
|
|
if (rigid.editable) { |
|
MStatus status; |
|
MFnDagNode fn { transformNode }; |
|
|
|
// Route: inclusiveMatrix() |
|
// Seems like a bad idea, going outside of the black-box |
|
// ------------------------ |
|
// MDagPath dagPath; |
|
// fn.getPath(dagPath); |
|
// MMatrix matrix = dagPath.inclusiveMatrix(&status); |
|
// assert(status == MS::kSuccess); |
|
|
|
// MFnMatrixData matrixData; |
|
// MObject matrixObj = matrixData.create(matrix, &status); |
|
// assert(status == MS::kSuccess); |
|
|
|
// MPlug matrixPlug { node->thisMObject(), RigidNode::aInputMatrix }; |
|
// matrixPlug.setMObject(matrixObj); |
|
|
|
// Route: MFnTransform |
|
// Bad status, unclear as to why |
|
// ------------------- |
|
// MFnTransform fnt { transformNode }; |
|
// MVector translate = fnt.getTranslation(MSpace::kWorld, &status); |
|
// assert(status == MS::kSuccess); |
|
// MQuaternion rotate; |
|
// status = fnt.getRotation(rotate, MSpace::kWorld); |
|
// assert(status == MS::kSuccess); |
|
|
|
// Route: worldMatrix |
|
// ------------------- |
|
MPlug worldMatrixCompound { node->thisMObject(), MPxLocatorNode::worldMatrix }; |
|
MPlug worldMatrix = worldMatrixCompound.elementByLogicalIndex(0, &status); |
|
assert(status == MS::kSuccess); |
|
MObject obj = worldMatrix.asMObject(&status); |
|
assert(status == MS::kSuccess); |
|
MFnMatrixData data(obj, &status); |
|
assert(status == MS::kSuccess); |
|
MMatrix matrix = data.matrix(&status); |
|
assert(status == MS::kSuccess); |
|
|
|
rigid.edited = true; |
|
rigid.editedMatrix = matrix; |
|
|
|
Print() << "+-- onWorldMatrixModified(): Recorded rigid[" << rigid.uid << "].translate" << Translate(matrix) << "\n"; |
|
} |
|
} |
|
|
|
|
|
static void onAttributeChanged(MNodeMessage::AttributeMessage msg, MPlug& plug, MPlug& otherPlug, void* clientData) { |
|
if (plug == RigidNode::worldMatrix) { |
|
if (msg & MNodeMessage:: kAttributeEval) { |
|
Print() << "--- aInputMatrix evaluaeted\n"; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* This isn't a good idea, we shouldn't evaluate during dirty propagation |
|
* |
|
*/ |
|
// static void onPlugDirty(MObject& obj, MPlug& plug, void* clientData) { |
|
// if (plug == RigidNode::aInputMatrix) { |
|
// MStatus status { MS::kSuccess }; |
|
// Print() << "--- onPlugDirty()\n"; |
|
|
|
// RigidNode* node = static_cast<RigidNode*>(clientData); |
|
// RigidBody& rigid = gRigids.at(node->uid()); |
|
|
|
// if (rigid.editable) { |
|
// Print() << " +++ rigid.editable\n"; |
|
// MPlug plug { obj, RigidNode::aInputMatrix }; |
|
|
|
// MObject obj = plug.asMObject(&status); |
|
// assert(status == MS::kSuccess); |
|
// MFnMatrixData data(obj, &status); |
|
// assert(status == MS::kSuccess); |
|
// MMatrix matrix = data.matrix(&status); |
|
// assert(status == MS::kSuccess); |
|
// rigid.editedMatrix = matrix; |
|
// rigid.edited = true; |
|
// } |
|
// } |
|
// } |
|
|
|
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 }; |
|
|
|
MFnDagNode fn { thisMObject() }; |
|
MDagPath dagPath; |
|
fn.getPath(dagPath); |
|
|
|
_callbackIds.append( |
|
MDagMessage::addWorldMatrixModifiedCallback( |
|
dagPath, onWorldMatrixModified, this, &status |
|
) |
|
); |
|
|
|
// Bypass warning; below callback takes a reference, |
|
// even though it doesn't care for actually keeping |
|
// it around. Bad design. |
|
// MObject tempobj { this->thisMObject() }; |
|
|
|
// _callbackIds.append( |
|
// MNodeMessage::addNodeDirtyPlugCallback( |
|
// tempobj, onPlugDirty, this, &status |
|
// ) |
|
// ); |
|
// assert(status == MS::kSuccess); |
|
|
|
/** |
|
* This doesn't work, it won't trigger on changes to connected |
|
* attributes, like the pCube1.matrix |
|
* |
|
*/ |
|
// _callbackIds.append( |
|
// MNodeMessage::addAttributeChangedCallback( |
|
// tempobj, onAttributeChanged, this, &status |
|
// ) |
|
// ); |
|
assert(status == MS::kSuccess); |
|
} |
|
|
|
|
|
void RigidNode::reset(MDataBlock& datablock) { |
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
|
|
MFnDagNode fn { thisMObject() }; |
|
|
|
rigid.name = fn.name(); |
|
|
|
if (rigid.edited) { |
|
Print() << "<-- Storing recorded value..\n"; |
|
MFnMatrixData matrixData; |
|
MObject matrixObj = matrixData.create(rigid.editedMatrix, &status); |
|
assert(status == MS::kSuccess); |
|
|
|
MPlug matrixPlug { thisMObject(), aInputMatrix }; |
|
status = matrixPlug.setMObject(matrixObj); |
|
assert(status == MS::kSuccess); |
|
|
|
rigid.edited = false; |
|
rigid.restMatrix = rigid.editedMatrix; |
|
} |
|
else { |
|
Print() << "--> Restoring recorded value..\n"; |
|
MMatrix matrix = InputValue(datablock, aInputMatrix).asMatrix(); |
|
rigid.parentInverseMatrix = InputValue(datablock, aInputParentInverseMatrix).asMatrix(); |
|
rigid.restMatrix = matrix * rigid.parentInverseMatrix.inverse(); |
|
} |
|
|
|
rigid.worldMatrix = rigid.restMatrix; |
|
|
|
Print() << "(---) RigidNode: Rigid[" << _uid << "] reset @ " << Translate(rigid.restMatrix) << "\n"; |
|
} |
|
|
|
|
|
/** |
|
* Restore rest matrix |
|
* |
|
*/ |
|
MStatus RigidNode::computeStartState(const MPlug& plug, MDataBlock& datablock) { |
|
Print() << "RigidNode:: Rigid[" << _uid << "].computeStartState() ===============\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
|
|
// Establish position from creation |
|
if (_lastFrame == -1) { |
|
reset(datablock); |
|
// rigid. = true; |
|
} |
|
|
|
rigid.worldMatrix = rigid.restMatrix; |
|
rigid.velocity = {0, 0, 0}; |
|
rigid.editable = true; |
|
|
|
MFnDagNode fn { thisMObject() }; |
|
rigid.name = fn.name(); |
|
|
|
OutputValue(datablock, aStartState).set(_uid); |
|
|
|
datablock.setClean(plug); |
|
|
|
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 }; |
|
|
|
RigidBody& rigid = gRigids.at(_uid); |
|
rigid.editable = false; |
|
|
|
// Pick up the possibly-modified initial position |
|
if (_lastFrame == 0) { |
|
reset(datablock); |
|
} |
|
|
|
OutputValue(datablock, aCurrentState).set(_uid); |
|
|
|
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); |
|
|
|
// Pull and let the SolverNode go to work.. |
|
_lastFrame = InputValue(datablock, aNextState).asInt(); |
|
|
|
Print() << "Last frame: " << _lastFrame << "\n"; |
|
|
|
// . |
|
// .. |
|
// ... |
|
// aaaaand we're back. |
|
|
|
// At this point, the solver will have pulled on either.. |
|
// |
|
// - RigidNode::computeStartState |
|
// - RigidNode::computeCurrentState |
|
// |
|
// ..depending on what time it was. They will in turn have populated |
|
// our translate values with the latest up-to-date simulated values |
|
// |
|
|
|
// Preserve hierarchy when manipulating an object during standstill |
|
if (rigid.worldMatrixChanged) { |
|
rigid.parentInverseMatrix = InputValue(datablock, aInputParentInverseMatrix).asMatrix(); |
|
rigid.worldMatrixChanged = false; |
|
} |
|
|
|
MMatrix localMatrix = rigid.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); |
|
|
|
Print() << "RigidNode: Rigid[" << _uid << "].translate" << translate << " written\n"; |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::computeUid(const MPlug& plug, MDataBlock& datablock) { |
|
if (debug()) Print() << "RigidNode::computeUid()\n"; |
|
MStatus status { MS::kSuccess }; |
|
|
|
// We'll use this from the solver mostly to sanity-check |
|
// that we're talking to the rigid we think we're talking to. |
|
MDataHandle outputHandle = OutputValue(datablock, aUid); |
|
outputHandle.setInt(_uid); |
|
|
|
datablock.setClean(plug); |
|
|
|
return status; |
|
} |
|
|
|
|
|
MStatus RigidNode::compute(const MPlug& plug, MDataBlock& datablock) { |
|
MStatus status { MS::kUnknownParameter }; |
|
|
|
// 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 == aOutputTranslate || |
|
plug == aOutputTranslateX || |
|
plug == aOutputTranslateY || |
|
plug == aOutputTranslateZ) { |
|
status = computeOutputTranslate(plug, datablock); |
|
} |
|
|
|
else if (plug == aUid) { |
|
status = computeUid(plug, datablock); |
|
} |
|
|
|
return status; |
|
} |