diff --git a/CHANGES.md b/CHANGES.md index 58d7b71..f360bfd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -73,6 +73,8 @@ Version 1.0.0 - In Development - Move OpenVDB AX SOP into VDB/ASWF folder - Updated to use new Logger, can now report more than one error message at a time. + - Updated OpenVDB AX SOP UI to streamline interface. Renamed parameters to fit + better with existing Houdini Attribute/Volume Wrangle naming convention. CMake / Build / Testing: - Increased the minimum CMake version requirement to 3.12 diff --git a/openvdb_ax_houdini/SOP_OpenVDB_AX.cc b/openvdb_ax_houdini/SOP_OpenVDB_AX.cc index fe9182e..dbfb65b 100644 --- a/openvdb_ax_houdini/SOP_OpenVDB_AX.cc +++ b/openvdb_ax_houdini/SOP_OpenVDB_AX.cc @@ -52,6 +52,7 @@ #include #include #include +#include #include #include @@ -82,100 +83,6 @@ namespace hutil = houdini_utils; using namespace openvdb; - -//////////////////////////////////////// - - - -/// @brief OpPolicy for OpenVDB operator types from DNEG -class DnegVDBOpPolicy: public hutil::OpPolicy -{ -public: - std::string getValidName(const std::string& english) - { - UT_String s(english); - // Remove non-alphanumeric characters from the name. - s.forceValidVariableName(); - std::string name = s.toStdString(); - // Remove spaces and underscores. - name.erase(std::remove(name.begin(), name.end(), ' '), name.end()); - name.erase(std::remove(name.begin(), name.end(), '_'), name.end()); - return name; - } - - std::string getLowercaseName(const std::string& english) - { - UT_String s(english); - s.toLower(); - return s.toStdString(); - } - - std::string getName(const houdini_utils::OpFactory&, const std::string& english) override - { - return "DN_Open" + this->getValidName(english); - } - - std::string getLabelName(const houdini_utils::OpFactory& factory) override - { - return factory.english(); - } - - std::string getFirstName(const houdini_utils::OpFactory& factory) override - { - return this->getLowercaseName(this->getValidName(this->getLabelName(factory))); - } - - std::string getIconName(const houdini_utils::OpFactory& factory) override - { - return factory.flavorString() + "_OpenVDB"; - } - - std::string getTabSubMenuPath(const houdini_utils::OpFactory&) override - { - return "VDB/ASWF"; - } -}; - - -/// @brief Methods for adding a "groupTypeSimple" folder PRM_SWITCHER to the -/// OpenVDB AX SOP -/// @todo Expose option to set the switcher type in ParmFactory -void appendParameters(hutil::ParmList& templates1, const hutil::ParmList& templates2) -{ - for(size_t i = 0; i < templates2.size(); ++i) { - templates1.add(templates2.get()[i]); - } -} - -void addSimpleFolder(hutil::ParmList* templates, - const std::string& name, - const std::string& token, - const hutil::ParmList& parms) -{ - std::string* folderNameStr(new std::string(name)); - std::string* folderTokenStr(new std::string(token)); - - PRM_Name* folderToken(new PRM_Name(folderTokenStr->c_str())); - PRM_Default* folderDefaults(new PRM_Default(static_cast(parms.size()), folderNameStr->c_str())); - templates->add(PRM_Template(PRM_SWITCHER, 1, folderToken, folderDefaults, - 0, 0, 0, &PRM_SpareData::groupTypeSimple)); - appendParameters(*templates, parms); -} - -void addSimpleFolder(hutil::ParmList* templates, - const std::string& name, - const hutil::ParmList& parms) -{ - std::string token(name); - // Remove spaces and make lower case - token.erase(std::remove(token.begin(), token.end(), ' '), token.end()); - std::transform(token.begin(), token.end(), token.begin(), ::tolower); - addSimpleFolder(templates, name, token, parms); -} - -//////////////////////////////////////// - - struct CompilerCache { ax::Compiler::Ptr mCompiler = nullptr; @@ -274,6 +181,7 @@ class SOP_OpenVDB_AX: public hvdb::SOP_NodeVDB }; protected: + void resolveObsoleteParms(PRM_ParmList*) override; bool updateParmsFlags() override; void syncNodeVersion(const char*, const char*, bool*) override; }; // class SOP_OpenVDB_AX @@ -290,73 +198,72 @@ newSopOperator(OP_OperatorTable* table) hutil::ParmList parms; - hutil::ParmList execution; - - execution.add(hutil::ParmFactory(PRM_STRING, "vdbgroup", "Group") + parms.add(hutil::ParmFactory(PRM_STRING, "vdbgroup", "Group") .setHelpText("Specify a subset of the input VDB grids to be processed.") .setChoiceList(&hutil::PrimGroupMenu)); - execution.add(hutil::ParmFactory(PRM_STRING, "pointsgroup", "VDB Points Group") - .setHelpText("Specify a point group name to perform the execution on. If no name is " - "given, the AX snippet is applied to all points.")); - { const char* items[] = { - "active", "Active", - "inactive", "Inactive", - "all", "All", + "points", "Points", + "volumes", "Volumes", nullptr }; - execution.add(hutil::ParmFactory(PRM_ORD, "activity", "Voxel Activity") - .setDefault("active") - .setHelpText("Whether to run this snippet over Active, Inactive or All voxels.") + parms.add(hutil::ParmFactory(PRM_ORD, "runover", "Run Over") + .setDefault("points") + .setHelpText("Whether to run this snippet over OpenVDB Points or OpenVDB Volumes.") .setChoiceListItems(PRM_CHOICELIST_SINGLE, items)); } - execution.add(hutil::ParmFactory(PRM_TOGGLE, "createmissing", "Create Missing Attributes/Grids") - .setDefault(PRMoneDefaults) - .setHelpText("Create missing point attributes or VDB volumes accessed by @.")); - - addSimpleFolder(&parms, "Execution", execution); - - hutil::ParmList code; { const char* items[] = { - "points", "Points", - "volumes", "Volumes", + "active", "Active Voxels", + "inactive", "Inactive Voxels", + "all", "All Voxels", nullptr }; - code.add(hutil::ParmFactory(PRM_ORD, "targettype", "Target Type") - .setDefault("points") - .setHelpText("Whether to run this snippet over OpenVDB Points or OpenVDB Volumes.") + parms.add(hutil::ParmFactory(PRM_ORD, "activity", "") + .setDefault("active") + .setHelpText("Whether to run this snippet over Active, Inactive or All voxels.") .setChoiceListItems(PRM_CHOICELIST_SINGLE, items)); } - code.add(hutil::ParmFactory(PRM_STRING, "snippet", "AX Expression") + parms.add(hutil::ParmFactory(PRM_STRING, "vdbpointsgroup", "VDB Points Group") + .setHelpText("Specify a point group name to perform the execution on. If no name is " + "given, the AX snippet is applied to all points.") + .setChoiceList(&hvdb::VDBPointsGroupMenuInput1)); + + parms.beginSwitcher("tabMenu1"); + parms.addFolder("Code"); + + static PRM_SpareData theEditor(PRM_SpareArgs() + << PRM_SpareToken(PRM_SpareData::getEditorToken(), "1") + << PRM_SpareToken(PRM_SpareData::getEditorLanguageToken(), "ax") + << PRM_SpareToken(PRM_SpareData::getEditorLinesRangeToken(), "8-40") + ); + + parms.add(hutil::ParmFactory(PRM_STRING, "snippet", "AX Expression") .setHelpText("A snippet of AX code that will manipulate the attributes on the VDB Points or " "the VDB voxel values.") - .setSpareData(&PRM_SpareData::stringEditor)); - - addSimpleFolder(&parms, "Code", code); + .setSpareData(&theEditor)); // language/script modifiers - hutil::ParmList scriptModifiers; + parms.addFolder("Options"); - scriptModifiers.add(hutil::ParmFactory(PRM_TOGGLE, "allowvex", "Allow VEX") + parms.add(hutil::ParmFactory(PRM_TOGGLE, "allowvex", "Allow VEX") .setDefault(PRMoneDefaults) .setHelpText("Whether to enable support for various VEX functionality. When disabled, only AX " "syntax is supported.")); - scriptModifiers.add(hutil::ParmFactory(PRM_TOGGLE, "hscriptvars", "Allow HScript Variables") + parms.add(hutil::ParmFactory(PRM_TOGGLE, "hscriptvars", "Allow HScript Variables") .setDefault(PRMoneDefaults) .setHelpText("Whether to enable support for various $ variables available in the current node's " "context. As $ is used for custom parameters in AX, a warning will be generated " "if a Houdini parameter also exists of the same name as the given $ variable.")); - scriptModifiers.add(hutil::ParmFactory(PRM_STRING, "cwdpath", "Evaluation Node Path") + parms.add(hutil::ParmFactory(PRM_STRING, "cwdpath", "Evaluation Node Path") .setTypeExtended(PRM_TYPE_DYNAMIC_PATH) .setDefault(".") .setHelpText("Functions like ch() and $ syntax usually evaluate with respect to this node. " @@ -365,33 +272,61 @@ newSopOperator(OP_OperatorTable* table) "Note that HScript variables (if enabled) always refer to the AX node and ignore " "the evaluation path.")); - addSimpleFolder(&parms, "Script Modifiers", scriptModifiers); + parms.endSwitcher(); - // volume modifiers - - hutil::ParmList volumeModifiers; - - volumeModifiers.add(hutil::ParmFactory(PRM_TOGGLE, "prune", "Prune") + parms.add(hutil::ParmFactory(PRM_TOGGLE, "createattributes", "Create New Attributes/Grids") .setDefault(PRMoneDefaults) - .setHelpText("Whether to prune VDBs after execution")); - - addSimpleFolder(&parms, "Volume Modifiers", volumeModifiers); + .setHelpText("Create point attributes or VDB volumes accessed by @ that do not exist on input.")); - // point modifiers - - hutil::ParmList pointModifiers; + parms.add(hutil::ParmFactory(PRM_TOGGLE, "prune", "") + .setDefault(PRMoneDefaults) + .setTypeExtended(PRM_TYPE_TOGGLE_JOIN) + .setTooltip("Collapse regions of constant value in output grids. " + "Voxel values are considered equal if they differ " + "by less than the specified threshold.") + .setDocumentation(nullptr)); - pointModifiers.add(hutil::ParmFactory(PRM_TOGGLE, "compact", "Compact Attributes") + parms.add(hutil::ParmFactory(PRM_FLT_J, "tolerance", "Prune Tolerance") + .setDefault(PRMzeroDefaults) + .setRange(PRM_RANGE_RESTRICTED, 0, PRM_RANGE_UI, 1) + .setTooltip( + "When pruning is enabled, voxel values are considered equal" + " if they differ by less than the specified tolerance.") + .setDocumentation( + "If enabled, reduce the memory footprint of output grids that have" + " (sufficiently large) regions of voxels with the same value," + " where values are considered equal if they differ by less than" + " the specified threshold.\n\n" + "NOTE:\n" + " Pruning affects only the memory usage of a grid.\n" + " It does not remove voxels, apart from inactive voxels\n" + " whose value is equal to the background.")); + + parms.add(hutil::ParmFactory(PRM_TOGGLE, "compact", "Compact Attributes") .setDefault(PRMzeroDefaults) .setHelpText("Whether to try to compact VDB Point Attributes after execution.")); - addSimpleFolder(&parms, "Point Modifiers", pointModifiers); + // Obsolete parameters + hutil::ParmList obsoleteParms; + { + const char* items[] = { + "points", "Points", + "volumes", "Volumes", + nullptr + }; + + obsoleteParms.add(hutil::ParmFactory(PRM_ORD, "targettype", "Target Type") + .setDefault("points") + .setChoiceListItems(PRM_CHOICELIST_SINGLE, items)); + } + obsoleteParms.add(hutil::ParmFactory(PRM_STRING, "pointsgroup", "VDB Points Group")); + obsoleteParms.add(hutil::ParmFactory(PRM_TOGGLE, "createmissing", "Create Missing") + .setDefault(PRMoneDefaults)); ////////// // Register this operator. - hutil::OpFactory factory(DnegVDBOpPolicy(), "VDB AX", - SOP_OpenVDB_AX::factory, parms, *table); + hvdb::OpenVDBOpFactory factory("VDB AX", SOP_OpenVDB_AX::factory, parms, *table); factory.addInput("VDBs to manipulate"); factory.addAliasVerbatim("DW_OpenVDBAX"); @@ -817,12 +752,25 @@ SOP_OpenVDB_AX::Cache::Cache() { mCompilerCache.mCompiler = ax::Compiler::create(); mCompilerCache.mCustomData.reset(new ax::CustomData); + + auto locFromStr = [&] (const std::string& str) -> UT_SourceLocation { + // find error location at end of message + size_t locColon = str.rfind(":"); + size_t locLine = str.rfind(" ", locColon); + int line = std::atoi(str.substr(locLine + 1, locColon - locLine - 1).c_str()); + int col = std::atoi(str.substr(locColon + 1, str.size()).c_str()); + // currently only does one character, as we don't know the offending code's length + return UT_SourceLocation(nullptr, line, col, col+1); + }; + mCompilerCache.mLogger.reset(new ax::Logger( - [this](const std::string& str) { - this->addError(SOP_MESSAGE, str.c_str()); + [this, &locFromStr](const std::string& str) { + UT_SourceLocation loc = locFromStr(str); + this->cookparms()->sopAddError(SOP_MESSAGE, str.c_str(), &loc); }, - [this](const std::string& str) { - this->addWarning(SOP_MESSAGE, str.c_str()); + [this, &locFromStr](const std::string& str) { + UT_SourceLocation loc = locFromStr(str); + this->cookparms()->sopAddWarning(SOP_MESSAGE, str.c_str(), &loc); }) ); mCompilerCache.mLogger->setErrorPrefix(""); @@ -832,15 +780,39 @@ SOP_OpenVDB_AX::Cache::Cache() initializeFunctionRegistry(*mCompilerCache.mCompiler, /*allow vex*/true); } +void +SOP_OpenVDB_AX::resolveObsoleteParms(PRM_ParmList* obsoleteParms) +{ + if (!obsoleteParms) return; + + resolveRenamedParm(*obsoleteParms, "targettype", "runover"); + resolveRenamedParm(*obsoleteParms, "pointsgroup", "vdbpointsgroup"); + resolveRenamedParm(*obsoleteParms, "createmissing", "createattributes"); + + // Delegate to the base class. + hvdb::SOP_NodeVDB::resolveObsoleteParms(obsoleteParms); +} + bool SOP_OpenVDB_AX::updateParmsFlags() { bool changed = false; - const bool points = evalInt("targettype", 0, 0) == 0; - changed |= enableParm("pointsgroup", points); + const bool points = evalInt("runover", 0, 0) == 0; + changed |= enableParm("vdbpointsgroup", points); + changed |= setVisibleState("vdbpointsgroup", points); + changed |= enableParm("prune", !points); + const bool prune = static_cast(evalInt("prune", 0, 0)); + changed |= enableParm("tolerance", prune && !points ); + changed |= setVisibleState("prune", !points); + changed |= setVisibleState("tolerance", !points); + changed |= enableParm("activity", !points); + changed |= setVisibleState("activity", !points); + changed |= enableParm("compact", points); + changed |= setVisibleState("compact", points); + return changed; } @@ -858,11 +830,11 @@ void SOP_OpenVDB_AX::syncNodeVersion(const char* old_version, "0.1.0", { // We can't just return 0 as the expected behaviour here is for points to always // create attribute and for volumes to error. This preserves that behaviour. - // { "createmissing", [](const SOP_OpenVDB_AX&) -> std::string { return "0"} } - { "createmissing", + // { "createattributes", [](const SOP_OpenVDB_AX&) -> std::string { return "0"} } + { "createattributes", [](const SOP_OpenVDB_AX& node) -> std::string { - const int targetInt = static_cast(node.evalInt("targettype", 0, 0)); - if (targetInt == 0) return "1"; // points, keep default (on) + const int targetType = static_cast(node.evalInt("runover", 0, 0)); + if (targetType == 0) return "1"; // points, keep default (on) else return "0"; // volumes, turn off } } @@ -939,10 +911,14 @@ void SOP_OpenVDB_AX::syncNodeVersion(const char* old_version, struct PruneOp { + PruneOp(const fpreal tol) + : mTol(tol) {} + template void operator()(GridT& grid) const { - tools::prune(grid.tree()); + tools::prune(grid.tree(), typename GridT::TreeType::ValueType(mTol)); } + const fpreal mTol; }; OP_ERROR @@ -986,7 +962,7 @@ SOP_OpenVDB_AX::Cache::cookVDBSop(OP_Context& context) else this->evalString(snippet, "snippet", 0, time); if (snippet.length() == 0) return error(); - const int targetInt = static_cast(evalInt("targettype", 0, time)); + const int targetType = static_cast(evalInt("runover", 0, time)); // get the node which is set as the current evaluation path. If we can't find the // node, all channel links are zero valued. This matches VEX behaviour. @@ -1001,7 +977,7 @@ SOP_OpenVDB_AX::Cache::cookVDBSop(OP_Context& context) } ParameterCache parmCache; - parmCache.mTargetType = static_cast(targetInt); + parmCache.mTargetType = static_cast(targetType); parmCache.mVEXSupport = evalInt("allowvex", 0, time); parmCache.mHScriptSupport = evalInt("hscriptvars", 0, time); @@ -1147,12 +1123,12 @@ SOP_OpenVDB_AX::Cache::cookVDBSop(OP_Context& context) snippet.clear(); - const bool createMissing = static_cast(evalInt("createmissing", 0, time)); + const bool createMissing = static_cast(evalInt("createattributes", 0, time)); if (mParameterCache.mTargetType == hax::TargetType::POINTS) { UT_String pointsStr; - evalString(pointsStr, "pointsgroup", 0, time); + evalString(pointsStr, "vdbpointsgroup", 0, time); const std::string pointsGroup = pointsStr.toStdString(); for (; vdbIt; ++vdbIt) { @@ -1232,7 +1208,8 @@ SOP_OpenVDB_AX::Cache::cookVDBSop(OP_Context& context) mCompilerCache.mVolumeExecutable->execute(grids); if (evalInt("prune", 0, time)) { - PruneOp op; + const fpreal tol = evalFloat("tolerance", 0, time); + PruneOp op(tol); for (auto& vdbPrim : guPrims) { GEOvdbProcessTypedGridTopology(*vdbPrim, op, /*make_unique*/false); }