From b19d345ab6a7d2942ce6f8ea13bc2142311e1e88 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Tue, 23 Jun 2026 13:59:57 -0700 Subject: [PATCH 1/7] Avoid separate primary and secondary VDB blend segments: building it in a single pass. Signed-off-by: Andre Pradhana --- openvdb/openvdb/tools/Blend.h | 395 +++++++++++++++++++++++++++++++++- 1 file changed, 384 insertions(+), 11 deletions(-) diff --git a/openvdb/openvdb/tools/Blend.h b/openvdb/openvdb/tools/Blend.h index 167996cab0..613e83ef5e 100644 --- a/openvdb/openvdb/tools/Blend.h +++ b/openvdb/openvdb/tools/Blend.h @@ -115,6 +115,7 @@ struct UnionWithFillet { struct BuildPrimarySegment; struct BuildSecondarySegment; + struct BuildFusedSegment; TreePtrType mSegment; GridT const * const mLhsGrid; @@ -632,6 +633,385 @@ struct UnionWithFillet::BuildSecondarySegment { FilletParms mParms; }; +template +struct UnionWithFillet::BuildFusedSegment { + using MaskTreeType = typename MaskT::TreeType; + using ValueType = typename TreeT::ValueType; + using TreePtrType = typename TreeT::Ptr; + using LeafNodeType = typename TreeT::LeafNodeType; + using NodeMaskType = typename LeafNodeType::NodeMaskType; + using RootNodeType = typename TreeT::RootNodeType; + using NodeChainType = typename RootNodeType::NodeChainType; + using InternalNodeType = typename NodeChainType::template Get<1>; + using MaskLeafNodeType = typename MaskT::TreeType::LeafNodeType; + using MaskValueType = typename MaskT::ValueType; + + BuildFusedSegment(TreeT const * const lhs, + TreeT const * const rhs, + typename MaskT::TreeType::ConstPtr mask, + FilletParms parms) + : mSegment(new TreeT(lhs->background())) + , mLhsTree(lhs) + , mRhsTree(rhs) + , mMaskTree(mask) + , mParms(parms) + { + } + + void operator()() const + { + // Gather both lhs and rhs internal-node lists up front. One combined + // internal-node reduce walks both sides. Internal nodes that do not + // overlap the opposite tree and are outside the opposite SDF are copied + // directly into the output tree. Overlapping internal nodes are expanded + // into two leaf work queues: primaryLeafNodes contains lhs leaves that + // need overlap/primary handling, and secondaryLeafNodes contains rhs + // leaves that need rhs-only handling. + // + // One combined leaf reduce processes both queues into a single output + // tree. There is no separate primary tree, secondary tree, or final + // primary/secondary merge. + std::vector lhsInternalNodes; + std::vector rhsInternalNodes; + std::vector primaryLeafNodes; + std::vector secondaryLeafNodes; + + mLhsTree->getNodes(lhsInternalNodes); + mRhsTree->getNodes(rhsInternalNodes); + + ProcessInternalNodes internalOp(lhsInternalNodes, rhsInternalNodes, + *mLhsTree, *mRhsTree, *mSegment, primaryLeafNodes, secondaryLeafNodes, mParms); + tbb::parallel_reduce( + tbb::blocked_range(0, lhsInternalNodes.size() + rhsInternalNodes.size()), + internalOp); + + if (mMaskTree) { + ProcessLeafNodesMask leafOp(primaryLeafNodes, secondaryLeafNodes, + *mLhsTree, *mRhsTree, mMaskTree, *mSegment, mParms); + tbb::parallel_reduce( + tbb::blocked_range(0, primaryLeafNodes.size() + secondaryLeafNodes.size()), + leafOp); + } else { + ProcessLeafNodes leafOp(primaryLeafNodes, secondaryLeafNodes, + *mLhsTree, *mRhsTree, *mSegment, mParms); + tbb::parallel_reduce( + tbb::blocked_range(0, primaryLeafNodes.size() + secondaryLeafNodes.size()), + leafOp); + } + } + + TreePtrType& segment() { return mSegment; } + +private: + struct ProcessInternalNodes { + ProcessInternalNodes( + std::vector& lhsNodes, + std::vector& rhsNodes, + const TreeT& lhsTree, + const TreeT& rhsTree, + TreeT& outputTree, + std::vector& primaryLeafNodes, + std::vector& secondaryLeafNodes, + FilletParms parms) + : mLhsNodes(lhsNodes) + , mRhsNodes(rhsNodes) + , mLhsTree(&lhsTree) + , mRhsTree(&rhsTree) + , mLocalTree(lhsTree.background()) + , mOutputTree(&outputTree) + , mPrimaryLocalLeafNodes() + , mSecondaryLocalLeafNodes() + , mPrimaryLeafNodes(&primaryLeafNodes) + , mSecondaryLeafNodes(&secondaryLeafNodes) + , mParms(parms) + { + } + + ProcessInternalNodes(ProcessInternalNodes& other, tbb::split) + : mLhsNodes(other.mLhsNodes) + , mRhsNodes(other.mRhsNodes) + , mLhsTree(other.mLhsTree) + , mRhsTree(other.mRhsTree) + , mLocalTree(mLhsTree->background()) + , mOutputTree(&mLocalTree) + , mPrimaryLocalLeafNodes() + , mSecondaryLocalLeafNodes() + , mPrimaryLeafNodes(&mPrimaryLocalLeafNodes) + , mSecondaryLeafNodes(&mSecondaryLocalLeafNodes) + , mParms(other.mParms) + { + } + + void join(ProcessInternalNodes& other) + { + mOutputTree->merge(*other.mOutputTree); + mPrimaryLeafNodes->insert(mPrimaryLeafNodes->end(), + other.mPrimaryLeafNodes->begin(), other.mPrimaryLeafNodes->end()); + mSecondaryLeafNodes->insert(mSecondaryLeafNodes->end(), + other.mSecondaryLeafNodes->begin(), other.mSecondaryLeafNodes->end()); + } + + void operator()(const tbb::blocked_range& range) + { + openvdb::tree::ValueAccessor lhsAcc(*mLhsTree); + openvdb::tree::ValueAccessor rhsAcc(*mRhsTree); + openvdb::tree::ValueAccessor outputAcc(*mOutputTree); + + std::vector tmpLeafNodes; + const size_t lhsSize = mLhsNodes.size(); + + for (size_t n = range.begin(), N = range.end(); n < N; ++n) { + if (n < lhsSize) { + const InternalNodeType& lhsNode = *mLhsNodes[n]; + const openvdb::math::Coord& ijk = lhsNode.origin(); + const InternalNodeType* rhsNode = + rhsAcc.template probeConstNode(ijk); + + if (rhsNode) { + lhsNode.getNodes(*mPrimaryLeafNodes); + } else if (!(rhsAcc.getValue(ijk) < ValueType(0))) { + tmpLeafNodes.clear(); + lhsNode.getNodes(tmpLeafNodes); + for (const LeafNodeType* leaf : tmpLeafNodes) { + outputAcc.addLeaf(new LeafNodeType(*leaf)); + } + } + } else { + const InternalNodeType& rhsNode = *mRhsNodes[n - lhsSize]; + const openvdb::math::Coord& ijk = rhsNode.origin(); + const InternalNodeType* lhsNode = + lhsAcc.template probeConstNode(ijk); + + if (lhsNode) { + rhsNode.getNodes(*mSecondaryLeafNodes); + } else if (!(lhsAcc.getValue(ijk) < ValueType(0))) { + tmpLeafNodes.clear(); + rhsNode.getNodes(tmpLeafNodes); + for (const LeafNodeType* leaf : tmpLeafNodes) { + outputAcc.addLeaf(new LeafNodeType(*leaf)); + } + } + } + } + } + + const std::vector& mLhsNodes; + const std::vector& mRhsNodes; + TreeT const * const mLhsTree; + TreeT const * const mRhsTree; + TreeT mLocalTree; + TreeT * const mOutputTree; + std::vector mPrimaryLocalLeafNodes; + std::vector mSecondaryLocalLeafNodes; + std::vector * const mPrimaryLeafNodes; + std::vector * const mSecondaryLeafNodes; + FilletParms mParms; + }; + + struct ProcessLeafNodes { + ProcessLeafNodes( + std::vector& primaryLeafNodes, + std::vector& secondaryLeafNodes, + const TreeT& lhsTree, + const TreeT& rhsTree, + TreeT& outputTree, + FilletParms parms) + : mPrimaryLeafNodes(primaryLeafNodes) + , mSecondaryLeafNodes(secondaryLeafNodes) + , mLhsTree(&lhsTree) + , mRhsTree(&rhsTree) + , mLocalTree(lhsTree.background()) + , mOutputTree(&outputTree) + , mParms(parms) + { + } + + ProcessLeafNodes(ProcessLeafNodes& other, tbb::split) + : mPrimaryLeafNodes(other.mPrimaryLeafNodes) + , mSecondaryLeafNodes(other.mSecondaryLeafNodes) + , mLhsTree(other.mLhsTree) + , mRhsTree(other.mRhsTree) + , mLocalTree(mLhsTree->background()) + , mOutputTree(&mLocalTree) + , mParms(other.mParms) + { + } + + void join(ProcessLeafNodes& rhs) { mOutputTree->merge(*rhs.mOutputTree); } + + void operator()(const tbb::blocked_range& range) + { + openvdb::tree::ValueAccessor lhsAcc(*mLhsTree); + openvdb::tree::ValueAccessor rhsAcc(*mRhsTree); + openvdb::tree::ValueAccessor outputAcc(*mOutputTree); + + const float alpha = mParms.mAlpha; + const float beta = mParms.mBeta; + const float gamma = mParms.mGamma; + const size_t primarySize = mPrimaryLeafNodes.size(); + + for (size_t n = range.begin(), N = range.end(); n < N; ++n) { + if (n < primarySize) { + const LeafNodeType& lhsNode = *mPrimaryLeafNodes[n]; + const openvdb::math::Coord& ijk = lhsNode.origin(); + const LeafNodeType* rhsNode = rhsAcc.probeConstLeaf(ijk); + + if (rhsNode) { + LeafNodeType* outputNode = outputAcc.touchLeaf(ijk); + ValueType* outputData = outputNode->buffer().data(); + NodeMaskType& outputMask = outputNode->getValueMask(); + + const ValueType* lhsData = lhsNode.buffer().data(); + const NodeMaskType& lhsMask = lhsNode.getValueMask(); + const ValueType* rhsData = rhsNode->buffer().data(); + const NodeMaskType& rhsMask = rhsNode->getValueMask(); + + for (openvdb::Index pos = 0; pos < LeafNodeType::SIZE; ++pos) { + const float A = lhsData[pos]; + const float B = rhsData[pos]; + const float m = openvdb::math::Clamp((alpha - A) / alpha, 0.f, 1.f) * + openvdb::math::Clamp((alpha - B) / alpha, 0.f, 1.f); + const bool hasValidFilletSamples = lhsMask.isOn(pos) && rhsMask.isOn(pos); + const float offset = hasValidFilletSamples ? + openvdb::math::Pow(m, beta) * gamma : 0.0f; + const bool isAMin = A < B; + outputData[pos] = isAMin ? A - offset : B - offset; + outputMask.set(pos, isAMin ? lhsMask.isOn(pos) : rhsMask.isOn(pos)); + } + } else if (!(rhsAcc.getValue(ijk) < ValueType(0.0))) { + outputAcc.addLeaf(new LeafNodeType(lhsNode)); + } + } else { + const LeafNodeType& rhsNode = *mSecondaryLeafNodes[n - primarySize]; + const openvdb::math::Coord& ijk = rhsNode.origin(); + const LeafNodeType* lhsNode = lhsAcc.probeConstLeaf(ijk); + + if (!lhsNode && !(lhsAcc.getValue(ijk) < ValueType(0))) { + outputAcc.addLeaf(new LeafNodeType(rhsNode)); + } + } + } + } + + const std::vector& mPrimaryLeafNodes; + const std::vector& mSecondaryLeafNodes; + TreeT const * const mLhsTree; + TreeT const * const mRhsTree; + TreeT mLocalTree; + TreeT * const mOutputTree; + FilletParms mParms; + }; + + struct ProcessLeafNodesMask { + ProcessLeafNodesMask( + std::vector& primaryLeafNodes, + std::vector& secondaryLeafNodes, + const TreeT& lhsTree, + const TreeT& rhsTree, + typename MaskT::TreeType::ConstPtr maskTree, + TreeT& outputTree, + FilletParms parms) + : mPrimaryLeafNodes(primaryLeafNodes) + , mSecondaryLeafNodes(secondaryLeafNodes) + , mLhsTree(&lhsTree) + , mRhsTree(&rhsTree) + , mMaskTree(maskTree) + , mLocalTree(lhsTree.background()) + , mOutputTree(&outputTree) + , mParms(parms) + { + } + + ProcessLeafNodesMask(ProcessLeafNodesMask& other, tbb::split) + : mPrimaryLeafNodes(other.mPrimaryLeafNodes) + , mSecondaryLeafNodes(other.mSecondaryLeafNodes) + , mLhsTree(other.mLhsTree) + , mRhsTree(other.mRhsTree) + , mMaskTree(other.mMaskTree) + , mLocalTree(mLhsTree->background()) + , mOutputTree(&mLocalTree) + , mParms(other.mParms) + { + } + + void join(ProcessLeafNodesMask& rhs) { mOutputTree->merge(*rhs.mOutputTree); } + + void operator()(const tbb::blocked_range& range) + { + openvdb::tree::ValueAccessor lhsAcc(*mLhsTree); + openvdb::tree::ValueAccessor rhsAcc(*mRhsTree); + openvdb::tree::ValueAccessor maskAcc(*mMaskTree); + openvdb::tree::ValueAccessor outputAcc(*mOutputTree); + + const float alpha = mParms.mAlpha; + const float beta = mParms.mBeta; + const float gamma = mParms.mGamma; + const float maskBackground = mMaskTree->background(); + const size_t primarySize = mPrimaryLeafNodes.size(); + + for (size_t n = range.begin(), N = range.end(); n < N; ++n) { + if (n < primarySize) { + const LeafNodeType& lhsNode = *mPrimaryLeafNodes[n]; + const openvdb::math::Coord& ijk = lhsNode.origin(); + const LeafNodeType* rhsNode = rhsAcc.probeConstLeaf(ijk); + const MaskLeafNodeType* maskNode = maskAcc.probeConstLeaf(ijk); + + if (rhsNode) { + LeafNodeType* outputNode = outputAcc.touchLeaf(ijk); + ValueType* outputData = outputNode->buffer().data(); + NodeMaskType& outputMask = outputNode->getValueMask(); + + const ValueType* lhsData = lhsNode.buffer().data(); + const NodeMaskType& lhsMask = lhsNode.getValueMask(); + const ValueType* rhsData = rhsNode->buffer().data(); + const NodeMaskType& rhsMask = rhsNode->getValueMask(); + const MaskValueType* maskData = maskNode ? maskNode->buffer().data() : nullptr; + + for (openvdb::Index pos = 0; pos < LeafNodeType::SIZE; ++pos) { + const float A = lhsData[pos]; + const float B = rhsData[pos]; + const float m = openvdb::math::Clamp((alpha - A) / alpha, 0.f, 1.f) * + openvdb::math::Clamp((alpha - B) / alpha, 0.f, 1.f); + const bool hasValidFilletSamples = lhsMask.isOn(pos) && rhsMask.isOn(pos); + const float offset = hasValidFilletSamples ? + openvdb::math::Pow(m, beta) * gamma : 0.0f; + const bool isAMin = A < B; + const float multiplier = maskData ? maskData[pos] : maskBackground; + outputData[pos] = isAMin ? (A - offset * multiplier) : (B - offset * multiplier); + outputMask.set(pos, isAMin ? lhsMask.isOn(pos) : rhsMask.isOn(pos)); + } + } else if (!(rhsAcc.getValue(ijk) < ValueType(0.0))) { + outputAcc.addLeaf(new LeafNodeType(lhsNode)); + } + } else { + const LeafNodeType& rhsNode = *mSecondaryLeafNodes[n - primarySize]; + const openvdb::math::Coord& ijk = rhsNode.origin(); + const LeafNodeType* lhsNode = lhsAcc.probeConstLeaf(ijk); + + if (!lhsNode && !(lhsAcc.getValue(ijk) < ValueType(0))) { + outputAcc.addLeaf(new LeafNodeType(rhsNode)); + } + } + } + } + + const std::vector& mPrimaryLeafNodes; + const std::vector& mSecondaryLeafNodes; + TreeT const * const mLhsTree; + TreeT const * const mRhsTree; + typename MaskT::TreeType::ConstPtr mMaskTree; + TreeT mLocalTree; + TreeT * const mOutputTree; + FilletParms mParms; + }; + + TreePtrType mSegment; + TreeT const * const mLhsTree; + TreeT const * const mRhsTree; + typename MaskT::TreeType::ConstPtr mMaskTree; + FilletParms mParms; +}; + template typename GridT::Ptr UnionWithFillet::blend() { @@ -639,21 +1019,14 @@ typename GridT::Ptr UnionWithFillet::blend() { parms.mAlpha = mBandwidth; parms.mBeta = mExponent; parms.mGamma = mMultiplier; - BuildPrimarySegment primary(mLhsTree, mRhsTree, mMaskTree, parms); - BuildSecondarySegment secondary(mLhsTree, mRhsTree, parms); - - // Exploiting nested parallelism - tbb::task_group tasks; - tasks.run(primary); - tasks.run(secondary); - tasks.wait(); + BuildFusedSegment fused(mLhsTree, mRhsTree, mMaskTree, parms); - primary.segment()->merge(*secondary.segment()); + fused(); // The leafnode (level = 0) sign is set in the segment construction. - openvdb::tools::signedFloodFill(*primary.segment(), /*threaded=*/true, /*grainSize=*/1, /*minLevel=*/1); + openvdb::tools::signedFloodFill(*fused.segment(), /*threaded=*/true, /*grainSize=*/1, /*minLevel=*/1); - mSegment = primary.segment(); + mSegment = fused.segment(); typename GridT::Ptr ret = GridT::create(mSegment); ret->setTransform((mRhsGrid->transform()).copy()); From e17498f92409aeffc8a96d03fb4c1b85e3426eb1 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Tue, 23 Jun 2026 14:17:19 -0700 Subject: [PATCH 2/7] Use better naming for VDB Fillet SOP. Signed-off-by: Andre Pradhana --- .../openvdb_houdini/SOP_OpenVDB_Fillet.cc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 8f524f1cb0..488cfc5ca1 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -433,29 +433,28 @@ newSopOperator(OP_OperatorTable* table) .setTooltip("Optional scalar VDB used for alpha masking\n\n" "Values are assumed to be between 0 and 1.")); - // Band radius of influence / falloff width, i.e. alpha - parms.add(hutil::ParmFactory(PRM_FLT_J, "alpha", "Band Radius") + // Blend radius of influence / falloff width, i.e. alpha + parms.add(hutil::ParmFactory(PRM_FLT_J, "alpha", "Blend Radius") .setDefault(10.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( - "Band radius of influence measures the distance from the zero\n" + "Blend radius of influence measures the distance from the zero\n" "iso-contour of the intersection that is going to be modified.\n" "This is measured in world-space.")); // Exponent, i.e. beta - parms.add(hutil::ParmFactory(PRM_FLT_J, "beta", "Exponent") + parms.add(hutil::ParmFactory(PRM_FLT_J, "beta", "Falloff Sharpness") .setDefault(100.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( - "Blending curve exponential used in the model.")); + "Controls how sharply the fillet influence falls off.")); // Amplitude - parms.add(hutil::ParmFactory(PRM_FLT_J, "gamma", "Multiplier") + parms.add(hutil::ParmFactory(PRM_FLT_J, "gamma", "Fillet Strength") .setDefault(10.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( - "Amplitude provides a multiplier to make the blended\n" - "influence weaker or stronger.")); + "Controls the strength of the fillet offset.")); // Menu of resampling options parms.add(hutil::ParmFactory(PRM_ORD, "resample", "Resample") From a531adee3c0cc990c314664325fb79500a964697 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 15 Jul 2026 11:10:35 -0700 Subject: [PATCH 3/7] Rename alpha, beta, gamma parameters to be more meaningful. Signed-off-by: Andre Pradhana --- .../openvdb_houdini/SOP_OpenVDB_Fillet.cc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 488cfc5ca1..52704c5dc7 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -339,9 +339,9 @@ SOP_OpenVDB_Fillet::Cache::evalParms(OP_Context& context, VDBFilletParms& parms) { const fpreal time = context.getTime(); - parms.mAlpha = static_cast(evalFloat("alpha", 0, time)); - parms.mBeta = static_cast(evalFloat("beta", 0, time)); - parms.mGamma = static_cast(evalFloat("gamma", 0, time)); + parms.mAlpha = static_cast(evalFloat("blend_radius", 0, time)); + parms.mBeta = static_cast(evalFloat("falloff_sharpness", 0, time)); + parms.mGamma = static_cast(evalFloat("fillet_strength", 0, time)); parms.mResampleMode = asResampleMode(evalInt("resample", 0, time)); parms.mSamplingOrder = static_cast(evalInt("resampleinterp", 0, time)); @@ -433,8 +433,8 @@ newSopOperator(OP_OperatorTable* table) .setTooltip("Optional scalar VDB used for alpha masking\n\n" "Values are assumed to be between 0 and 1.")); - // Blend radius of influence / falloff width, i.e. alpha - parms.add(hutil::ParmFactory(PRM_FLT_J, "alpha", "Blend Radius") + // Blend radius of influence / falloff width + parms.add(hutil::ParmFactory(PRM_FLT_J, "blend_radius", "Blend Radius") .setDefault(10.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( @@ -442,15 +442,15 @@ newSopOperator(OP_OperatorTable* table) "iso-contour of the intersection that is going to be modified.\n" "This is measured in world-space.")); - // Exponent, i.e. beta - parms.add(hutil::ParmFactory(PRM_FLT_J, "beta", "Falloff Sharpness") + // Exponent + parms.add(hutil::ParmFactory(PRM_FLT_J, "falloff_sharpness", "Falloff Sharpness") .setDefault(100.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( "Controls how sharply the fillet influence falls off.")); // Amplitude - parms.add(hutil::ParmFactory(PRM_FLT_J, "gamma", "Fillet Strength") + parms.add(hutil::ParmFactory(PRM_FLT_J, "fillet_strength", "Fillet Strength") .setDefault(10.f) .setRange(PRM_RANGE_UI, 0.f, PRM_RANGE_UI, 1000.f) .setTooltip( From 7b7a340ee2b640ddb32383811975ae1c2e203bf3 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 15 Jul 2026 11:27:33 -0700 Subject: [PATCH 4/7] Disable interpolation when resampling is off, and disable mask controls unless a mask input is connected and enabled. Signed-off-by: Andre Pradhana --- .../openvdb_houdini/SOP_OpenVDB_Fillet.cc | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 52704c5dc7..80d255cd0a 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -522,12 +522,19 @@ SOP_OpenVDB_Fillet::disableParms() { unsigned changed = 0; - // Disable parms. - //changed += enableParm("dummy", - // evalInt("dummy", 0, /*time=*/0)); - - - //setVisibleState("dummy", getEnableState("dummy")); + // Interpolation only affects grids that the SOP resamples. When + // resampling is disabled, keep this control inactive to avoid implying + // that it changes the fillet result. + changed += enableParm("resampleinterp", evalInt("resample", 0, /*time=*/0) != RESAMPLE_OFF); + + // The mask controls are meaningful only when the optional third input is + // wired and masking is enabled. This mirrors the cook-time requirement + // that alpha masks come from input 3. + const bool hasMask = (this->nInputs() == 3); + const bool useMask = hasMask && bool(evalInt("mask", 0, /*time=*/0)); + + changed += enableParm("mask", hasMask); + changed += enableParm("maskname", useMask); return changed; } From d389551d99d6ffe469b7210cfab2f4535ac225a7 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 15 Jul 2026 11:31:09 -0700 Subject: [PATCH 5/7] Add backward compatibility for the renamed parameters. Signed-off-by: Andre Pradhana --- .../openvdb_houdini/SOP_OpenVDB_Fillet.cc | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 80d255cd0a..290a35ae55 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -328,6 +328,7 @@ class SOP_OpenVDB_Fillet: public hvdb::SOP_NodeVDB protected: unsigned disableParms() override; + void resolveObsoleteParms(PRM_ParmList*) override; }; @@ -484,6 +485,14 @@ newSopOperator(OP_OperatorTable* table) " sampling artifacts. Quadratic interpolation is slow but high-quality." " Linear interpolation is intermediate in speed and quality.")); + // Obsolete parameters + hutil::ParmList obsoleteParms; + obsoleteParms.add(hutil::ParmFactory(PRM_FLT_J, "alpha", "Blend Radius") + .setDefault(10.f)); + obsoleteParms.add(hutil::ParmFactory(PRM_FLT_J, "beta", "Falloff Sharpness") + .setDefault(100.f)); + obsoleteParms.add(hutil::ParmFactory(PRM_FLT_J, "gamma", "Fillet Strength") + .setDefault(10.f)); // Register this operator. // (See houdini_utils/Utils.h for OpFactory details.) @@ -491,6 +500,7 @@ newSopOperator(OP_OperatorTable* table) .addInput("A VDBs") .addOptionalInput("B VDBs") .addOptionalInput("Mask VDB") + .setObsoleteParms(obsoleteParms) .setVerb(SOP_NodeVerb::COOK_INPLACE, []() { return new SOP_OpenVDB_Fillet::Cache; }); } @@ -516,6 +526,24 @@ SOP_OpenVDB_Fillet::SOP_OpenVDB_Fillet(OP_Network* net, //////////////////////////////////////// +void +SOP_OpenVDB_Fillet::resolveObsoleteParms(PRM_ParmList* obsoleteParms) +{ + if (!obsoleteParms) return; + + // Preserve values from scenes and presets saved before the controls were + // renamed from algorithm shorthand to user-facing parameter names. + resolveRenamedParm(*obsoleteParms, "alpha", "blend_radius"); + resolveRenamedParm(*obsoleteParms, "beta", "falloff_sharpness"); + resolveRenamedParm(*obsoleteParms, "gamma", "fillet_strength"); + + hvdb::SOP_NodeVDB::resolveObsoleteParms(obsoleteParms); +} + + +//////////////////////////////////////// + + // Enable/disable or show/hide parameters in the UI. unsigned SOP_OpenVDB_Fillet::disableParms() From 05bcceafff107235e9d8844cddebfeb7c588a828 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 15 Jul 2026 11:40:28 -0700 Subject: [PATCH 6/7] Fix VDB Fillet mask resampling, i.e. not using level-set rebuild when resampling mask. Signed-off-by: Andre Pradhana --- .../openvdb_houdini/SOP_OpenVDB_Fillet.cc | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 290a35ae55..1c61574ed8 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -215,8 +215,19 @@ struct ResampleOp { const openvdb::math::Transform& maskXform = mask->constTransform(); const openvdb::math::Transform& aGrdXform = mAGrid->constTransform(); if (maskXform != aGrdXform) { - typename GridT::ConstPtr maskRsmpl = this->resampleToMatch(*mask /* src */, *mAGrid /* ref */, mParms.mSamplingOrder); - mask = maskRsmpl; + // Alpha masks are scalar weights, not signed distance fields. Even + // if the input grid class is set to level set, preserve its values + // with sampler-based resampling rather than rebuilding it as an SDF. + typename GridT::Ptr resampledMask = mask->copyWithNewTree(); + resampledMask->setTransform(aGrdXform.copy()); + using namespace openvdb; + switch (mParms.mSamplingOrder) { + case 0: tools::resampleToMatch(*mask, *resampledMask); break; + case 1: tools::resampleToMatch(*mask, *resampledMask); break; + case 2: tools::resampleToMatch(*mask, *resampledMask); break; + // note: no default case because sampling order is guaranteed to be 0, 1, or 2 in evalParms. + } + mask = resampledMask; } } From 6208f2aeb133996122cfb6d925c3660f31700b87 Mon Sep 17 00:00:00 2001 From: Andre Pradhana Date: Wed, 15 Jul 2026 11:43:01 -0700 Subject: [PATCH 7/7] Update tooltip and documentation. Signed-off-by: Andre Pradhana --- openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc index 1c61574ed8..7a86f1fae0 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fillet.cc @@ -487,11 +487,13 @@ newSopOperator(OP_OperatorTable* table) "quadratic", "Quadratic" }) .setTooltip( - "Specify the type of interpolation to be used when\n" - "resampling one VDB to match the other's transform.") + "Specify the interpolation used when resampling alpha masks\n" + "or fallback value grids. Level set A/B inputs are rebuilt\n" + "to preserve valid signed distance fields.") .setDocumentation( - "The type of interpolation to be used when resampling one VDB" - " to match the other's transform\n\n" + "The type of interpolation to be used when resampling alpha masks" + " or fallback value grids. Level set A/B inputs are rebuilt to" + " preserve valid signed distance fields.\n\n" "Nearest neighbor interpolation is fast but can introduce noticeable" " sampling artifacts. Quadratic interpolation is slow but high-quality." " Linear interpolation is intermediate in speed and quality."));