Files
workinf_Blender_Wasm/blender-5.2.0/intern/cycles/bvh/octree.cpp
2026-08-12 04:47:48 -04:00

474 lines
15 KiB
C++

/* SPDX-FileCopyrightText: 2025 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "bvh/octree.h"
#include "scene/object.h"
#include "scene/volume.h"
#include "integrator/shader_eval.h"
#include "util/log.h"
#include "util/progress.h"
#ifdef WITH_OPENVDB
# include <openvdb/tools/FindActiveValues.h>
#endif
#include <fstream>
CCL_NAMESPACE_BEGIN
__forceinline int Octree::flatten_index(int x, int y, int z) const
{
return x + resolution_ * (y + z * resolution_);
}
Extrema<float> Octree::get_extrema(const int3 index_min, const int3 index_max) const
{
const blocked_range3d<int> range(
index_min.x, index_max.x, 32, index_min.y, index_max.y, 32, index_min.z, index_max.z, 32);
const Extrema<float> identity = {FLT_MAX, -FLT_MAX};
auto reduction_func = [&](const blocked_range3d<int> &r, Extrema<float> init) -> Extrema<float> {
for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
init = merge(init, sigmas_[flatten_index(x, y, z)]);
}
}
}
return init;
};
auto join_func = [](Extrema<float> a, Extrema<float> b) -> Extrema<float> {
return merge(a, b);
};
return parallel_reduce(range, identity, reduction_func, join_func);
}
__forceinline float3 Octree::position_to_index(const float3 p) const
{
return (p - bbox_min) * position_to_index_scale_;
}
int3 Octree::position_to_floor_index(const float3 p) const
{
const float3 index = round(position_to_index(p));
return clamp(make_int3(int(index.x), int(index.y), int(index.z)), 0, resolution_ - 1);
}
int3 Octree::position_to_ceil_index(const float3 p) const
{
if (any_zero(position_to_index_scale_)) {
/* Octree with degenerate shape, force max index. */
return make_int3(resolution_);
}
const float3 index = round(position_to_index(p));
return clamp(make_int3(int(index.x), int(index.y), int(index.z)), 1, resolution_);
}
__forceinline float3 Octree::index_to_position(int x, int y, int z) const
{
return bbox_min + make_float3(x, y, z) * index_to_position_scale_;
}
__forceinline float3 Octree::voxel_size() const
{
return index_to_position_scale_;
}
bool Octree::should_split(std::shared_ptr<OctreeNode> &node) const
{
const int3 index_min = position_to_floor_index(node->bbox.min);
const int3 index_max = position_to_ceil_index(node->bbox.max);
node->sigma = get_extrema(index_min, index_max);
const float3 bbox_size = node->bbox.size();
if (any_zero(bbox_size)) {
/* Octree with degenerate shape, can happen for implicit volume. */
return false;
}
/* The threshold is set so that ideally only one sample needs to be taken per node. Value taken
* from "Volume Rendering for Pixar's Elemental". */
return (node->sigma.range() * len(bbox_size) * scale_ > 1.442f &&
node->depth < VOLUME_OCTREE_MAX_DEPTH);
}
#ifdef WITH_OPENVDB
/* Check if a interior mask grid intersects with a bounding box defined by `p_min` and `p_max`. */
static bool vdb_voxel_intersect(const float3 p_min,
const float3 p_max,
openvdb::BoolGrid::ConstPtr &grid,
const openvdb::tools::FindActiveValues<openvdb::BoolTree> &find)
{
if (grid->empty()) {
/* Non-mesh volume or open mesh. */
return true;
}
const openvdb::math::CoordBBox coord_bbox(
openvdb::Coord::floor(grid->worldToIndex({p_min.x, p_min.y, p_min.z})),
openvdb::Coord::ceil(grid->worldToIndex({p_max.x, p_max.y, p_max.z})));
/* Check if the bounding box lies inside or partially overlaps the mesh.
* For interior mask grids, all the interior voxels are active. */
return find.anyActiveValues(coord_bbox, true);
}
#endif
/* Fill in coordinates for shading the volume density. */
static void fill_shader_input(device_vector<KernelShaderEvalInput> &d_input,
const Octree *octree,
const Object *object,
const Shader *shader,
#ifdef WITH_OPENVDB
openvdb::BoolGrid::ConstPtr &interior_mask,
#endif
const int resolution)
{
const int object_id = object->get_device_index();
const uint shader_id = shader->id;
KernelShaderEvalInput *d_input_data = d_input.data();
const float3 voxel_size = octree->voxel_size();
/* Dilate the voxel in case we miss features at the boundary. */
const float3 pad = 0.2f * voxel_size;
const float3 padded_size = voxel_size + pad * 2.0f;
const blocked_range3d<int> range(0, resolution, 8, 0, resolution, 8, 0, resolution, 8);
parallel_for(range, [&](const blocked_range3d<int> &r) {
#ifdef WITH_OPENVDB
/* One accessor per thread is important for cached access. */
const auto find = openvdb::tools::FindActiveValues(interior_mask->tree());
#endif
for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
const int offset = octree->flatten_index(x, y, z);
const float3 p = octree->index_to_position(x, y, z);
#ifdef WITH_OPENVDB
/* Zero density for cells outside of the mesh. */
if (!vdb_voxel_intersect(p, p + voxel_size, interior_mask, find)) {
d_input_data[offset * 2].object = OBJECT_NONE;
d_input_data[offset * 2 + 1].object = SHADER_NONE;
continue;
}
#endif
KernelShaderEvalInput in;
in.object = object_id;
in.prim = __float_as_int(p.x - pad.x);
in.u = p.y - pad.y;
in.v = p.z - pad.z;
d_input_data[offset * 2] = in;
in.object = shader_id;
in.prim = __float_as_int(padded_size.x);
in.u = padded_size.y;
in.v = padded_size.z;
d_input_data[offset * 2 + 1] = in;
}
}
}
});
}
/* Read back the volume density. */
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)
{
const float *d_output_data = d_output.data();
const blocked_range3d<int> range(0, resolution, 32, 0, resolution, 32, 0, resolution, 32);
parallel_for(range, [&](const blocked_range3d<int> &r) {
for (int z = r.cols().begin(); z < r.cols().end(); ++z) {
for (int y = r.rows().begin(); y < r.rows().end(); ++y) {
for (int x = r.pages().begin(); x < r.pages().end(); ++x) {
const int index = octree->flatten_index(x, y, z);
sigmas[index].min = d_output_data[index * num_channels + 0];
sigmas[index].max = d_output_data[index * num_channels + 1];
}
}
}
});
}
void Octree::evaluate_volume_density(Device *device,
Progress &progress,
#ifdef WITH_OPENVDB
openvdb::BoolGrid::ConstPtr &interior_mask,
#endif
const Object *object,
const Shader *shader)
{
/* For heterogeneous volume, the grid resolution is 2^max_depth in each 3D dimension;
* for homogeneous volume, only one grid is needed. */
resolution_ = VolumeManager::is_homogeneous_volume(object, shader) ?
1 :
power_of_2(VOLUME_OCTREE_MAX_DEPTH);
index_to_position_scale_ = root_->bbox.size() / float(resolution_);
position_to_index_scale_ = safe_divide(one_float3(), index_to_position_scale_);
/* Initialize density field. */
/* TODO(weizhen): maybe lower the resolution depending on the object size. */
const int size = resolution_ * resolution_ * resolution_;
sigmas_.resize(size);
parallel_for(0, size, [&](int i) { sigmas_[i] = {0.0f, 0.0f}; });
/* Min and max. */
const int num_channels = 2;
/* Need the size of two `KernelShaderEvalInput`s per voxel for evaluating the shader. */
const int num_inputs = size * 2;
/* Evaluate shader on device. */
ShaderEval shader_eval(device, progress);
shader_eval.eval(
SHADER_EVAL_VOLUME_DENSITY,
num_inputs,
num_channels,
[&](device_vector<KernelShaderEvalInput> &d_input) {
#ifdef WITH_OPENVDB
fill_shader_input(d_input, this, object, shader, interior_mask, resolution_);
#else
fill_shader_input(d_input, this, object, shader, resolution_);
#endif
return size;
},
[&](device_vector<float> &d_output) {
read_shader_output(d_output, this, num_channels, resolution_, sigmas_);
});
}
float Octree::volume_scale(const Object *object) const
{
const Geometry *geom = object->get_geometry();
if (geom->is_volume()) {
const Volume *volume = static_cast<const Volume *>(geom);
if (volume->get_object_space()) {
/* The density changes with object scale, we scale the density accordingly in the final
* render. */
if (volume->transform_applied) {
const float3 unit = normalize(one_float3());
return 1.0f / len(transform_direction(&object->get_tfm(), unit));
}
}
else {
/* The density does not change with object scale, we scale the node in the viewport to it's
* true size. */
if (!volume->transform_applied) {
const float3 unit = normalize(one_float3());
return len(transform_direction(&object->get_tfm(), unit));
}
}
}
else {
/* TODO(weizhen): use the maximal scale of all instances. */
if (!geom->transform_applied) {
const float3 unit = normalize(one_float3());
return len(transform_direction(&object->get_tfm(), unit));
}
}
return 1.0f;
}
std::shared_ptr<OctreeInternalNode> Octree::make_internal(std::shared_ptr<OctreeNode> &node)
{
num_nodes_ += 8;
auto internal = std::make_shared<OctreeInternalNode>(*node);
/* Create bounding boxes for children. */
const float3 center = internal->bbox.center();
for (int i = 0; i < 8; i++) {
const float3 t = make_float3(i & 1, (i >> 1) & 1, (i >> 2) & 1);
const BoundBox bbox(mix(internal->bbox.min, center, t), mix(center, internal->bbox.max, t));
internal->children_[i] = std::make_shared<OctreeNode>(bbox, internal->depth + 1);
}
return internal;
}
void Octree::recursive_build(std::shared_ptr<OctreeNode> &octree_node)
{
if (!should_split(octree_node)) {
return;
}
/* Make the current node an internal node. */
auto internal = make_internal(octree_node);
for (auto &child : internal->children_) {
task_pool_.push([&] { recursive_build(child); });
}
octree_node = internal;
}
void Octree::flatten(KernelOctreeNode *knodes,
const int current_index,
const std::shared_ptr<OctreeNode> &node,
int &child_index) const
{
KernelOctreeNode &knode = knodes[current_index];
knode.sigma = node->sigma;
if (auto internal_ptr = std::dynamic_pointer_cast<OctreeInternalNode>(node)) {
knode.first_child = child_index;
child_index += 8;
/* Loop through all the children and flatten in breadth-first manner, so that children are
* stored in contiguous indices. */
for (int i = 0; i < 8; i++) {
knodes[knode.first_child + i].parent = current_index;
flatten(knodes, knode.first_child + i, internal_ptr->children_[i], child_index);
}
}
else {
knode.first_child = -1;
}
}
void Octree::set_flattened(const bool flattened)
{
is_flattened_ = flattened;
}
bool Octree::is_flattened() const
{
return is_flattened_;
}
void Octree::build(Device *device,
Progress &progress,
#ifdef WITH_OPENVDB
openvdb::BoolGrid::ConstPtr &interior_mask,
#endif
const Object *object,
const Shader *shader)
{
const char *name = object->get_asset_name().c_str();
progress.set_substatus(string_printf("Evaluating density for %s", name));
#ifdef WITH_OPENVDB
evaluate_volume_density(device, progress, interior_mask, object, shader);
#else
evaluate_volume_density(device, progress, object, shader);
#endif
if (progress.get_cancel()) {
return;
}
progress.set_substatus(string_printf("Building octree for %s", name));
scale_ = volume_scale(object);
recursive_build(root_);
task_pool_.wait_work();
is_built_ = true;
sigmas_.clear();
}
Octree::Octree(const BoundBox &bbox)
{
bbox_min = bbox.min;
root_ = std::make_shared<OctreeNode>(bbox, 0);
is_built_ = false;
is_flattened_ = false;
}
bool Octree::is_built() const
{
return is_built_;
}
int Octree::get_num_nodes() const
{
return num_nodes_;
}
std::shared_ptr<OctreeNode> Octree::get_root() const
{
return root_;
}
void OctreeNode::visualize(std::string &str) const
{
const auto *internal = dynamic_cast<const OctreeInternalNode *>(this);
if (!internal) {
/* Skip leaf nodes. */
return;
}
/* Create three orthogonal faces for inner nodes. */
const float3 mid = bbox.center();
const float3 max = bbox.max;
const float3 min = bbox.min;
const std::string mid_x = to_string(mid.x), mid_y = to_string(mid.y), mid_z = to_string(mid.z),
min_x = to_string(min.x), min_y = to_string(min.y), min_z = to_string(min.z),
max_x = to_string(max.x), max_y = to_string(max.y), max_z = to_string(max.z);
// clang-format off
str += "(" + mid_x + "," + mid_y + "," + min_z + "), "
"(" + mid_x + "," + mid_y + "," + max_z + "), "
"(" + mid_x + "," + max_y + "," + max_z + "), "
"(" + mid_x + "," + max_y + "," + min_z + "), "
"(" + mid_x + "," + min_y + "," + min_z + "), "
"(" + mid_x + "," + min_y + "," + max_z + "), ";
str += "(" + min_x + "," + mid_y + "," + mid_z + "), "
"(" + max_x + "," + mid_y + "," + mid_z + "), "
"(" + max_x + "," + mid_y + "," + max_z + "), "
"(" + min_x + "," + mid_y + "," + max_z + "), "
"(" + min_x + "," + mid_y + "," + min_z + "), "
"(" + max_x + "," + mid_y + "," + min_z + "), ";
str += "(" + mid_x + "," + min_y + "," + mid_z + "), "
"(" + mid_x + "," + max_y + "," + mid_z + "), "
"(" + max_x + "," + max_y + "," + mid_z + "), "
"(" + max_x + "," + min_y + "," + mid_z + "), "
"(" + min_x + "," + min_y + "," + mid_z + "), "
"(" + min_x + "," + max_y + "," + mid_z + "), ";
// clang-format on
for (const auto &child : internal->children_) {
child->visualize(str);
}
}
void Octree::visualize(std::ofstream &file, const std::string object_name) const
{
std::string str = "vertices = [";
root_->visualize(str);
str +=
"]\nr = range(len(vertices))\n"
"edges = [(i, i+1 if i%6<5 else i-4) for i in r]\n"
"mesh = bpy.data.meshes.new('Octree')\n"
"mesh.from_pydata(vertices, edges, [])\n"
"mesh.update()\n"
"obj = bpy.data.objects.new('" +
object_name +
"', mesh)\n"
"octree.objects.link(obj)\n"
"bpy.context.view_layer.objects.active = obj\n"
"bpy.ops.object.mode_set(mode='EDIT')\n";
file << str;
const float3 center = root_->bbox.center();
const float3 size = root_->bbox.size() * 0.5f;
file << "bpy.ops.mesh.primitive_cube_add(location = " << center << ", scale = " << size << ")\n";
file << "bpy.ops.mesh.delete(type='ONLY_FACE')\n"
"bpy.ops.object.mode_set(mode='OBJECT')\n"
"obj.select_set(True)\n";
}
CCL_NAMESPACE_END