Skip to content

Instantly share code, notes, and snippets.

@mottosso
Last active February 11, 2021 15:57
Show Gist options
  • Save mottosso/8eb83c940d7a7627443e2093cfb4bbac to your computer and use it in GitHub Desktop.
Save mottosso/8eb83c940d7a7627443e2093cfb4bbac to your computer and use it in GitHub Desktop.
Editable Initial State - Attempt 4

⬅️ Back

If the problem is inWorldMatrix not being evaluated reliably when time changes, what if we explicitly pull inWorldMatrix whenever time changes?

We can't pull it when outputTranslate is being computed, because it's a cycle. But we can call it at any other point in time. So how about we setup a callback to whenever currentTime is dirtied, and pull it then? We don't necessarily care for the result, we just want outputValue to return the value from the previous time.

attempt47

In short, this doesn't work.

  1. Dirty doesn't trigger on undo
  2. Dirty doesn't trigger on channel box edits

We could work around that with more callbacks, but I suspect we'll never catch'em'all, which is an invitation for bugs and edge cases.


Build & Run

From Powershell.

cd c:\
git clone https://gist.github.com/8eb83c940d7a7627443e2093cfb4bbac.git initialStateNode4
mkdir initialStateNode4/build
cd initialStateNode4/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:\initialState4\build\initialStateNode.mll"
cmds.loadPlugin(fname)

time = "time1"
cube, _ = cmds.polyCube()
cmds.move(-3, 15, -7)
cmds.scale(4, 4, 4)
node = cmds.createNode("initialStateNode")
cmds.setAttr(node + ".startTime", cmds.getAttr(time + ".outTime"))
cmds.setAttr(node + ".gravity", 0, -9.81, 0)
cmds.connectAttr(time + ".outTime", node + ".currentTime")
cmds.connectAttr(cube + ".worldMatrix[0]", node + ".inWorldMatrix")
cmds.connectAttr(node + ".outputTranslateX", cube + ".translateX")
cmds.connectAttr(node + ".outputTranslateY", cube + ".translateY")
cmds.connectAttr(node + ".outputTranslateZ", cube + ".translateZ")

#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
initialStateNode.cpp
)
set(LIBRARIES
OpenMaya
Foundation
)
build_plugin()
#include <string.h>
#include <maya/MPxNode.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFnMatrixAttribute.h>
#include <maya/MFnUnitAttribute.h>
#include <maya/MFnPlugin.h>
#include <maya/MMessage.h>
#include <maya/MTypeId.h>
#include <maya/MPlug.h>
#include <maya/MVector.h>
#include <maya/MTransformationMatrix.h>
#include <maya/MFnMatrixData.h>
#include <maya/MMatrix.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MStreamUtils.h>
#include <maya/MNodeMessage.h>
#include <maya/MCallbackIdArray.h>
#define Print MStreamUtils::stdErrorStream
class initialStateNode : public MPxNode {
public:
initialStateNode();
~initialStateNode() override;
MStatus compute(const MPlug& plug, MDataBlock& data) override;
MStatus computeStartState(const MPlug& plug, MDataBlock& data);
MStatus computeFirstState(const MPlug& plug, MDataBlock& data);
MStatus computeCurrentState(const MPlug& plug, MDataBlock& data);
MStatus writeOutputTranslate(MDataBlock& datablock);
MStatus stepSimulation(MDataBlock& datablock);
static void* creator() { return new initialStateNode(); }
static MStatus initialize();
void postConstructor() override;
bool isPassiveOutput(const MPlug& plug) const override;
public:
static MTypeId id;
// Attributes
static MObject aStartTime;
static MObject aCurrentTime;
static MObject aGravity;
static MObject aGravityX;
static MObject aGravityY;
static MObject aGravityZ;
static MObject aInWorldMatrix;
static MObject aOutputTranslate;
static MObject aOutputTranslateX;
static MObject aOutputTranslateY;
static MObject aOutputTranslateZ;
private:
bool _isStartTime { false };
MTime _lastTime { };
MTime _currentTime { };
MMatrix _restMatrix {};
MMatrix _outputMatrix {};
MVector _velocity { 0, 0, 0 };
MCallbackIdArray _callbackIds;
bool _isFirstTime { false };
};
MTypeId initialStateNode::id(0x80012);
MObject initialStateNode::aInWorldMatrix;
MObject initialStateNode::aOutputTranslate;
MObject initialStateNode::aOutputTranslateX;
MObject initialStateNode::aOutputTranslateY;
MObject initialStateNode::aOutputTranslateZ;
MObject initialStateNode::aGravity;
MObject initialStateNode::aGravityX;
MObject initialStateNode::aGravityY;
MObject initialStateNode::aGravityZ;
MObject initialStateNode::aStartTime;
MObject initialStateNode::aCurrentTime;
initialStateNode::initialStateNode() {}
initialStateNode::~initialStateNode() {
for (unsigned int ii=0; ii<_callbackIds.length(); ii++)
MMessage::removeCallback(_callbackIds[ii]);
_callbackIds.clear();
}
bool initialStateNode::isPassiveOutput(const MPlug& plug) const {
if (plug == aOutputTranslate ||
plug == aOutputTranslateX ||
plug == aOutputTranslateY ||
plug == aOutputTranslateZ) {
return true;
}
return false;
}
MStatus initialStateNode::initialize() {
MStatus status;
MFnNumericAttribute numFn;
MFnMatrixAttribute matFn;
MFnUnitAttribute uniFn;
aStartTime = uniFn.create("startTime", "stti", MFnUnitAttribute::kTime, 0.0, &status);
aCurrentTime = uniFn.create("currentTime", "cuti", MFnUnitAttribute::kTime, 0.0, &status);
aInWorldMatrix = matFn.create("inWorldMatrix", "inwm");
aOutputTranslateX = uniFn.create("outputTranslateX", "outx", MFnUnitAttribute::kDistance, 0.0, &status);
aOutputTranslateY = uniFn.create("outputTranslateY", "outy", MFnUnitAttribute::kDistance, 0.0, &status);
aOutputTranslateZ = uniFn.create("outputTranslateZ", "outz", MFnUnitAttribute::kDistance, 0.0, &status);
aOutputTranslate = numFn.create("outputTranslate", "outr", aOutputTranslateX, aOutputTranslateY, aOutputTranslateZ);
numFn.setWritable(false);
numFn.setStorable(false);
// numFn.setAffectsWorldSpace(true); // ! Causes crash
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);
addAttribute(aStartTime);
addAttribute(aCurrentTime);
addAttribute(aInWorldMatrix);
addAttribute(aOutputTranslate);
addAttribute(aGravity);
attributeAffects(aGravity, aOutputTranslate);
attributeAffects(aCurrentTime, aOutputTranslate);
return MS::kSuccess;
}
static void onPlugDirty(MObject& node, MPlug& plug, void* clientData) {
if (plug == initialStateNode::aCurrentTime) {
MPlug inWorldMatrix { node, initialStateNode::aInWorldMatrix };
// Just pull, results don't matter
MObject obj = inWorldMatrix.asMObject();
MFnMatrixData(obj).matrix();
}
}
void initialStateNode::postConstructor() {
MStatus status { MS::kSuccess };
// 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, nullptr, &status
)
);
}
/**
* Read and store rest matrix
*
*/
MStatus initialStateNode::computeStartState(const MPlug& plug, MDataBlock& datablock) {
Print() << "computeStartState()\n";
MStatus status { MS::kSuccess };
_outputMatrix = _restMatrix;
_isFirstTime = true;
writeOutputTranslate(datablock);
datablock.setClean(plug);
return status;
}
/**
* Following the start time
*
*/
MStatus initialStateNode::computeFirstState(const MPlug& plug, MDataBlock& datablock) {
Print() << " FirstState()\n";
MStatus status { MS::kUnknownParameter };
// Now we can trust that the value has definitely
// been computed via the callback (?)
_restMatrix = datablock.outputValue(aInWorldMatrix).asMatrix();
_outputMatrix = _restMatrix;
_velocity = { 0, 0, 0 };
_isFirstTime = false;
stepSimulation(datablock);
writeOutputTranslate(datablock);
datablock.setClean(plug);
return status;
}
/**
* Following the first frame
*
*/
MStatus initialStateNode::computeCurrentState(const MPlug& plug, MDataBlock& datablock) {
Print() << " CurrentState()\n";
MStatus status { MS::kSuccess };
stepSimulation(datablock);
writeOutputTranslate(datablock);
datablock.setClean(plug);
return status;
}
/**
* Apply gravity and update position
*
*/
MStatus initialStateNode::stepSimulation(MDataBlock& datablock) {
MStatus status { MS::kSuccess };
MTransformationMatrix tm { _outputMatrix };
double deltaTime = (_currentTime - _lastTime).as(MTime::kSeconds);
if (deltaTime > 0.0) {
MVector gravity = datablock.inputValue(aGravity, &status).asDouble3();
_velocity += gravity * deltaTime;
tm.addTranslation(_velocity, MSpace::kTransform);
}
_outputMatrix = tm.asMatrix();
return status;
}
MStatus initialStateNode::writeOutputTranslate(MDataBlock& datablock) {
MStatus status { MS::kSuccess };
MTransformationMatrix tm { _outputMatrix };
MVector translate = tm.translation(MSpace::kTransform);
MDataHandle tx = datablock.outputValue(aOutputTranslateX),
ty = datablock.outputValue(aOutputTranslateY),
tz = datablock.outputValue(aOutputTranslateZ);
tx.set(translate.x);
ty.set(translate.y);
tz.set(translate.z);
Print() << "<-- Writing: " << translate << "\n";
return status;
}
MStatus initialStateNode::compute(const MPlug& plug, MDataBlock& datablock) {
MStatus status { MS::kUnknownParameter };
const MTime currentTime = datablock.inputValue(aCurrentTime).asTime();
const MTime startTime = datablock.inputValue(aStartTime).asTime();
_currentTime = currentTime;
_isStartTime = currentTime <= startTime;
if (plug == aOutputTranslate ||
plug == aOutputTranslateX ||
plug == aOutputTranslateY ||
plug == aOutputTranslateZ) {
if (_isStartTime) status = computeStartState(plug, datablock);
else if (_isFirstTime) status = computeFirstState(plug, datablock);
else status = computeCurrentState(plug, datablock);
}
_lastTime = currentTime;
return status;
}
MStatus initializePlugin(MObject obj) {
MStatus status;
MFnPlugin plugin(obj, PLUGIN_COMPANY, "3.0", "Any");
status = plugin.registerNode("initialStateNode",
initialStateNode::id,
initialStateNode::creator,
initialStateNode::initialize);
if (!status) {
status.perror("registerNode");
return status;
}
Print() << "==============\n";
Print() << "Initialising\n";
return status;
}
MStatus uninitializePlugin(MObject obj) {
MStatus status;
MFnPlugin plugin(obj);
status = plugin.deregisterNode(initialStateNode::id);
if (!status) {
status.perror("deregisterNode");
return status;
}
Print() << "Deinitialising.\n";
Print() << "==============\n";
return status;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment