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