1 // Copyright Contributors to the OpenVDB Project
2 // SPDX-License-Identifier: MPL-2.0
3 
4 /// @file SOP_OpenVDB_Clip.cc
5 ///
6 /// @author FX R&D OpenVDB team
7 ///
8 /// @brief Clip grids
9 
10 #include <houdini_utils/ParmFactory.h>
11 #include <openvdb_houdini/GeometryUtil.h> // for drawFrustum(), frustumTransformFromCamera()
12 #include <openvdb_houdini/Utils.h>
13 #include <openvdb_houdini/SOP_NodeVDB.h>
14 #include <openvdb/tools/Clip.h> // for tools::clip()
15 #include <openvdb/tools/LevelSetUtil.h> // for tools::sdfInteriorMask()
16 #include <openvdb/tools/Mask.h> // for tools::interiorMask()
17 #include <openvdb/tools/Morphology.h> // for tools::dilateActiveValues(), tools::erodeActiveValues()
18 #include <openvdb/points/PointDataGrid.h>
19 #include <OBJ/OBJ_Camera.h>
20 #include <cmath> // for std::abs(), std::round()
21 #include <exception>
22 #include <string>
23 
24 
25 
26 namespace hvdb = openvdb_houdini;
27 namespace hutil = houdini_utils;
28 
29 
30 class SOP_OpenVDB_Clip: public hvdb::SOP_NodeVDB
31 {
32 public:
33     SOP_OpenVDB_Clip(OP_Network*, const char* name, OP_Operator*);
~SOP_OpenVDB_Clip()34     ~SOP_OpenVDB_Clip() override {}
35 
36     static OP_Node* factory(OP_Network*, const char* name, OP_Operator*);
37 
isRefInput(unsigned input) const38     int isRefInput(unsigned input) const override { return (input == 1); }
39 
40     class Cache: public SOP_VDBCacheOptions
41     {
42     public:
frustum() const43         openvdb::math::Transform::Ptr frustum() const { return mFrustum; }
44     protected:
45         OP_ERROR cookVDBSop(OP_Context&) override;
46     private:
47         void getFrustum(OP_Context&);
48 
49         openvdb::math::Transform::Ptr mFrustum;
50     }; // class Cache
51 
52 protected:
53     void resolveObsoleteParms(PRM_ParmList*) override;
54     bool updateParmsFlags() override;
55     OP_ERROR cookMyGuide1(OP_Context&) override;
56 };
57 
58 
59 ////////////////////////////////////////
60 
61 
62 void
newSopOperator(OP_OperatorTable * table)63 newSopOperator(OP_OperatorTable* table)
64 {
65     if (table == nullptr) return;
66 
67     hutil::ParmList parms;
68 
69     parms.add(hutil::ParmFactory(PRM_STRING, "group", "Group")
70         .setChoiceList(&hutil::PrimGroupMenuInput1)
71         .setTooltip("Specify a subset of VDBs from the first input to be clipped.")
72         .setDocumentation(
73             "A subset of VDBs from the first input to be clipped"
74             " (see [specifying volumes|/model/volumes#group])"));
75 
76     parms.add(hutil::ParmFactory(PRM_TOGGLE, "inside", "Keep Inside")
77         .setDefault(PRMoneDefaults)
78         .setTooltip(
79             "If enabled, keep voxels that lie inside the clipping region.\n"
80             "If disabled, keep voxels that lie outside the clipping region.")
81         .setDocumentation(
82             "If enabled, keep voxels that lie inside the clipping region,"
83             " otherwise keep voxels that lie outside the clipping region."));
84 
85     parms.add(hutil::ParmFactory(PRM_STRING, "clipper", "Clip To")
86         .setChoiceListItems(PRM_CHOICELIST_SINGLE, {
87             "camera",   "Camera",
88             "geometry", "Geometry",
89             "mask",     "Mask VDB"
90         })
91         .setDefault("geometry")
92         .setTooltip("Specify how the clipping region should be defined.")
93         .setDocumentation("\
94 How to define the clipping region\n\
95 \n\
96 Camera:\n\
97     Use a camera frustum as the clipping region.\n\
98 Geometry:\n\
99     Use the bounding box of geometry from the second input as the clipping region.\n\
100 Mask VDB:\n\
101     Use the active voxels of a VDB volume from the second input as a clipping mask.\n"));
102 
103     parms.add(hutil::ParmFactory(PRM_STRING, "mask", "Mask VDB")
104         .setChoiceList(&hutil::PrimGroupMenuInput2)
105         .setTooltip("Specify a VDB whose active voxels are to be used as a clipping mask.")
106         .setDocumentation(
107             "A VDB from the second input whose active voxels are to be used as a clipping mask"
108             " (see [specifying volumes|/model/volumes#group])"));
109 
110     parms.add(hutil::ParmFactory(PRM_STRING, "camera", "Camera")
111         .setTypeExtended(PRM_TYPE_DYNAMIC_PATH)
112         .setSpareData(&PRM_SpareData::objCameraPath)
113         .setTooltip("Specify the path to a reference camera")
114         .setDocumentation(
115             "The path to the camera whose frustum is to be used as a clipping region"
116             " (e.g., `/obj/cam1`)"));
117 
118     parms.add(hutil::ParmFactory(PRM_TOGGLE, "setnear", "")
119         .setDefault(PRMzeroDefaults)
120         .setTypeExtended(PRM_TYPE_TOGGLE_JOIN)
121         .setTooltip("If enabled, override the camera's near clipping plane."));
122 
123     parms.add(hutil::ParmFactory(PRM_FLT_E, "near", "Near Clipping")
124         .setDefault(0.001)
125         .setTooltip("The position of the near clipping plane")
126         .setDocumentation(
127             "The position of the near clipping plane\n\n"
128             "If enabled, this setting overrides the camera's clipping plane."));
129 
130     parms.add(hutil::ParmFactory(PRM_TOGGLE, "setfar", "")
131         .setDefault(PRMzeroDefaults)
132         .setTypeExtended(PRM_TYPE_TOGGLE_JOIN)
133         .setTooltip("If enabled, override the camera's far clipping plane."));
134 
135     parms.add(hutil::ParmFactory(PRM_FLT_E, "far", "Far Clipping")
136         .setDefault(10000)
137         .setTooltip("The position of the far clipping plane")
138         .setDocumentation(
139             "The position of the far clipping plane\n\n"
140             "If enabled, this setting overrides the camera's clipping plane."));
141 
142     parms.add(hutil::ParmFactory(PRM_TOGGLE, "setpadding", "")
143         .setDefault(PRMzeroDefaults)
144         .setTypeExtended(PRM_TYPE_TOGGLE_JOIN)
145         .setTooltip("If enabled, expand or shrink the clipping region."));
146 
147     parms.add(hutil::ParmFactory(PRM_FLT_E, "padding", "Padding")
148         .setVectorSize(3)
149         .setDefault(PRMzeroDefaults)
150         .setTooltip("Padding in world units to be added to the clipping region")
151         .setDocumentation(
152             "Padding in world units to be added to the clipping region\n\n"
153             "Negative values shrink the clipping region.\n\n"
154             "Nonuniform padding is not supported when clipping to a VDB volume.\n"
155             "The mask volume will be dilated or eroded uniformly"
156             " by the _x_-axis padding value."));
157 
158 
159     // Obsolete parameters
160     hutil::ParmList obsoleteParms;
161     obsoleteParms.add(hutil::ParmFactory(PRM_TOGGLE, "usemask", "").setDefault(PRMzeroDefaults));
162 
163 
164     hvdb::OpenVDBOpFactory("VDB Clip", SOP_OpenVDB_Clip::factory, parms, *table)
165         .addInput("VDBs")
166         .addOptionalInput("Mask VDB or bounding geometry")
167         .setObsoleteParms(obsoleteParms)
168         .setVerb(SOP_NodeVerb::COOK_INPLACE, []() { return new SOP_OpenVDB_Clip::Cache; })
169         .setDocumentation("\
170 #icon: COMMON/openvdb\n\
171 #tags: vdb\n\
172 \n\
173 \"\"\"Clip VDB volumes using a camera frustum, a bounding box, or another VDB as a mask.\"\"\"\n\
174 \n\
175 @overview\n\
176 \n\
177 This node clips VDB volumes, that is, it removes voxels that lie outside\n\
178 (or, optionally, inside) a given region by deactivating them and setting them\n\
179 to the background value.\n\
180 The clipping region may be one of the following:\n\
181 * the frustum of a camera\n\
182 * the bounding box of reference geometry\n\
183 * the active voxels of another VDB.\n\
184 \n\
185 When the clipping region is defined by a VDB, the operation\n\
186 is similar to [activity intersection|Node:sop/DW_OpenVDBCombine],\n\
187 except that clipped voxels are not only deactivated but also set\n\
188 to the background value.\n\
189 \n\
190 @related\n\
191 \n\
192 - [OpenVDB Combine|Node:sop/DW_OpenVDBCombine]\n\
193 - [OpenVDB Occlusion Mask|Node:sop/DW_OpenVDBOcclusionMask]\n\
194 - [Node:sop/vdbactivate]\n\
195 \n\
196 @examples\n\
197 \n\
198 See [openvdb.org|http://www.openvdb.org/download/] for source code\n\
199 and usage examples.\n");
200 }
201 
202 
203 void
resolveObsoleteParms(PRM_ParmList * obsoleteParms)204 SOP_OpenVDB_Clip::resolveObsoleteParms(PRM_ParmList* obsoleteParms)
205 {
206     if (!obsoleteParms) return;
207 
208     auto* parm = obsoleteParms->getParmPtr("usemask");
209     if (parm && !parm->isFactoryDefault()) { // factory default was Off
210         setString("clipper", CH_STRING_LITERAL, "mask", 0, 0.0);
211     }
212 
213     // Delegate to the base class.
214     hvdb::SOP_NodeVDB::resolveObsoleteParms(obsoleteParms);
215 }
216 
217 
218 bool
updateParmsFlags()219 SOP_OpenVDB_Clip::updateParmsFlags()
220 {
221     bool changed = false;
222 
223     UT_String clipper;
224     evalString(clipper, "clipper", 0, 0.0);
225 
226     const bool clipToCamera = (clipper == "camera");
227 
228     changed |= enableParm("mask", clipper == "mask");
229     changed |= enableParm("camera", clipToCamera);
230     changed |= enableParm("setnear", clipToCamera);
231     changed |= enableParm("near", clipToCamera && evalInt("setnear", 0, 0.0));
232     changed |= enableParm("setfar", clipToCamera);
233     changed |= enableParm("far", clipToCamera && evalInt("setfar", 0, 0.0));
234     changed |= enableParm("padding", 0 != evalInt("setpadding", 0, 0.0));
235 
236     changed |= setVisibleState("mask", clipper == "mask");
237     changed |= setVisibleState("camera", clipToCamera);
238     changed |= setVisibleState("setnear", clipToCamera);
239     changed |= setVisibleState("near", clipToCamera);
240     changed |= setVisibleState("setfar", clipToCamera);
241     changed |= setVisibleState("far", clipToCamera);
242 
243     return changed;
244 }
245 
246 
247 ////////////////////////////////////////
248 
249 
250 OP_Node*
factory(OP_Network * net,const char * name,OP_Operator * op)251 SOP_OpenVDB_Clip::factory(OP_Network* net,
252     const char* name, OP_Operator* op)
253 {
254     return new SOP_OpenVDB_Clip(net, name, op);
255 }
256 
257 
SOP_OpenVDB_Clip(OP_Network * net,const char * name,OP_Operator * op)258 SOP_OpenVDB_Clip::SOP_OpenVDB_Clip(OP_Network* net,
259     const char* name, OP_Operator* op):
260     hvdb::SOP_NodeVDB(net, name, op)
261 {
262 }
263 
264 
265 ////////////////////////////////////////
266 
267 
268 namespace {
269 
270 // Functor to convert a mask grid of arbitrary type to a BoolGrid
271 // and to dilate or erode it
272 struct DilatedMaskOp
273 {
DilatedMaskOp__anone226ebb30211::DilatedMaskOp274     DilatedMaskOp(int dilation_): dilation{dilation_} {}
275 
276     template<typename GridType>
operator ()__anone226ebb30211::DilatedMaskOp277     void operator()(const GridType& grid)
278     {
279         if (dilation == 0) return;
280 
281         maskGrid = openvdb::BoolGrid::create();
282         maskGrid->setTransform(grid.transform().copy());
283         maskGrid->topologyUnion(grid);
284 
285         UT_AutoInterrupt progress{
286             ((dilation > 0 ? "Dilating" : "Eroding") + std::string{" VDB mask"}).c_str()};
287 
288         int numIterations = std::abs(dilation);
289 
290         const int kNumIterationsPerPass = 4;
291         const int numPasses = numIterations / kNumIterationsPerPass;
292 
293         auto morphologyOp = [&](int iterations) {
294             if (dilation > 0) {
295                 openvdb::tools::dilateActiveValues(maskGrid->tree(), iterations);
296             } else {
297                 openvdb::tools::erodeActiveValues(maskGrid->tree(), iterations);
298             }
299         };
300 
301         // Since large dilations and erosions can be expensive, apply them
302         // in multiple passes and check for interrupts.
303         for (int pass = 0; pass < numPasses; ++pass, numIterations -= kNumIterationsPerPass) {
304             const bool interrupt = progress.wasInterrupted(
305                 /*pct=*/int((100.0 * pass * kNumIterationsPerPass) / std::abs(dilation)));
306             if (interrupt) {
307                 maskGrid.reset();
308                 throw std::runtime_error{"interrupted"};
309             }
310             morphologyOp(kNumIterationsPerPass);
311         }
312         if (numIterations > 0) {
313             morphologyOp(numIterations);
314         }
315     }
316 
317     int dilation = 0; // positive = dilation, negative = erosion
318     openvdb::BoolGrid::Ptr maskGrid;
319 };
320 
321 
322 struct LevelSetMaskOp
323 {
324     template<typename GridType>
operator ()__anone226ebb30211::LevelSetMaskOp325     void operator()(const GridType& grid)
326     {
327         outputGrid = openvdb::tools::sdfInteriorMask(grid);
328     }
329 
330     hvdb::GridPtr outputGrid;
331 };
332 
333 
334 struct BBoxClipOp
335 {
BBoxClipOp__anone226ebb30211::BBoxClipOp336     BBoxClipOp(const openvdb::BBoxd& bbox_, bool inside_ = true):
337         bbox(bbox_), inside(inside_)
338     {}
339 
340     template<typename GridType>
operator ()__anone226ebb30211::BBoxClipOp341     void operator()(const GridType& grid)
342     {
343         outputGrid = openvdb::tools::clip(grid, bbox, inside);
344     }
345 
346     openvdb::BBoxd bbox;
347     hvdb::GridPtr outputGrid;
348     bool inside = true;
349 };
350 
351 
352 struct FrustumClipOp
353 {
FrustumClipOp__anone226ebb30211::FrustumClipOp354     FrustumClipOp(const openvdb::math::Transform::Ptr& frustum_, bool inside_ = true):
355         frustum(frustum_), inside(inside_)
356     {}
357 
358     template<typename GridType>
operator ()__anone226ebb30211::FrustumClipOp359     void operator()(const GridType& grid)
360     {
361         openvdb::math::NonlinearFrustumMap::ConstPtr mapPtr;
362         if (frustum) mapPtr = frustum->constMap<openvdb::math::NonlinearFrustumMap>();
363         if (mapPtr) {
364             outputGrid = openvdb::tools::clip(grid, *mapPtr, inside);
365         }
366     }
367 
368     const openvdb::math::Transform::ConstPtr frustum;
369     const bool inside = true;
370     hvdb::GridPtr outputGrid;
371 };
372 
373 
374 template<typename GridType>
375 struct MaskClipDispatchOp
376 {
MaskClipDispatchOp__anone226ebb30211::MaskClipDispatchOp377     MaskClipDispatchOp(const GridType& grid_, bool inside_ = true):
378         grid(&grid_), inside(inside_)
379     {}
380 
381     template<typename MaskGridType>
operator ()__anone226ebb30211::MaskClipDispatchOp382     void operator()(const MaskGridType& mask)
383     {
384         outputGrid.reset();
385         if (grid) outputGrid = openvdb::tools::clip(*grid, mask, inside);
386     }
387 
388     const GridType* grid;
389     hvdb::GridPtr outputGrid;
390     bool inside = true;
391 };
392 
393 
394 struct MaskClipOp
395 {
MaskClipOp__anone226ebb30211::MaskClipOp396     MaskClipOp(hvdb::GridCPtr mask_, bool inside_ = true):
397         mask(mask_), inside(inside_)
398     {}
399 
400     template<typename GridType>
operator ()__anone226ebb30211::MaskClipOp401     void operator()(const GridType& grid)
402     {
403         outputGrid.reset();
404         if (mask) {
405             // Dispatch on the mask grid type, now that the source grid type is resolved.
406             MaskClipDispatchOp<GridType> op(grid, inside);
407             if (mask->apply<hvdb::AllGridTypes>(op)) {
408                 outputGrid = op.outputGrid;
409             }
410         }
411     }
412 
413     hvdb::GridCPtr mask;
414     hvdb::GridPtr outputGrid;
415     bool inside = true;
416 };
417 
418 } // unnamed namespace
419 
420 
421 ////////////////////////////////////////
422 
423 
424 /// Get the selected camera's frustum transform.
425 void
getFrustum(OP_Context & context)426 SOP_OpenVDB_Clip::Cache::getFrustum(OP_Context& context)
427 {
428     mFrustum.reset();
429 
430     const auto time = context.getTime();
431 
432     UT_String cameraPath;
433     evalString(cameraPath, "camera", 0, time);
434     if (!cameraPath.isstring()) {
435         throw std::runtime_error{"no camera path was specified"};
436     }
437 
438     OBJ_Camera* camera = nullptr;
439     if (auto* obj = cookparms()->getCwd()->findOBJNode(cameraPath)) {
440         camera = obj->castToOBJCamera();
441     }
442     OP_Node* self = cookparms()->getCwd();
443 
444     if (!camera) {
445         throw std::runtime_error{"camera \"" + cameraPath.toStdString() + "\" was not found"};
446     }
447     self->addExtraInput(camera, OP_INTEREST_DATA);
448 
449     OBJ_CameraParms cameraParms;
450     camera->getCameraParms(cameraParms, time);
451     if (cameraParms.projection != OBJ_PROJ_PERSPECTIVE) {
452         throw std::runtime_error{cameraPath.toStdString() + " is not a perspective camera"};
453         /// @todo support ortho and other cameras?
454     }
455 
456     const bool pad = (0 != evalInt("setpadding", 0, time));
457     const auto padding = pad ? evalVec3f("padding", time) : openvdb::Vec3f{0};
458 
459     const float nearPlane = (evalInt("setnear", 0, time)
460         ? static_cast<float>(evalFloat("near", 0, time))
461         : static_cast<float>(camera->getNEAR(time))) - padding[2];
462     const float farPlane = (evalInt("setfar", 0, time)
463         ? static_cast<float>(evalFloat("far", 0, time))
464         : static_cast<float>(camera->getFAR(time))) + padding[2];
465 
466     mFrustum = hvdb::frustumTransformFromCamera(*self, context, *camera,
467         /*offset=*/0.f, nearPlane, farPlane, /*voxelDepth=*/1.f, /*voxelCountX=*/100);
468 
469     if (!mFrustum || !mFrustum->constMap<openvdb::math::NonlinearFrustumMap>()) {
470         throw std::runtime_error{
471             "failed to compute frustum bounds for camera " + cameraPath.toStdString()};
472     }
473 
474     if (pad) {
475         const auto extents =
476             mFrustum->constMap<openvdb::math::NonlinearFrustumMap>()->getBBox().extents();
477         mFrustum->preScale(openvdb::Vec3d{
478             (extents[0] + 2 * padding[0]) / extents[0],
479             (extents[1] + 2 * padding[1]) / extents[1],
480             1.0});
481     }
482 }
483 
484 
485 ////////////////////////////////////////
486 
487 
488 OP_ERROR
cookMyGuide1(OP_Context &)489 SOP_OpenVDB_Clip::cookMyGuide1(OP_Context&)
490 {
491     myGuide1->clearAndDestroy();
492 
493     openvdb::math::Transform::ConstPtr frustum;
494     // Attempt to extract the frustum from our cache.
495     if (auto* cache = dynamic_cast<SOP_OpenVDB_Clip::Cache*>(myNodeVerbCache)) {
496         frustum = cache->frustum();
497     }
498 
499     if (frustum) {
500         const UT_Vector3 color{0.9f, 0.0f, 0.0f};
501         hvdb::drawFrustum(*myGuide1, *frustum, &color,
502             /*tickColor=*/nullptr, /*shaded=*/false, /*ticks=*/false);
503     }
504     return error();
505 }
506 
507 
508 OP_ERROR
cookVDBSop(OP_Context & context)509 SOP_OpenVDB_Clip::Cache::cookVDBSop(OP_Context& context)
510 {
511     try {
512         const fpreal time = context.getTime();
513 
514         UT_AutoInterrupt progress{"Clipping VDBs"};
515 
516         const GU_Detail* maskGeo = inputGeo(1);
517 
518         UT_String clipper;
519         evalString(clipper, "clipper", 0, time);
520 
521         const bool
522             useCamera = (clipper == "camera"),
523             useMask = (clipper == "mask"),
524             inside = evalInt("inside", 0, time),
525             pad = evalInt("setpadding", 0, time);
526 
527         const auto padding = pad ? evalVec3f("padding", time) : openvdb::Vec3f{0};
528 
529         mFrustum.reset();
530 
531         openvdb::BBoxd clipBox;
532         hvdb::GridCPtr maskGrid;
533 
534         if (useCamera) {
535             getFrustum(context);
536         } else if (maskGeo) {
537             if (useMask) {
538                 const GA_PrimitiveGroup* maskGroup = parsePrimitiveGroups(
539                     evalStdString("mask", time).c_str(), GroupCreator{maskGeo});
540                 hvdb::VdbPrimCIterator maskIt{maskGeo, maskGroup};
541                 if (maskIt) {
542                     if (maskIt->getConstGrid().getGridClass() == openvdb::GRID_LEVEL_SET) {
543                         // If the mask grid is a level set, extract an interior mask from it.
544                         LevelSetMaskOp op;
545                         hvdb::GEOvdbApply<hvdb::NumericGridTypes>(**maskIt, op);
546                         maskGrid = op.outputGrid;
547                     } else {
548                         maskGrid = maskIt->getConstGridPtr();
549                     }
550                 }
551                 if (!maskGrid) {
552                     addError(SOP_MESSAGE, "mask VDB not found");
553                     return error();
554                 }
555                 if (pad) {
556                     // If padding is enabled and nonzero, dilate or erode the mask grid.
557                     const auto paddingInVoxels = padding / maskGrid->voxelSize();
558                     if (!openvdb::math::isApproxEqual(paddingInVoxels[0], paddingInVoxels[1])
559                         || !openvdb::math::isApproxEqual(paddingInVoxels[1], paddingInVoxels[2]))
560                     {
561                         addWarning(SOP_MESSAGE,
562                             "nonuniform padding is not supported for mask clipping");
563                     }
564                     if (const int dilation = int(std::round(paddingInVoxels[0]))) {
565                         DilatedMaskOp op{dilation};
566                         maskGrid->apply<hvdb::AllGridTypes>(op);
567                         if (op.maskGrid) maskGrid = op.maskGrid;
568                     }
569                 }
570             } else {
571                 UT_BoundingBox box;
572                 maskGeo->getBBox(&box);
573 
574                 clipBox.min()[0] = box.xmin();
575                 clipBox.min()[1] = box.ymin();
576                 clipBox.min()[2] = box.zmin();
577                 clipBox.max()[0] = box.xmax();
578                 clipBox.max()[1] = box.ymax();
579                 clipBox.max()[2] = box.zmax();
580                 if (pad) {
581                     clipBox.min() -= padding;
582                     clipBox.max() += padding;
583                 }
584             }
585         } else {
586             addError(SOP_MESSAGE, "Not enough sources specified.");
587             return error();
588         }
589 
590         // Get the group of grids to process.
591         const GA_PrimitiveGroup* group = matchGroup(*gdp, evalStdString("group", time));
592 
593         int numLevelSets = 0;
594         for (hvdb::VdbPrimIterator it{gdp, group}; it; ++it) {
595             if (progress.wasInterrupted()) { throw std::runtime_error{"interrupted"}; }
596 
597             const auto& inGrid = it->getConstGrid();
598 
599             hvdb::GridPtr outGrid;
600 
601             if (inGrid.getGridClass() == openvdb::GRID_LEVEL_SET) {
602                 ++numLevelSets;
603             }
604 
605             progress.getInterrupt()->setAppTitle(
606                 ("Clipping VDB " + it.getPrimitiveIndexAndName().toStdString()).c_str());
607 
608             if (maskGrid) {
609                 MaskClipOp op{maskGrid, inside};
610                 if (hvdb::GEOvdbApply<hvdb::VolumeGridTypes>(**it, op)) { // all Houdini-supported volume grid types
611                     outGrid = op.outputGrid;
612                 } else if (inGrid.isType<openvdb::points::PointDataGrid>()) {
613                     addWarning(SOP_MESSAGE,
614                         "only bounding box clipping is currently supported for point data grids");
615                 }
616             } else if (useCamera) {
617                 FrustumClipOp op{mFrustum, inside};
618                 if (hvdb::GEOvdbApply<hvdb::VolumeGridTypes>(**it, op)) { // all Houdini-supported volume grid types
619                     outGrid = op.outputGrid;
620                 } else if (inGrid.isType<openvdb::points::PointDataGrid>()) {
621                     addWarning(SOP_MESSAGE,
622                         "only bounding box clipping is currently supported for point data grids");
623                 }
624             } else {
625                 BBoxClipOp op{clipBox, inside};
626                 if (hvdb::GEOvdbApply<hvdb::VolumeGridTypes>(**it, op)) { // all Houdini-supported volume grid types
627                     outGrid = op.outputGrid;
628                 } else if (inGrid.isType<openvdb::points::PointDataGrid>()) {
629                     if (inside) {
630                         outGrid = inGrid.deepCopyGrid();
631                         outGrid->clipGrid(clipBox);
632                     } else {
633                         addWarning(SOP_MESSAGE,
634                             "only Keep Inside mode is currently supported for point data grids");
635                     }
636                 }
637             }
638 
639             // Replace the original VDB primitive with a new primitive that contains
640             // the output grid and has the same attributes and group membership.
641             hvdb::replaceVdbPrimitive(*gdp, outGrid, **it, true);
642         }
643 
644         if (numLevelSets > 0) {
645             if (numLevelSets == 1) {
646                 addWarning(SOP_MESSAGE, "a level set grid was clipped;"
647                     " the resulting grid might not be a valid level set");
648             } else {
649                 addWarning(SOP_MESSAGE, "some level sets were clipped;"
650                     " the resulting grids might not be valid level sets");
651             }
652         }
653 
654     } catch (std::exception& e) {
655         addError(SOP_MESSAGE, e.what());
656     }
657 
658     return error();
659 }
660