1 // Copyright Contributors to the OpenVDB Project
2 // SPDX-License-Identifier: MPL-2.0
3 //
4 /// @file SOP_OpenVDB_Occlusion_Mask.cc
5 ///
6 /// @author FX R&D OpenVDB team
7 ///
8 /// @brief Masks the occluded regions behind objects in the camera frustum
9 
10 #include <houdini_utils/ParmFactory.h>
11 #include <openvdb_houdini/Utils.h>
12 #include <openvdb_houdini/GeometryUtil.h>
13 #include <openvdb_houdini/SOP_NodeVDB.h>
14 
15 #include <openvdb/tools/LevelSetUtil.h>
16 #include <openvdb/tools/GridTransformer.h>
17 #include <openvdb/tools/Morphology.h>
18 
19 #include <OBJ/OBJ_Camera.h>
20 
21 #include <cmath> // for std::floor()
22 #include <stdexcept>
23 
24 
25 namespace hvdb = openvdb_houdini;
26 namespace hutil = houdini_utils;
27 
28 
29 class SOP_OpenVDB_Occlusion_Mask: public hvdb::SOP_NodeVDB
30 {
31 public:
SOP_OpenVDB_Occlusion_Mask(OP_Network * net,const char * name,OP_Operator * op)32     SOP_OpenVDB_Occlusion_Mask(OP_Network* net, const char* name, OP_Operator* op):
33         hvdb::SOP_NodeVDB(net, name, op) {}
34     ~SOP_OpenVDB_Occlusion_Mask() override = default;
35 
36     static OP_Node* factory(OP_Network*, const char* name, OP_Operator*);
37 
38     class Cache: public SOP_VDBCacheOptions
39     {
40     public:
frustum() const41         openvdb::math::Transform::Ptr frustum() const { return mFrustum; }
42     protected:
43         OP_ERROR cookVDBSop(OP_Context&) override;
44     private:
45         openvdb::math::Transform::Ptr mFrustum;
46     }; // class Cache
47 
48 protected:
49     void resolveObsoleteParms(PRM_ParmList*) override;
50 
51     OP_ERROR cookMyGuide1(OP_Context&) override;
52 }; // class SOP_OpenVDB_Occlusion_Mask
53 
54 
55 ////////////////////////////////////////
56 
57 
58 OP_Node*
factory(OP_Network * net,const char * name,OP_Operator * op)59 SOP_OpenVDB_Occlusion_Mask::factory(OP_Network* net,
60     const char* name, OP_Operator* op)
61 {
62     return new SOP_OpenVDB_Occlusion_Mask(net, name, op);
63 }
64 
65 
66 void
newSopOperator(OP_OperatorTable * table)67 newSopOperator(OP_OperatorTable* table)
68 {
69     if (table == nullptr) return;
70 
71     hutil::ParmList parms;
72 
73     parms.add(hutil::ParmFactory(PRM_STRING, "group", "Group")
74         .setChoiceList(&hutil::PrimGroupMenuInput1)
75         .setTooltip("Specify a subset of the input VDB grids to be processed.")
76         .setDocumentation(
77             "A subset of the input VDBs to be processed"
78             " (see [specifying volumes|/model/volumes#group])"));
79 
80     parms.add(hutil::ParmFactory(PRM_STRING, "camera", "Camera")
81         .setTypeExtended(PRM_TYPE_DYNAMIC_PATH)
82         .setSpareData(&PRM_SpareData::objCameraPath)
83         .setTooltip("Reference camera path")
84         .setDocumentation("The path to the camera (e.g., `/obj/cam1`)"));
85 
86     parms.add(hutil::ParmFactory(PRM_INT_J, "voxelcount", "Voxel Count")
87         .setDefault(PRM100Defaults)
88         .setRange(PRM_RANGE_RESTRICTED, 1, PRM_RANGE_UI, 200)
89         .setTooltip("The desired width in voxels of the camera's near plane"));
90 
91     parms.add(hutil::ParmFactory(PRM_FLT_J, "voxeldepthsize", "Voxel Depth Size")
92         .setDefault(PRMoneDefaults)
93         .setRange(PRM_RANGE_RESTRICTED, 1e-5, PRM_RANGE_UI, 5)
94         .setTooltip("The depth of a voxel in world units (all voxels have equal depth)"));
95 
96     parms.add(hutil::ParmFactory(PRM_FLT_J, "depth", "Mask Depth")
97         .setDefault(100)
98         .setRange(PRM_RANGE_RESTRICTED, 0.0, PRM_RANGE_UI, 1000.0)
99         .setTooltip(
100             "The desired depth of the mask in world units"
101             " from the near plane to the far plane"));
102 
103     parms.add(hutil::ParmFactory(PRM_INT_J, "erode", "Erode")
104         .setRange(PRM_RANGE_RESTRICTED, 0, PRM_RANGE_UI, 10)
105         .setDefault(PRMzeroDefaults)
106         .setTooltip("The number of voxels by which to shrink the mask"));
107 
108     parms.add(hutil::ParmFactory(PRM_INT_J, "zoffset", "Z Offset")
109         .setRange(PRM_RANGE_UI, -10, PRM_RANGE_UI, 10)
110         .setDefault(PRMzeroDefaults)
111         .setTooltip("The number of voxels by which to offset the near plane"));
112 
113 
114     hutil::ParmList obsoleteParms;
115     obsoleteParms.add(hutil::ParmFactory(PRM_INT_J, "voxelCount", "Voxel Count")
116         .setDefault(PRM100Defaults));
117     obsoleteParms.add(hutil::ParmFactory(PRM_FLT_J, "voxelDepthSize", "Voxel Depth Size")
118         .setDefault(PRMoneDefaults));
119 
120 
121     hvdb::OpenVDBOpFactory("VDB Occlusion Mask",
122         SOP_OpenVDB_Occlusion_Mask::factory, parms, *table)
123         .addInput("VDBs")
124         .setObsoleteParms(obsoleteParms)
125         .setVerb(SOP_NodeVerb::COOK_INPLACE, []() { return new SOP_OpenVDB_Occlusion_Mask::Cache; })
126         .setDocumentation("\
127 #icon: COMMON/openvdb\n\
128 #tags: vdb\n\
129 \n\
130 \"\"\"Identify voxels of a VDB volume that are in shadow from a given camera.\"\"\"\n\
131 \n\
132 @overview\n\
133 \n\
134 This node outputs a VDB volume whose active voxels denote the voxels\n\
135 of an input volume inside a camera frustum that would be occluded\n\
136 when viewed through the camera.\n\
137 \n\
138 @related\n\
139 - [OpenVDB Clip|Node:sop/DW_OpenVDBClip]\n\
140 - [OpenVDB Create|Node:sop/DW_OpenVDBCreate]\n\
141 \n\
142 @examples\n\
143 \n\
144 See [openvdb.org|http://www.openvdb.org/download/] for source code\n\
145 and usage examples.\n");
146 }
147 
148 
149 void
resolveObsoleteParms(PRM_ParmList * obsoleteParms)150 SOP_OpenVDB_Occlusion_Mask::resolveObsoleteParms(PRM_ParmList* obsoleteParms)
151 {
152     if (!obsoleteParms) return;
153 
154     resolveRenamedParm(*obsoleteParms, "voxelCount", "voxelcount");
155     resolveRenamedParm(*obsoleteParms, "voxelDepthSize", "voxeldepthsize");
156 
157     // Delegate to the base class.
158     hvdb::SOP_NodeVDB::resolveObsoleteParms(obsoleteParms);
159 }
160 
161 
162 ////////////////////////////////////////
163 
164 
165 OP_ERROR
cookMyGuide1(OP_Context &)166 SOP_OpenVDB_Occlusion_Mask::cookMyGuide1(OP_Context&)
167 {
168     myGuide1->clearAndDestroy();
169 
170     openvdb::math::Transform::ConstPtr frustum;
171     // Attempt to extract the frustum from our cache.
172     if (auto* cache = dynamic_cast<SOP_OpenVDB_Occlusion_Mask::Cache*>(myNodeVerbCache)) {
173         frustum = cache->frustum();
174     }
175 
176     if (frustum) {
177         UT_Vector3 color(0.9f, 0.0f, 0.0f);
178         hvdb::drawFrustum(*myGuide1, *frustum, &color, nullptr, false, false);
179     }
180     return error();
181 }
182 
183 
184 ////////////////////////////////////////
185 
186 
187 namespace {
188 
189 
190 template<typename BoolTreeT>
191 class VoxelShadow
192 {
193 public:
194     using BoolLeafManagerT = openvdb::tree::LeafManager<const BoolTreeT>;
195 
196     //////////
197 
198     VoxelShadow(const BoolLeafManagerT& leafs, int zMax, int offset);
199     void run(bool threaded = true);
200 
tree() const201     BoolTreeT& tree() const { return *mNewTree; }
202 
203     //////////
204 
205     VoxelShadow(VoxelShadow&, tbb::split);
206     void operator()(const tbb::blocked_range<size_t>&);
join(const VoxelShadow & rhs)207     void join(const VoxelShadow& rhs)
208     {
209         mNewTree->merge(*rhs.mNewTree);
210         mNewTree->prune();
211     }
212 
213 private:
214     typename BoolTreeT::Ptr mNewTree;
215     const BoolLeafManagerT* mLeafs;
216     const int mOffset, mZMax;
217 };
218 
219 template<typename BoolTreeT>
VoxelShadow(const BoolLeafManagerT & leafs,int zMax,int offset)220 VoxelShadow<BoolTreeT>::VoxelShadow(const BoolLeafManagerT& leafs, int zMax, int offset)
221     : mNewTree(new BoolTreeT(false))
222     , mLeafs(&leafs)
223     , mOffset(offset)
224     , mZMax(zMax)
225 {
226 }
227 
228 template<typename BoolTreeT>
VoxelShadow(VoxelShadow & rhs,tbb::split)229 VoxelShadow<BoolTreeT>::VoxelShadow(VoxelShadow& rhs, tbb::split)
230     : mNewTree(new BoolTreeT(false))
231     , mLeafs(rhs.mLeafs)
232     , mOffset(rhs.mOffset)
233     , mZMax(rhs.mZMax)
234 {
235 }
236 
237 template<typename BoolTreeT>
238 void
run(bool threaded)239 VoxelShadow<BoolTreeT>::run(bool threaded)
240 {
241     if (threaded) tbb::parallel_reduce(mLeafs->getRange(), *this);
242     else (*this)(mLeafs->getRange());
243 }
244 
245 template<typename BoolTreeT>
246 void
operator ()(const tbb::blocked_range<size_t> & range)247 VoxelShadow<BoolTreeT>::operator()(const tbb::blocked_range<size_t>& range)
248 {
249     typename BoolTreeT::LeafNodeType::ValueOnCIter it;
250     openvdb::CoordBBox bbox;
251 
252     bbox.max()[2] = mZMax;
253 
254     for (size_t n = range.begin(); n != range.end(); ++n) {
255 
256         for (it = mLeafs->leaf(n).cbeginValueOn(); it; ++it) {
257 
258             bbox.min() = it.getCoord();
259             bbox.min()[2] += mOffset;
260             bbox.max()[0] = bbox.min()[0];
261             bbox.max()[1] = bbox.min()[1];
262 
263             mNewTree->fill(bbox, true, true);
264         }
265 
266         mNewTree->prune();
267     }
268 }
269 
270 
271 struct BoolSampler
272 {
name__anon16a48f850211::BoolSampler273     static const char* name() { return "bin"; }
radius__anon16a48f850211::BoolSampler274     static int radius() { return 2; }
mipmap__anon16a48f850211::BoolSampler275     static bool mipmap() { return false; }
consistent__anon16a48f850211::BoolSampler276     static bool consistent() { return true; }
277 
278     template<class TreeT>
sample__anon16a48f850211::BoolSampler279     static bool sample(const TreeT& inTree,
280         const openvdb::Vec3R& inCoord, typename TreeT::ValueType& result)
281     {
282         openvdb::Coord ijk;
283         ijk[0] = int(std::floor(inCoord[0]));
284         ijk[1] = int(std::floor(inCoord[1]));
285         ijk[2] = int(std::floor(inCoord[2]));
286         return inTree.probeValue(ijk, result);
287     }
288 };
289 
290 
291 struct ConstructShadow
292 {
ConstructShadow__anon16a48f850211::ConstructShadow293     ConstructShadow(const openvdb::math::Transform& frustum, int erode, int zoffset)
294         : mGrid(openvdb::BoolGrid::create(false))
295         , mFrustum(frustum)
296         , mErode(erode)
297         , mZOffset(zoffset)
298     {
299     }
300 
301 
302     template<typename GridType>
operator ()__anon16a48f850211::ConstructShadow303     void operator()(const GridType& grid)
304     {
305         using TreeType = typename GridType::TreeType;
306 
307         const TreeType& tree = grid.tree();
308 
309         // Resample active tree topology into camera frustum space.
310 
311         openvdb::BoolGrid frustumMask(false);
312         frustumMask.setTransform(mFrustum.copy());
313 
314         {
315             openvdb::BoolGrid topologyMask(false);
316             topologyMask.setTransform(grid.transform().copy());
317 
318             if (openvdb::GRID_LEVEL_SET == grid.getGridClass()) {
319 
320                 openvdb::BoolGrid::Ptr tmpGrid = openvdb::tools::sdfInteriorMask(grid);
321 
322                 topologyMask.tree().merge(tmpGrid->tree());
323 
324                 if (mErode > 3) {
325                     openvdb::tools::erodeActiveValues(topologyMask.tree(), (mErode - 3),
326                         openvdb::tools::NN_FACE, openvdb::tools::IGNORE_TILES);
327                 }
328 
329             } else {
330                 topologyMask.tree().topologyUnion(tree);
331 
332                 if (mErode > 0) {
333                     openvdb::tools::erodeActiveValues(topologyMask.tree(), mErode,
334                         openvdb::tools::NN_FACE, openvdb::tools::IGNORE_TILES);
335                 }
336             }
337 
338 
339             if (grid.transform().voxelSize()[0] < mFrustum.voxelSize()[0]) {
340                 openvdb::tools::resampleToMatch<openvdb::tools::PointSampler>(
341                     topologyMask, frustumMask);
342             } else {
343                 openvdb::tools::resampleToMatch<BoolSampler>(topologyMask, frustumMask);
344             }
345 
346         }
347 
348 
349         // Create shadow volume
350 
351         mGrid = openvdb::BoolGrid::create(false);
352         mGrid->setTransform(mFrustum.copy());
353         openvdb::BoolTree& shadowTree = mGrid->tree();
354 
355         const openvdb::math::NonlinearFrustumMap& map =
356             *mFrustum.map<openvdb::math::NonlinearFrustumMap>();
357         int zCoord = int(std::floor(map.getBBox().max()[2]));
358 
359         // Voxel shadows
360         openvdb::tree::LeafManager<const openvdb::BoolTree> leafs(frustumMask.tree());
361         VoxelShadow<openvdb::BoolTree> shadowOp(leafs, zCoord, mZOffset);
362         shadowOp.run();
363 
364         shadowTree.merge(shadowOp.tree());
365 
366         // Tile shadows
367         openvdb::CoordBBox bbox;
368         openvdb::BoolTree::ValueOnIter it(frustumMask.tree());
369         it.setMaxDepth(openvdb::BoolTree::ValueAllIter::LEAF_DEPTH - 1);
370         for ( ; it; ++it) {
371 
372             it.getBoundingBox(bbox);
373             bbox.min()[2] += mZOffset;
374             bbox.max()[2] = zCoord;
375 
376             shadowTree.fill(bbox, true, true);
377         }
378 
379         shadowTree.prune();
380     }
381 
grid__anon16a48f850211::ConstructShadow382     openvdb::BoolGrid::Ptr& grid() { return mGrid; }
383 
384 private:
385     openvdb::BoolGrid::Ptr mGrid;
386     const openvdb::math::Transform mFrustum;
387     const int mErode, mZOffset;
388 };
389 
390 
391 } // unnamed namespace
392 
393 
394 ////////////////////////////////////////
395 
396 
397 OP_ERROR
cookVDBSop(OP_Context & context)398 SOP_OpenVDB_Occlusion_Mask::Cache::cookVDBSop(OP_Context& context)
399 {
400     try {
401         const fpreal time = context.getTime();
402 
403         // Camera reference
404         mFrustum.reset();
405 
406         UT_String cameraPath;
407         evalString(cameraPath, "camera", 0, time);
408         cameraPath.harden();
409 
410         if (cameraPath.isstring()) {
411             OBJ_Node* camobj = cookparms()->getCwd()->findOBJNode(cameraPath);
412             OP_Node* self = cookparms()->getCwd();
413 
414             if (!camobj) {
415                 addError(SOP_MESSAGE, "Camera not found");
416                 return error();
417             }
418 
419             OBJ_Camera* cam = camobj->castToOBJCamera();
420             if (!cam) {
421                 addError(SOP_MESSAGE, "Camera not found");
422                 return error();
423             }
424 
425             // Register
426             this->addExtraInput(cam, OP_INTEREST_DATA);
427 
428             const float nearPlane = static_cast<float>(cam->getNEAR(time));
429             const float farPlane = static_cast<float>(nearPlane + evalFloat("depth", 0, time));
430             const float voxelDepthSize = static_cast<float>(evalFloat("voxeldepthsize", 0, time));
431             const int voxelCount = static_cast<int>(evalInt("voxelcount", 0, time));
432 
433             mFrustum = hvdb::frustumTransformFromCamera(*self, context, *cam,
434                 0, nearPlane, farPlane, voxelDepthSize, voxelCount);
435         } else {
436             addError(SOP_MESSAGE, "No camera referenced.");
437             return error();
438         }
439 
440 
441         ConstructShadow shadowOp(*mFrustum,
442             static_cast<int>(evalInt("erode", 0, time)),
443             static_cast<int>(evalInt("zoffset", 0, time)));
444 
445 
446         // Get the group of grids to surface.
447         const GA_PrimitiveGroup* group = matchGroup(*gdp, evalStdString("group", time));
448 
449         for (hvdb::VdbPrimIterator it(gdp, group); it; ++it) {
450 
451             hvdb::GEOvdbApply<hvdb::NumericGridTypes>(**it, shadowOp);
452 
453             // Replace the original VDB primitive with a new primitive that contains
454             // the output grid and has the same attributes and group membership.
455             if (GU_PrimVDB* prim = hvdb::replaceVdbPrimitive(*gdp, shadowOp.grid(), **it, true)) {
456                 // Visualize our bool grids as "smoke", not whatever the input
457                 // grid was, which can be a levelset.
458                 prim->setVisualization(GEO_VOLUMEVIS_SMOKE, prim->getVisIso(),
459                     prim->getVisDensity());
460             }
461         }
462 
463     } catch (std::exception& e) {
464         addError(SOP_MESSAGE, e.what());
465     }
466 
467     return error();
468 }
469