Blender V5.0
cycles/bvh/octree.cpp
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Foundation
2 *
3 * SPDX-License-Identifier: Apache-2.0 */
4
5#include "bvh/octree.h"
6
7#include "scene/object.h"
8#include "scene/volume.h"
9
11
12#include "util/log.h"
13#include "util/progress.h"
14
15#ifdef WITH_OPENVDB
16# include <openvdb/tools/FindActiveValues.h>
17#endif
18
20
21__forceinline int Octree::flatten_index(int x, int y, int z) const
22{
23 return x + resolution_ * (y + z * resolution_);
24}
25
26Extrema<float> Octree::get_extrema(const int3 index_min, const int3 index_max) const
27{
28 const blocked_range3d<int> range(
29 index_min.x, index_max.x, 32, index_min.y, index_max.y, 32, index_min.z, index_max.z, 32);
30
31 const Extrema<float> identity = {FLT_MAX, -FLT_MAX};
32
33 auto reduction_func = [&](const blocked_range3d<int> &r, Extrema<float> init) -> Extrema<float> {
34 for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
35 for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
36 for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
37 init = merge(init, sigmas_[flatten_index(x, y, z)]);
38 }
39 }
40 }
41 return init;
42 };
43
44 auto join_func = [](Extrema<float> a, Extrema<float> b) -> Extrema<float> {
45 return merge(a, b);
46 };
47
48 return parallel_reduce(range, identity, reduction_func, join_func);
49}
50
51__forceinline float3 Octree::position_to_index(const float3 p) const
52{
53 return (p - bbox_min) * position_to_index_scale_;
54}
55
56int3 Octree::position_to_floor_index(const float3 p) const
57{
58 const float3 index = round(position_to_index(p));
59 return clamp(make_int3(int(index.x), int(index.y), int(index.z)), 0, resolution_ - 1);
60}
61
62int3 Octree::position_to_ceil_index(const float3 p) const
63{
64 if (any_zero(position_to_index_scale_)) {
65 /* Octree with degenerate shape, force max index. */
66 return make_int3(resolution_);
67 }
68 const float3 index = round(position_to_index(p));
69 return clamp(make_int3(int(index.x), int(index.y), int(index.z)), 1, resolution_);
70}
71
73{
74 return bbox_min + make_float3(x, y, z) * index_to_position_scale_;
75}
76
78{
79 return index_to_position_scale_;
80}
81
82bool Octree::should_split(std::shared_ptr<OctreeNode> &node) const
83{
84 const int3 index_min = position_to_floor_index(node->bbox.min);
85 const int3 index_max = position_to_ceil_index(node->bbox.max);
86 node->sigma = get_extrema(index_min, index_max);
87
88 const float3 bbox_size = node->bbox.size();
89 if (any_zero(bbox_size)) {
90 /* Octree with degenerate shape, can happen for implicit volume. */
91 return false;
92 }
93
94 /* The threshold is set so that ideally only one sample needs to be taken per node. Value taken
95 * from "Volume Rendering for Pixar's Elemental". */
96 return (node->sigma.range() * len(bbox_size) * scale_ > 1.442f &&
97 node->depth < VOLUME_OCTREE_MAX_DEPTH);
98}
99
100#ifdef WITH_OPENVDB
101/* Check if a interior mask grid intersects with a bounding box defined by `p_min` and `p_max`. */
102static bool vdb_voxel_intersect(const float3 p_min,
103 const float3 p_max,
104 openvdb::BoolGrid::ConstPtr &grid,
105 const openvdb::tools::FindActiveValues<openvdb::BoolTree> &find)
106{
107 if (grid->empty()) {
108 /* Non-mesh volume or open mesh. */
109 return true;
110 }
111
112 const openvdb::math::CoordBBox coord_bbox(
113 openvdb::Coord::floor(grid->worldToIndex({p_min.x, p_min.y, p_min.z})),
114 openvdb::Coord::ceil(grid->worldToIndex({p_max.x, p_max.y, p_max.z})));
115
116 /* Check if the bounding box lies inside or partially overlaps the mesh.
117 * For interior mask grids, all the interior voxels are active. */
118 return find.anyActiveValues(coord_bbox, true);
119}
120#endif
121
122/* Fill in coordinates for shading the volume density. */
124 const Octree *octree,
125 const Object *object,
126 const Shader *shader,
127#ifdef WITH_OPENVDB
128 openvdb::BoolGrid::ConstPtr &interior_mask,
129#endif
130 const int resolution)
131{
132 const int object_id = object->get_device_index();
133 const uint shader_id = shader->id;
134
135 KernelShaderEvalInput *d_input_data = d_input.data();
136
137 const float3 voxel_size = octree->voxel_size();
138 /* Dilate the voxel in case we miss features at the boundary. */
139 const float3 pad = 0.2f * voxel_size;
140 const float3 padded_size = voxel_size + pad * 2.0f;
141
142 const blocked_range3d<int> range(0, resolution, 8, 0, resolution, 8, 0, resolution, 8);
143 parallel_for(range, [&](const blocked_range3d<int> &r) {
144#ifdef WITH_OPENVDB
145 /* One accessor per thread is important for cached access. */
146 const auto find = openvdb::tools::FindActiveValues(interior_mask->tree());
147#endif
148
149 for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
150 for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
151 for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
152 const int offset = octree->flatten_index(x, y, z);
153 const float3 p = octree->index_to_position(x, y, z);
154
155#ifdef WITH_OPENVDB
156 /* Zero density for cells outside of the mesh. */
157 if (!vdb_voxel_intersect(p, p + voxel_size, interior_mask, find)) {
158 d_input_data[offset * 2].object = OBJECT_NONE;
159 d_input_data[offset * 2 + 1].object = SHADER_NONE;
160 continue;
161 }
162#endif
163
165 in.object = object_id;
166 in.prim = __float_as_int(p.x - pad.x);
167 in.u = p.y - pad.y;
168 in.v = p.z - pad.z;
169 d_input_data[offset * 2] = in;
170
171 in.object = shader_id;
172 in.prim = __float_as_int(padded_size.x);
173 in.u = padded_size.y;
174 in.v = padded_size.z;
175 d_input_data[offset * 2 + 1] = in;
176 }
177 }
178 }
179 });
180}
181
182/* Read back the volume density. */
183static void read_shader_output(const device_vector<float> &d_output,
184 const Octree *octree,
185 const int num_channels,
186 const int resolution,
187 vector<Extrema<float>> &sigmas)
188{
189 const float *d_output_data = d_output.data();
190 const blocked_range3d<int> range(0, resolution, 32, 0, resolution, 32, 0, resolution, 32);
191
192 parallel_for(range, [&](const blocked_range3d<int> &r) {
193 for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
194 for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
195 for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
196 const int index = octree->flatten_index(x, y, z);
197 sigmas[index].min = d_output_data[index * num_channels + 0];
198 sigmas[index].max = d_output_data[index * num_channels + 1];
199 }
200 }
201 }
202 });
203}
204
205void Octree::evaluate_volume_density(Device *device,
206 Progress &progress,
207#ifdef WITH_OPENVDB
208 openvdb::BoolGrid::ConstPtr &interior_mask,
209#endif
210 const Object *object,
211 const Shader *shader)
212{
213 /* For heterogeneous volume, the grid resolution is 2^max_depth in each 3D dimension;
214 * for homogeneous volume, only one grid is needed. */
215 resolution_ = VolumeManager::is_homogeneous_volume(object, shader) ?
216 1 :
218 index_to_position_scale_ = root_->bbox.size() / float(resolution_);
219 position_to_index_scale_ = safe_divide(one_float3(), index_to_position_scale_);
220
221 /* Initialize density field. */
222 /* TODO(weizhen): maybe lower the resolution depending on the object size. */
223 const int size = resolution_ * resolution_ * resolution_;
224 sigmas_.resize(size);
225 parallel_for(0, size, [&](int i) { sigmas_[i] = {0.0f, 0.0f}; });
226
227 /* Min and max. */
228 const int num_channels = 2;
229
230 /* Need the size of two `KernelShaderEvalInput`s per voxel for evaluating the shader. */
231 const int num_inputs = size * 2;
232
233 /* Evaluate shader on device. */
234 ShaderEval shader_eval(device, progress);
235 shader_eval.eval(
237 num_inputs,
238 num_channels,
239 [&](device_vector<KernelShaderEvalInput> &d_input) {
240#ifdef WITH_OPENVDB
241 fill_shader_input(d_input, this, object, shader, interior_mask, resolution_);
242#else
243 fill_shader_input(d_input, this, object, shader, resolution_);
244#endif
245 return size;
246 },
247 [&](device_vector<float> &d_output) {
248 read_shader_output(d_output, this, num_channels, resolution_, sigmas_);
249 });
250}
251
252float Octree::volume_scale(const Object *object) const
253{
254 const Geometry *geom = object->get_geometry();
255 if (geom->is_volume()) {
256 const Volume *volume = static_cast<const Volume *>(geom);
257 if (volume->get_object_space()) {
258 /* The density changes with object scale, we scale the density accordingly in the final
259 * render. */
260 if (volume->transform_applied) {
261 const float3 unit = normalize(one_float3());
262 return 1.0f / len(transform_direction(&object->get_tfm(), unit));
263 }
264 }
265 else {
266 /* The density does not change with object scale, we scale the node in the viewport to it's
267 * true size. */
268 if (!volume->transform_applied) {
269 const float3 unit = normalize(one_float3());
270 return len(transform_direction(&object->get_tfm(), unit));
271 }
272 }
273 }
274 else {
275 /* TODO(weizhen): use the maximal scale of all instances. */
276 if (!geom->transform_applied) {
277 const float3 unit = normalize(one_float3());
278 return len(transform_direction(&object->get_tfm(), unit));
279 }
280 }
281
282 return 1.0f;
283}
284
285std::shared_ptr<OctreeInternalNode> Octree::make_internal(std::shared_ptr<OctreeNode> &node)
286{
287 num_nodes_ += 8;
288 auto internal = std::make_shared<OctreeInternalNode>(*node);
289
290 /* Create bounding boxes for children. */
291 const float3 center = internal->bbox.center();
292 for (int i = 0; i < 8; i++) {
293 const float3 t = make_float3(i & 1, (i >> 1) & 1, (i >> 2) & 1);
294 const BoundBox bbox(mix(internal->bbox.min, center, t), mix(center, internal->bbox.max, t));
295 internal->children_[i] = std::make_shared<OctreeNode>(bbox, internal->depth + 1);
296 }
297
298 return internal;
299}
300
301void Octree::recursive_build(std::shared_ptr<OctreeNode> &octree_node)
302{
303 if (!should_split(octree_node)) {
304 return;
305 }
306
307 /* Make the current node an internal node. */
308 auto internal = make_internal(octree_node);
309
310 for (auto &child : internal->children_) {
311 task_pool_.push([&] { recursive_build(child); });
312 }
313
314 octree_node = internal;
315}
316
318 const int current_index,
319 const std::shared_ptr<OctreeNode> &node,
320 int &child_index) const
321{
322 KernelOctreeNode &knode = knodes[current_index];
323 knode.sigma = node->sigma;
324
325 if (auto internal_ptr = std::dynamic_pointer_cast<OctreeInternalNode>(node)) {
326 knode.first_child = child_index;
327 child_index += 8;
328 /* Loop through all the children and flatten in breadth-first manner, so that children are
329 * stored in contiguous indices. */
330 for (int i = 0; i < 8; i++) {
331 knodes[knode.first_child + i].parent = current_index;
332 flatten(knodes, knode.first_child + i, internal_ptr->children_[i], child_index);
333 }
334 }
335 else {
336 knode.first_child = -1;
337 }
338}
339
340void Octree::set_flattened(const bool flattened)
341{
342 is_flattened_ = flattened;
343}
344
346{
347 return is_flattened_;
348}
349
351 Progress &progress,
352#ifdef WITH_OPENVDB
353 openvdb::BoolGrid::ConstPtr &interior_mask,
354#endif
355 const Object *object,
356 const Shader *shader)
357{
358 const char *name = object->get_asset_name().c_str();
359 progress.set_substatus(string_printf("Evaluating density for %s", name));
360
361#ifdef WITH_OPENVDB
362 evaluate_volume_density(device, progress, interior_mask, object, shader);
363#else
364 evaluate_volume_density(device, progress, object, shader);
365#endif
366 if (progress.get_cancel()) {
367 return;
368 }
369
370 progress.set_substatus(string_printf("Building octree for %s", name));
371
372 scale_ = volume_scale(object);
373 recursive_build(root_);
374
375 task_pool_.wait_work();
376
377 is_built_ = true;
378 sigmas_.clear();
379}
380
382{
383 bbox_min = bbox.min;
384 root_ = std::make_shared<OctreeNode>(bbox, 0);
385 is_built_ = false;
386 is_flattened_ = false;
387}
388
390{
391 return is_built_;
392}
393
395{
396 return num_nodes_;
397}
398
399std::shared_ptr<OctreeNode> Octree::get_root() const
400{
401 return root_;
402}
403
404void OctreeNode::visualize(std::string &str) const
405{
406 const auto *internal = dynamic_cast<const OctreeInternalNode *>(this);
407
408 if (!internal) {
409 /* Skip leaf nodes. */
410 return;
411 }
412
413 /* Create three orthogonal faces for inner nodes. */
414 const float3 mid = bbox.center();
415 const float3 max = bbox.max;
416 const float3 min = bbox.min;
417 const std::string mid_x = to_string(mid.x), mid_y = to_string(mid.y), mid_z = to_string(mid.z),
418 min_x = to_string(min.x), min_y = to_string(min.y), min_z = to_string(min.z),
419 max_x = to_string(max.x), max_y = to_string(max.y), max_z = to_string(max.z);
420 // clang-format off
421 str += "(" + mid_x + "," + mid_y + "," + min_z + "), "
422 "(" + mid_x + "," + mid_y + "," + max_z + "), "
423 "(" + mid_x + "," + max_y + "," + max_z + "), "
424 "(" + mid_x + "," + max_y + "," + min_z + "), "
425 "(" + mid_x + "," + min_y + "," + min_z + "), "
426 "(" + mid_x + "," + min_y + "," + max_z + "), ";
427 str += "(" + min_x + "," + mid_y + "," + mid_z + "), "
428 "(" + max_x + "," + mid_y + "," + mid_z + "), "
429 "(" + max_x + "," + mid_y + "," + max_z + "), "
430 "(" + min_x + "," + mid_y + "," + max_z + "), "
431 "(" + min_x + "," + mid_y + "," + min_z + "), "
432 "(" + max_x + "," + mid_y + "," + min_z + "), ";
433 str += "(" + mid_x + "," + min_y + "," + mid_z + "), "
434 "(" + mid_x + "," + max_y + "," + mid_z + "), "
435 "(" + max_x + "," + max_y + "," + mid_z + "), "
436 "(" + max_x + "," + min_y + "," + mid_z + "), "
437 "(" + min_x + "," + min_y + "," + mid_z + "), "
438 "(" + min_x + "," + max_y + "," + mid_z + "), ";
439 // clang-format on
440 for (const auto &child : internal->children_) {
441 child->visualize(str);
442 }
443}
444
445void Octree::visualize(std::ofstream &file, const std::string object_name) const
446{
447 std::string str = "vertices = [";
448 root_->visualize(str);
449 str +=
450 "]\nr = range(len(vertices))\n"
451 "edges = [(i, i+1 if i%6<5 else i-4) for i in r]\n"
452 "mesh = bpy.data.meshes.new('Octree')\n"
453 "mesh.from_pydata(vertices, edges, [])\n"
454 "mesh.update()\n"
455 "obj = bpy.data.objects.new('" +
456 object_name +
457 "', mesh)\n"
458 "octree.objects.link(obj)\n"
459 "bpy.context.view_layer.objects.active = obj\n"
460 "bpy.ops.object.mode_set(mode='EDIT')\n";
461 file << str;
462
463 const float3 center = root_->bbox.center();
464 const float3 size = root_->bbox.size() * 0.5f;
465 file << "bpy.ops.mesh.primitive_cube_add(location = " << center << ", scale = " << size << ")\n";
466 file << "bpy.ops.mesh.delete(type='ONLY_FACE')\n"
467 "bpy.ops.object.mode_set(mode='OBJECT')\n"
468 "obj.select_set(True)\n";
469}
470
MINLINE float power_of_2(float f)
MINLINE float safe_divide(float a, float b)
unsigned int uint
struct BoundBox BoundBox
struct Volume Volume
int pad[32 - sizeof(int)]
void init()
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
SIMD_FORCE_INLINE const btScalar & z() const
Return the z value.
Definition btQuadWord.h:117
bool transform_applied
bool is_volume() const
void visualize(std::ofstream &file, const std::string object_name) const
int get_num_nodes() const
float3 voxel_size() const
int flatten_index(int x, int y, int z) const
void flatten(KernelOctreeNode *, const int, const std::shared_ptr< OctreeNode > &, int &) const
void set_flattened(const bool=true)
float3 index_to_position(int x, int y, int z) const
std::shared_ptr< OctreeNode > get_root() const
void build(Device *, Progress &, const Object *, const Shader *)
bool is_flattened() const
Octree(const BoundBox &bbox)
bool is_built() const
void set_substatus(const string &substatus_)
Definition progress.h:259
bool get_cancel() const
Definition progress.h:77
static bool is_homogeneous_volume(const Object *, const Shader *)
nullptr float
static void fill_shader_input(device_vector< KernelShaderEvalInput > &d_input, const Octree *octree, const Object *object, const Shader *shader, const int resolution)
static void read_shader_output(const device_vector< float > &d_output, const Octree *octree, const int num_channels, const int resolution, vector< Extrema< float > > &sigmas)
#define SHADER_NONE
#define OBJECT_NONE
#define VOLUME_OCTREE_MAX_DEPTH
#define __forceinline
#define CCL_NAMESPACE_END
ccl_device_forceinline float3 make_float3(const float x, const float y, const float z)
ccl_device_forceinline int3 make_int3(const int x, const int y, const int z)
#define __float_as_int(x)
#define str(s)
static const char * to_string(const Interpolation &interp)
Definition gl_shader.cc:103
#define in
#define round
VecBase< float, D > normalize(VecOp< float, D >) RET
constexpr T clamp(T, U, U) RET
VecBase< float, 3 > float3
OrientationBounds merge(const OrientationBounds &cone_a, const OrientationBounds &cone_b)
ccl_device_inline float3 one_float3()
Definition math_float3.h:26
ccl_device_inline bool any_zero(const float3 a)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
const char * name
#define mix
@ SHADER_EVAL_VOLUME_DENSITY
Definition shader_eval.h:22
#define min(a, b)
Definition sort.cc:36
#define FLT_MAX
Definition stdcycles.h:14
CCL_NAMESPACE_BEGIN string string_printf(const char *format,...)
Definition string.cpp:23
float3 min
Definition boundbox.h:20
Extrema< float > sigma
void visualize(std::string &str) const
float z
Definition sky_math.h:136
float y
Definition sky_math.h:136
float x
Definition sky_math.h:136
i
Definition text_draw.cc:230
max
Definition text_draw.cc:251
static Value parallel_reduce(const int range, const Value &identity, const Function &function, const Reduction &reduction)
Definition tracking.cc:2514
ccl_device_inline float3 transform_direction(const ccl_private Transform *t, const float3 a)
Definition transform.h:127
uint len