CAPI: Case Study: Deformer inside DCC
Hey folks,
in this post I’d like to look at a case study of implementing a custom deformer inside a DCC. The deformer is more like a “deformer-host”, since it can become any kind of deformer effect. The source code and its parameters are flexible and user-defined, so the C++ work has to be done only once for a variety of deformers. Also once the C++ work is done, the TDs can define high performance deforms without the need of any additional C++ compilation and distribution.
I’ve chosen Softimage for this sample, but you can implement the very same functionality inside of any other DCC (such as Maya, for example). The implementation will be done in C++ and utilize our CAPI. The source code of this case study with ship with the beta version of Creation Platform starting at 1.0.30, the CAPI will be shipped with the beta version of Fabric Engine Core starting at 1.5.
Here’s a video accompanying this blog post showing the deformer in action.
These are the requirements:
- Maximum performance for memory traffic
- Variable number of input parameters
- Variable number of input transforms
- Just-in-time compilation
- Persistence within the DCC
For number 1, when you register the types to KL, you can change the basic member types to match your DCCs memory layout. Softimage for example is using “double” values for its math types, therefore I am switching the “Scalar” to “Float64″:
FabricEngine::Core::RTStructMemberInfo Vec3Members[3] = {
{ "x", "Float64" },
{ "y", "Float64" },
{ "z", "Float64" }
};
I load the KL bindings from the types defined in Creation Platform when registering the type, so that the TD in the DCC can use the full potential of the KL types.
For number 2, the command to create the operator takes in a string option which encodes all parameters to define. This option is a comma-separated list of parameter names and types, which then are added both to the DCC operator as well as the Fabric Engine node as members.
parameters = "factor=Scalar,frequency=Scalar,amplitude=Scalar,offset=Vec3"
The variable number of input transforms is solved by the number of selected additional objects (aside from the one to deform). The transforms may also be specified as a comma-separated option listing all of the transforms to use.
Inside the DCC operator, utilizing the CAPI, we can then construct the basic Fabric Engine Core graph to use for the deformation. Since deformation should happen in a multithreaded fashion, two nodes are constructed: A sliced geometryNode (1 slice per vertex) and a single-slice parameterNode. The geometryNode only holds the position, while the parameterNode holds the “xfos” list as well as all additional optional parameters:
// construct the DG elements
geometryNode = FabricEngine::Core::DGNode(gDeformerClient, "geometry");
parameterNode = FabricEngine::Core::DGNode(gDeformerClient, "parameters");
geometryNode.setDependency("parameters", parameterNode);
// setup the geometry members
geometryNode.addMember("position", "Vec3");
// setup the parameter members
parameterNode.addMember("xfos", "Xfo[]");
CString parametersJoined = ctxt.GetParameterValue(L"parameters");
CStringArray parameters = parametersJoined.Split(L",");
for(LONG i=0;i<parameters.GetCount();i++)
{
CStringArray tokens = parameters[i].Split(L"=");
if(tokens[1] == L"Scalar" ||
tokens[1] == L"Size" ||
tokens[1] == L"Integer" ||
tokens[1] == L"String" ||
tokens[1] == L"Boolean" ||
tokens[1] == L"Vec3" ||
tokens[1] == L"Quat")
{
parameterNode.addMember(tokens[0].GetAsciiString(), tokens[1].GetAsciiString());
parameterNames.push_back(tokens[0].GetAsciiString());
parameterTypes.push_back(tokens[1].GetAsciiString());
}
}
The used “DGOperator” can then be bound using a parameter layout based on the “position”, “xfos” and the additional parameters:
op = FabricEngine::Core::DGOperator(gDeformerClient, opName.GetAsciiString());
// construct the layout
std::vector<std::string> paramLayout;
std::vector<const char*> paramLayoutChar;
paramLayout.push_back("self.position");
paramLayout.push_back("parameters.xfos");
for(size_t i=0;i<parameterNames.size();i++)
paramLayout.push_back(std::string("parameters.")+parameterNames[i].c_str());
for(size_t i=0;i<paramLayout.size();i++)
paramLayoutChar.push_back(paramLayout[i].c_str());
// construct the binding
FabricEngine::Core::DGBinding binding(
op,
(uint32_t)paramLayoutChar.size(),
¶mLayoutChar[0]
);
geometryNode.appendBinding( binding );
When the user changes the source code, we can now update it on the operator to recompile it. We can also massage the source code prior to that to do string replacements, for example, or code extensions:
sourceCode = "require Vec3;require Quat;require Xfo;" + sourceCode;
std::string paramList = "io Vec3 position, io Xfo xfos[]";
for(size_t i=0;i<parameterNames.size();i++)
paramList += ", io " + parameterTypes[i] + " " + parameterNames[i];
sourceCode = replaceStdString(sourceCode, "$PARAMS", paramList);
op.setSourceCode(sourceCode.c_str());
op.setEntryPoint(entryFunction.c_str());
On every invocation of the deformer, all transforms as well all parameters have to be set on the parameterNode, and the incoming positions have to be set on the geometryNode. For the parameters you can use the “Variant” type for setting all parameters in a single call …
FabricEngine::Core::Variant allVariants= FabricEngine::Core::Variant::CreateDict();
// loop over all additional parameters
for(size_t i=0;i<parameterTypes.size();i++)
{
FabricEngine::Core::Variant variant;
if(parameterTypes[i] == "Scalar")
{
double value = ctxt.GetParameterValue(CString(parameterNames[i].c_str()));
variant = FabricEngine::Core::Variant::CreateFloat64(value);
}
/* ... */
else
continue;
// push the variant to the list of variants
allVariants.setDictValue(parameterNames[i].c_str(), variant);
}
// set all of the member data at once
parameterNode.setSliceData_Variant(0, allVariants);
… and for the positions we can use the “void *” interface. Notice that the size of the DCC’s “CVector3″ and Fabric Engine’s “Vec3″ match due to the correct type definitions earlier.
// retrieve the positions from the DCC
Geometry inputGeo = Primitive(ctxt.GetInputValue(0)).GetGeometry();
MATH::CVector3Array positions = inputGeo.GetPoints().GetPositionArray();
// make enough space in fabric
geometryNode.setSize((uint32_t)positions.GetCount());
// now copy in all of the positions
geometryNode.setMemberAllSlicesData("position", sizeof(MATH::CVector3) * positions.GetCount(), &positions[0]);
Now the deformer is ready to perform. All we have to do is to evaluate the geometryNode and retrieve the positions data back out:
// evaluate the geometryNode
geometryNode.evaluate();
// copy the positions back
geometryNode.getMemberAllSlicesData("position", sizeof(MATH::CVector3) * positions.GetCount(), &positions[0]);
Geometry outputGeo = Primitive(ctxt.GetOutputTarget()).GetGeometry();
outputGeo.GetPoints().PutPositionArray(positions);
I hope this was informative, for any questions feel free to comment or post on the Creation Platform mailing list!
Let me know what you think.
Twitter
Facebook
LinkedIn
Vimeo
Google +