Skip to content

Instantly share code, notes, and snippets.

@mottosso
Last active February 24, 2021 16:50
Show Gist options
  • Save mottosso/9e6ab97b7a7b9aa1c1621d7e1cd3ddb8 to your computer and use it in GitHub Desktop.
Save mottosso/9e6ab97b7a7b9aa1c1621d7e1cd3ddb8 to your computer and use it in GitHub Desktop.
Editable Initial State - Attempt 16

⬅️ Back

In the previous example, we got DG and Parallel both working. But setting the initial state was still a manual effort. In this attempt, we act on time changes via a callback, before evaluation starts. That way, (1) time changes, (2) we record worldmatrices as-they-are, (3) simulation runs.

In this case, the inWorldMatrix is connected but not actually pulled during evaluation. Instead, we cache the immediate value of inWorldMatrix in an internal, hidden inRestMatrix attribute. The trick being that we pull inRestMatrix during evaluation, and update it separately, from the onTimeChanged callback. Trusting that this callback triggers ahead of any and all evaluation whenever time changes. Which seems to be the case.

It works for a limited set of uses, I'm hoping the rest will be up to logic.

basicsOk

I can manipulate the start frame, which is ok. delaystartok

On the start frame, we can safely manipulate the hierarchy, but not before the start frame. I'm expecting a logic mishap here, something within our control to fix.

cancelouthierarchybad

So long as the time is actually standing on the start frame, we are OK.

modifyatstartOk

Oddity

But then there's this. What on earth is happening here?!

breakwihbullet

Internally, I can see that when a Bullet node is created, the evaluation of my own nodes changes. Now computeStartState is called on every frame. Why?! This is the strangest thing. I'm hoping we can solve this through logic.. but I'm at a loss as to how and why Bullet - and likely any other node - can influence which plugs get evaluated and when.


Build & Run

From Powershell.

cd c:\
git clone https://gist.github.com/9e6ab97b7a7b9aa1c1621d7e1cd3ddb8.git initialStateNode16
mkdir initialStateNode16/build
cd initialStateNode10/build
$env:DEVKIT_LOCATION="c:\path\to\your\devkit"
cmake .. -G Ninja
ninja

Then from the Script Editor.

import os
from maya import cmds

fname = r"c:\initialState16\build\initialStateNode.mll"
cmds.loadPlugin(fname)

time = "time1"
solver = cmds.createNode("solverNode", parent=cmds.createNode("transform", name="solver"))
cmds.setAttr(solver + ".gravity", 0, -9.81, 0)
cmds.setAttr(solver + ".startTime", cmds.getAttr(time + ".outTime"))
cmds.connectAttr(time + ".outTime", solver + ".currentTime")

parent = None
for i in range(3):
    cube, _ = cmds.polyCube()

    if parent:
        cmds.parent(cube, parent, relative=True)
        cmds.move(3, 0, 0, relative=True)
    else:
        cmds.move(-3 * i, 15, -7)

    rigid = cmds.createNode("rigidNode", parent=cube)

    cmds.connectAttr(time + ".outTime", rigid + ".currentTime")
    cmds.connectAttr(cube + ".parentInverseMatrix", rigid + ".inParentInverseMatrix")
    cmds.connectAttr(cube + ".worldMatrix[0]", rigid + ".inWorldMatrix")
    cmds.connectAttr(rigid + ".outputTranslateX", cube + ".translateX")
    cmds.connectAttr(rigid + ".outputTranslateY", cube + ".translateY")
    cmds.connectAttr(rigid + ".outputTranslateZ", cube + ".translateZ")

    # Connect to scene
    cmds.connectAttr(rigid + ".startState", solver + ".startState[%d]" % i)
    cmds.connectAttr(rigid + ".currentState", solver + ".currentState[%d]" % i)
    cmds.connectAttr(solver + ".nextState[%d]" % i, rigid + ".nextState")

    parent = cube


cmds.file(new=True, force=True)
cmds.unloadPlugin(os.path.basename(fname))
cmake_minimum_required(VERSION 2.8)
include($ENV{DEVKIT_LOCATION}/cmake/pluginEntry.cmake)
set(PROJECT_NAME initialStateNode)
set(SOURCE_FILES
main.cpp
)
set(LIBRARIES
OpenMaya
OpenMayaRender
OpenMayaAnim
OpenMayaUI
Foundation
)
add_compile_definitions("/D_DEBUG /Zi")
build_plugin()
#include <string.h>
#include <unordered_map>
#include <vector>
#include <cassert>
#include <maya/MPxLocatorNode.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFnMatrixAttribute.h>
#include <maya/MFnUnitAttribute.h>
#include <maya/MFnPlugin.h>
#include <maya/MFnDagNode.h>
#include <maya/MFnTransform.h>
#include <maya/MDagPath.h>
#include <maya/MVector.h>
#include <maya/MQuaternion.h>
#include <maya/MTypeId.h>
#include <maya/MPlug.h>
#include <maya/MString.h>
#include <maya/MVector.h>
#include <maya/MTransformationMatrix.h>
#include <maya/MMatrix.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MStreamUtils.h>
#include <maya/MNodeMessage.h>
#include <maya/MFnDependencyNode.h>
#include <maya/MFnMatrixData.h>
#include <maya/MCallbackIdArray.h>
#include <maya/MGlobal.h>
#include <maya/MMessage.h>
#include <maya/MDGMessage.h>
#include <maya/MDagMessage.h>
#include <maya/MAnimControl.h>
#include <maya/MEvaluationNode.h>
#include <maya/MEvaluationManager.h>
#define Print MStreamUtils::stdErrorStream
#define Translate(mat) MTransformationMatrix(mat).translation(MSpace::kTransform)
#define AddAttribute(attr) assert(addAttribute(attr) == MS::kSuccess);
#define AttributeAffects(attr, affects) assert(attributeAffects(attr, affects) == MS::kSuccess);
static bool debug() {
static bool debugInfoExists { false };
const bool debugInfo = MGlobal::optionVarIntValue("debugInfo", &debugInfoExists);
return debugInfoExists ? debugInfo : false;
}
// Global counter, for a unique UID per rigid
static int gUniqueIdCounter { 1000 };
struct Transform {
MMatrix matrix;
MVector velocity;
};
struct RigidBody {
int uid { -1 };
MString name;
MVector velocity { 0, 0, 0 };
float damping { 0.5f };
MMatrix parentInverseMatrix {};
MMatrix restMatrix {};
MMatrix worldMatrix {};
bool worldMatrixChanged { true };
bool initialStateModified { false };
std::vector<Transform> history;
};
using RigidMap = std::unordered_map<int, RigidBody>;
// For purposes of demonstration, rather
// than passing data between nodes, we'll
// use a global variable and pass an index
// into this map between nodes instead.
static RigidMap gRigids;
// Helper functions
inline MDataHandle InputValue(MDataBlock& datablock, MObject attr) {
MStatus status;
MDataHandle handle = datablock.inputValue(attr, &status);
assert(status == MS::kSuccess);
return handle;
}
inline MDataHandle OutputValue(MDataBlock& datablock, MObject attr) {
MStatus status;
MDataHandle handle = datablock.outputValue(attr, &status);
assert(status == MS::kSuccess);
return handle;
}
#include "rigid.hpp"
#include "solver.hpp"
MStatus initializePlugin(MObject obj) {
MStatus status;
MFnPlugin plugin(obj, PLUGIN_COMPANY, "3.0", "Any");
status = plugin.registerNode(SolverNode::name,
SolverNode::id,
SolverNode::creator,
SolverNode::initialize,
MPxNode::kLocatorNode,
&SolverNode::drawDbClassification);
status = plugin.registerNode(RigidNode::name,
RigidNode::id,
RigidNode::creator,
RigidNode::initialize,
MPxNode::kLocatorNode,
&RigidNode::drawDbClassification);
Print() << "==============\n";
Print() << "Initialising\n";
return status;
}
MStatus uninitializePlugin(MObject obj) {
MStatus status;
MFnPlugin plugin(obj);
status = plugin.deregisterNode(RigidNode::id);
status = plugin.deregisterNode(SolverNode::id);
Print() << "Deinitialising.\n";
Print() << "==============\n";
return status;
}
/**
* 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 aCurrentTime;
static MObject aInputRestMatrix;
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 };
int _previousFrame { -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::aInputRestMatrix;
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);
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);
aInputRestMatrix = matFn.create("inRestMatrix", "inre");
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(aDamping);
AddAttribute(aNextState);
AddAttribute(aStartState);
AddAttribute(aCurrentState);
AddAttribute(aCurrentTime);
AddAttribute(aInputRestMatrix);
AddAttribute(aInputWorldMatrix);
AddAttribute(aInputParentInverseMatrix);
AddAttribute(aOutputWorldMatrix);
AddAttribute(aOutputTranslate);
// Affects initial state
AttributeAffects(aInputRestMatrix, aStartState);
AttributeAffects(aInputParentInverseMatrix, aOutputWorldMatrix);
AttributeAffects(aNextState, aOutputWorldMatrix);
AttributeAffects(aCurrentTime, aOutputWorldMatrix);
AttributeAffects(aDamping, aOutputTranslate);
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.initialStateModified) {
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::aInputRestMatrix };
obj = MFnMatrixData().create(mat);
restMatrixPlug.setMObject(obj);
rigid.initialStateModified = false;
Print() << "(+) Successfully updated initial state\n";
}
}
/**
* 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::aInputRestMatrix };
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 };
updateStartState(plug, datablock);
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 };
updateCurrentState(plug, datablock);
OutputValue(datablock, aCurrentState).set(_uid);
return status;
}
MStatus RigidNode::updateStartState(const MPlug& plug, MDataBlock& datablock) {
MStatus status { MS::kSuccess };
RigidBody& rigid = gRigids.at(_uid);
// Restore initial state, do not evaluate anything
MMatrix worldMatrix = InputValue(datablock, aInputRestMatrix).asMatrix();
rigid.restMatrix = worldMatrix;
rigid.worldMatrix = rigid.restMatrix;
rigid.damping = InputValue(datablock, aDamping).asFloat();
MFnDagNode fn { thisMObject() };
rigid.name = fn.name();
OutputValue(datablock, aCurrentState).set(_uid);
return status;
}
MStatus RigidNode::updateCurrentState(const MPlug& plug, MDataBlock& datablock) {
MStatus status { MS::kSuccess };
RigidBody& rigid = gRigids.at(_uid);
rigid.damping = InputValue(datablock, aDamping).asFloat();
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);
}
if (simulatedFrame < 0) {
rigid.worldMatrixChanged = true;
}
// 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);
// Detect if evaluation happens in response to changes in initial state
if (simulatedFrame == 0 && simulatedFrame == _previousFrame) {
rigid.initialStateModified = true;
}
_previousFrame = simulatedFrame;
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;
}
/**
* Pull on all rigid at once, and solve them in one big batch.
*
*/
class SolverNode : public MPxLocatorNode {
public:
SolverNode() {};
~SolverNode() override;
static void* creator() { return new SolverNode(); }
static MStatus initialize();
MStatus compute(const MPlug& plug, MDataBlock& data) override;
MStatus computeOutputRigids(const MPlug& plug, MDataBlock& data);
MStatus addRigids(const MPlug& plug, MDataBlock& datablock);
MStatus updateRigids(const MPlug& plug, MDataBlock& datablock);
MStatus setDependentsDirty(const MPlug&, MPlugArray&) override;
void postConstructor() override;
MStatus stepSimulation(MDataBlock& datablock);
MStatus restoreSimulation(MDataBlock& datablock);
public:
static MString name;
static MTypeId id;
static const MString drawDbClassification;
static const MString drawRegistrantId;
// Attributes
static MObject aStartTime;
static MObject aCurrentTime;
static MObject aGravity;
static MObject aGravityX;
static MObject aGravityY;
static MObject aGravityZ;
// Initial state of each rigid, e.g. position and orientation
static MObject aStartState;
// Current values of rigid plugs, e.g. damping
static MObject aCurrentState;
// Resulting transforms after simulation
static MObject aNextState;
private:
bool _isStartTime { false };
bool _beforeStartTime { false };
bool _isFirstTime { false };
MTime _lastTime { };
MTime _currentTime { };
double _deltaTime { 0.0 };
// 0-based time
int _startFrame { };
int _currentFrame { };
int _simulationFrame { 0 };
MCallbackIdArray _callbackIds;
};
MString SolverNode::name("solverNode");
MTypeId SolverNode::id(0x80013);
const MString SolverNode::drawDbClassification("drawdb/geometry/SolverNode");
const MString SolverNode::drawRegistrantId("SolverNodePlugin");
MObject SolverNode::aCurrentState;
MObject SolverNode::aStartState;
MObject SolverNode::aNextState;
MObject SolverNode::aStartTime;
MObject SolverNode::aCurrentTime;
MObject SolverNode::aGravity;
MObject SolverNode::aGravityX;
MObject SolverNode::aGravityY;
MObject SolverNode::aGravityZ;
/**
* To help separate print statements in the Output Window
*
*/
static void onForceUpdate(MTime& time, void* clientData) {
Print() << "\n";
Print() << "-------------------------------------------------------------------\n";
Print() << "(!) onForceUpdate() " << time.value() << "\n";
Print() << "-------------------------------------------------------------------\n";
Print() << "\n";
}
SolverNode::~SolverNode() {
for (unsigned int ii=0; ii<_callbackIds.length(); ii++)
MMessage::removeCallback(_callbackIds[ii]);
_callbackIds.clear();
}
MStatus SolverNode::initialize() {
MStatus status;
MFnNumericAttribute numFn;
MFnUnitAttribute uniFn;
aStartTime = uniFn.create("startTime", "stti", MFnUnitAttribute::kTime, 0.0, &status);
uniFn.setKeyable(true);
aCurrentTime = uniFn.create("currentTime", "cuti", MFnUnitAttribute::kTime, 0.0, &status);
uniFn.setKeyable(true);
aStartState = numFn.create("startState", "stst", MFnNumericData::kInt, -1, &status);
numFn.setStorable(false);
numFn.setReadable(true);
numFn.setWritable(true);
numFn.setArray(true);
numFn.setDisconnectBehavior(MFnUnitAttribute::kDelete);
aCurrentState = numFn.create("currentState", "cust", MFnNumericData::kInt, -1, &status);
numFn.setStorable(false);
numFn.setReadable(true);
numFn.setWritable(true);
numFn.setArray(true);
numFn.setDisconnectBehavior(MFnUnitAttribute::kDelete);
aNextState = numFn.create("nextState", "nest", MFnNumericData::kInt, -1, &status);
numFn.setStorable(false);
numFn.setWritable(false);
numFn.setArray(true);
numFn.setDisconnectBehavior(MFnUnitAttribute::kDelete);
aGravityX = uniFn.create("gravityX", "grvx", MFnUnitAttribute::kDistance, 0.0, &status);
aGravityY = uniFn.create("gravityY", "grvy", MFnUnitAttribute::kDistance, -981.0, &status);
aGravityZ = uniFn.create("gravityZ", "grvz", MFnUnitAttribute::kDistance, 0.0, &status);
aGravity = numFn.create("gravity", "grav", aGravityX, aGravityY, aGravityZ);
numFn.setKeyable(true);
AddAttribute(aStartTime);
AddAttribute(aCurrentTime);
AddAttribute(aGravity);
AddAttribute(aCurrentState);
AddAttribute(aStartState);
AddAttribute(aNextState);
AttributeAffects(aStartTime, aNextState);
AttributeAffects(aCurrentTime, aNextState);
AttributeAffects(aGravity, aNextState);
AttributeAffects(aStartState, aNextState);
AttributeAffects(aCurrentState, aNextState);
return status;
}
void SolverNode::postConstructor() {
MStatus status;
/**
* We aren't actually using this, but it's interesting to see
* in what order this is called relative nextState etc.
*
*/
_callbackIds.append(
MDGMessage::addForceUpdateCallback(onForceUpdate, this, &status)
);
assert(status == MS::kSuccess);
}
MStatus SolverNode::setDependentsDirty(const MPlug& plugBeingDirtied, MPlugArray& affectedPlugs) {
return MPxLocatorNode::setDependentsDirty(plugBeingDirtied, affectedPlugs);
}
MStatus SolverNode::compute(const MPlug& plug, MDataBlock& datablock) {
MStatus status { MS::kUnknownParameter };
if (plug == aNextState && plug.isElement()){
const MTime currentTime = InputValue(datablock, aCurrentTime).asTime();
const MTime startTime = InputValue(datablock, aStartTime).asTime();
_currentTime = currentTime;
_startFrame = static_cast<int>(startTime.value());
_currentFrame = static_cast<int>(currentTime.value()) - _startFrame;
_deltaTime = (_currentTime - _lastTime).as(MTime::kSeconds);
_isFirstTime = _isStartTime && _deltaTime > 0.0;
_isStartTime = currentTime == startTime;
_beforeStartTime = currentTime < startTime;
status = computeOutputRigids(plug, datablock);
_lastTime = currentTime;
}
datablock.setClean(plug);
return status;
}
MStatus SolverNode::computeOutputRigids(const MPlug& plug, MDataBlock& datablock) {
if (debug()) Print() << "SolverNode::computeOutputRigids()\n";
MStatus status { MS::kSuccess };
auto noChange = [&]() -> MStatus {
MStatus status;
const int index = plug.logicalIndex(&status);
assert(status == MS::kSuccess);
MArrayDataHandle nextStateHandle = datablock.outputArrayValue(aNextState, &status);
assert(status == MS::kSuccess);
nextStateHandle.jumpToElement(index);
MDataHandle outputHandle = nextStateHandle.outputValue(&status);
assert(status == MS::kSuccess);
outputHandle.setInt(_currentFrame);
return status;
};
if (_beforeStartTime) status = noChange();
else if (_isStartTime) status = addRigids(plug, datablock);
else status = updateRigids(plug, datablock);
// These should all have succeeded
assert(status == MS::kSuccess);
return status;
}
MStatus SolverNode::addRigids(const MPlug& plug, MDataBlock& datablock) {
MStatus status { MS::kUnknownParameter };
// In DG mode, Maya appears to cache previously-visited frames
// resulting in compute() *sometimes* not being called.
if (!MEvaluationManager::evaluationManagerActive(MDGContext::current())) {
Print() << "ogs -reset\n";
MGlobal::executeCommand("ogs -reset");
}
// Reset physics-related data
for (auto& pair : gRigids) {
RigidBody& rigid = pair.second;
rigid.worldMatrixChanged = true;
rigid.velocity = {0, 0, 0};
}
_simulationFrame = 0;
const int index = plug.logicalIndex(&status);
assert(status == MS::kSuccess);
// Only consider rigids that are still connected
MPlug startStatePlug { thisMObject(), aStartState };
MPlug elementPlug = startStatePlug.elementByLogicalIndex(index, &status);
assert(status == MS::kSuccess);
if (!elementPlug.isConnected()) {
Print() << "SolverNode: Disconnected index: " << index << "\n";
return MS::kFailure;
}
// Pull on *all* startStates
MArrayDataHandle startStateHandle = datablock.inputArrayValue(aStartState, &status);
assert(status == MS::kSuccess);
MArrayDataHandle nextStateHandle = datablock.outputArrayValue(aNextState, &status);
assert(status == MS::kSuccess);
status = startStateHandle.jumpToElement(index);
assert(status == MS::kSuccess);
status = nextStateHandle.jumpToElement(index);
assert(status == MS::kSuccess);
// Pull on rigid.outputStartState
int uid = startStateHandle.inputValue(&status).asInt();
assert(status == MS::kSuccess && uid >= 0);
// Tell rigid about which frame we just solved, i.e. the start frame.
MDataHandle outputHandle = nextStateHandle.outputValue(&status);
assert(status == MS::kSuccess);
outputHandle.setInt(_currentFrame);
Print() << "SolverNode: Solver pulled on solver.index[" << index << "] --> rigid.next[" << uid << "]\n";
return status;
}
MStatus SolverNode::updateRigids(const MPlug& plug, MDataBlock& datablock) {
// Print() << "SolverNode::updateRigids()\n";
MStatus status { MS::kSuccess };
const int index = plug.logicalIndex(&status);
assert(status == MS::kSuccess);
// Pull on *all* currentStates
MArrayDataHandle currentStateHandle = datablock.inputArrayValue(aCurrentState, &status);
assert(status == MS::kSuccess);
MArrayDataHandle nextStateHandle = datablock.outputArrayValue(aNextState, &status);
assert(status == MS::kSuccess);
status = currentStateHandle.jumpToElement(index);
assert(status == MS::kSuccess);
status = nextStateHandle.jumpToElement(index);
assert(status == MS::kSuccess);
MDataHandle inputHandle = currentStateHandle.inputValue(&status);
assert(status == MS::kSuccess);
inputHandle.asInt();
if (_currentFrame > _simulationFrame) {
stepSimulation(datablock);
}
MDataHandle outputHandle = nextStateHandle.outputValue(&status);
assert(status == MS::kSuccess);
outputHandle.setInt(_currentFrame);
return status;
}
MStatus SolverNode::restoreSimulation(MDataBlock& datablock) {
MStatus status { MS::kSuccess };
for (auto& pair : gRigids) {
RigidBody& rigid = pair.second;
Transform& transform = rigid.history.at(_currentFrame - 1);
rigid.worldMatrix = transform.matrix;
rigid.velocity = transform.velocity;
rigid.worldMatrixChanged = true;
}
_simulationFrame = _currentFrame;
return status;
}
/**
* Apply gravity and update position
*
*/
MStatus SolverNode::stepSimulation(MDataBlock& datablock) {
MStatus status { MS::kSuccess };
MVector gravity = InputValue(datablock, aGravity).asDouble3();
for (auto& pair : gRigids) {
RigidBody& rigid = pair.second;
MTransformationMatrix tm { rigid.worldMatrix };
rigid.velocity += gravity * _deltaTime;
tm.addTranslation(rigid.velocity * rigid.damping, MSpace::kTransform);
rigid.worldMatrix = tm.asMatrix();
rigid.worldMatrixChanged = true;
rigid.history.resize(_simulationFrame + 1);
rigid.history[_simulationFrame] = { rigid.worldMatrix, rigid.velocity };
}
_simulationFrame = _currentFrame;
return status;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment