Geometry Nodes: Distribute Points in Volume

This commit adds a node to distribute points inside of volume grids.
The "Random" mode usese OpenVDB's "point scatter" implementation, and
there is also a "Grid" mode for uniform distributions. Both methods
operate on all of the float grids in the volume, using every voxel with
a value higher than the threshold. The random method is not stable as
the input volume deforms.

Based on a patch by Angus Stanton (@abstanton), which was based on a
patch by Kenzie (@kenziemac130).

Differential Revision: https://developer.blender.org/D15375
This commit is contained in:
Iyad Ahmed 2022-09-19 10:13:55 -05:00 committed by Hans Goudey
parent 0e6a8df6df
commit b6e26a410c
Notes: blender-bot 2023-02-14 06:42:53 +01:00
Referenced by issue #85898, Geometry Nodes: Distribute Points in Volume
9 changed files with 326 additions and 0 deletions

View File

@ -241,6 +241,7 @@ def point_node_items(context):
space = context.space_data
if not space:
return
yield NodeItem("GeometryNodeDistributePointsInVolume")
yield NodeItem("GeometryNodeDistributePointsOnFaces")
yield NodeItem("GeometryNodePoints")
yield NodeItem("GeometryNodePointsToVertices")

View File

@ -1526,6 +1526,7 @@ struct TexResult;
#define GEO_NODE_EDGE_PATHS_TO_CURVES 1169
#define GEO_NODE_EDGE_PATHS_TO_SELECTION 1170
#define GEO_NODE_MESH_FACE_SET_BOUNDARIES 1171
#define GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME 1172
/** \} */

View File

@ -4702,6 +4702,7 @@ static void registerGeometryNodes()
register_node_type_geo_curve_trim();
register_node_type_geo_deform_curves_on_surface();
register_node_type_geo_delete_geometry();
register_node_type_geo_distribute_points_in_volume();
register_node_type_geo_distribute_points_on_faces();
register_node_type_geo_dual_mesh();
register_node_type_geo_duplicate_elements();

View File

@ -1595,6 +1595,11 @@ typedef struct NodeGeometryUVUnwrap {
uint8_t method;
} NodeGeometryUVUnwrap;
typedef struct NodeGeometryDistributePointsInVolume {
/* GeometryNodePointDistributeVolumeMode. */
uint8_t mode;
} NodeGeometryDistributePointsInVolume;
typedef struct NodeFunctionCompare {
/* NodeCompareOperation */
int8_t operation;
@ -2176,6 +2181,11 @@ typedef enum GeometryNodeTriangulateQuads {
GEO_NODE_TRIANGULATE_QUAD_LONGEDGE = 4,
} GeometryNodeTriangulateQuads;
typedef enum GeometryNodeDistributePointsInVolumeMode {
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM = 0,
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID = 1,
} GeometryNodeDistributePointsInVolumeMode;
typedef enum GeometryNodeDistributePointsOnFacesMode {
GEO_NODE_POINT_DISTRIBUTE_POINTS_ON_FACES_RANDOM = 0,
GEO_NODE_POINT_DISTRIBUTE_POINTS_ON_FACES_POISSON = 1,

View File

@ -9614,6 +9614,31 @@ static void def_geo_extrude_mesh(StructRNA *srna)
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_geo_distribute_points_in_volume(StructRNA *srna)
{
PropertyRNA *prop;
static const EnumPropertyItem mode_items[] = {
{GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM,
"DENSITY_RANDOM",
0,
"Random",
"Distribute points randomly inside of the volume"},
{GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID,
"DENSITY_GRID",
0,
"Grid",
"Distribute the points in a grid pattern inside of the volume"},
{0, NULL, 0, NULL, NULL},
};
RNA_def_struct_sdna_from(srna, "NodeGeometryDistributePointsInVolume", "storage");
prop = RNA_def_property(srna, "mode", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, mode_items);
RNA_def_property_ui_text(prop, "Distribution Method", "Method to use for scattering points");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_socket_update");
}
static void def_geo_distribute_points_on_faces(StructRNA *srna)
{
PropertyRNA *prop;

View File

@ -49,6 +49,7 @@ void register_node_type_geo_curve_to_points(void);
void register_node_type_geo_curve_trim(void);
void register_node_type_geo_deform_curves_on_surface(void);
void register_node_type_geo_delete_geometry(void);
void register_node_type_geo_distribute_points_in_volume(void);
void register_node_type_geo_distribute_points_on_faces(void);
void register_node_type_geo_dual_mesh(void);
void register_node_type_geo_duplicate_elements(void);

View File

@ -305,6 +305,7 @@ DefNode(GeometryNode, GEO_NODE_CURVE_TO_MESH, 0, "CURVE_TO_MESH", CurveToMesh, "
DefNode(GeometryNode, GEO_NODE_CURVE_TO_POINTS, def_geo_curve_to_points, "CURVE_TO_POINTS", CurveToPoints, "Curve to Points", "Generate a point cloud by sampling positions along curves")
DefNode(GeometryNode, GEO_NODE_DEFORM_CURVES_ON_SURFACE, 0, "DEFORM_CURVES_ON_SURFACE", DeformCurvesOnSurface, "Deform Curves on Surface", "Translate and rotate curves based on changes between the object's original and evaluated surface mesh")
DefNode(GeometryNode, GEO_NODE_DELETE_GEOMETRY, def_geo_delete_geometry, "DELETE_GEOMETRY", DeleteGeometry, "Delete Geometry", "Remove selected elements of a geometry")
DefNode(GeometryNode, GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME, def_geo_distribute_points_in_volume, "DISTRIBUTE_POINTS_IN_VOLUME", DistributePointsInVolume, "Distribute Points In Volume", "Generate points inside a volume")
DefNode(GeometryNode, GEO_NODE_DISTRIBUTE_POINTS_ON_FACES, def_geo_distribute_points_on_faces, "DISTRIBUTE_POINTS_ON_FACES", DistributePointsOnFaces, "Distribute Points on Faces", "Generate points spread out on the surface of a mesh")
DefNode(GeometryNode, GEO_NODE_DUAL_MESH, 0, "DUAL_MESH", DualMesh, "Dual Mesh", "Convert Faces into vertices and vertices into faces")
DefNode(GeometryNode, GEO_NODE_DUPLICATE_ELEMENTS, def_geo_duplicate_elements, "DUPLICATE_ELEMENTS", DuplicateElements, "Duplicate Elements", "Generate an arbitrary number copies of each selected input element")

View File

@ -59,6 +59,7 @@ set(SRC
nodes/node_geo_curve_trim.cc
nodes/node_geo_deform_curves_on_surface.cc
nodes/node_geo_delete_geometry.cc
nodes/node_geo_distribute_points_in_volume.cc
nodes/node_geo_distribute_points_on_faces.cc
nodes/node_geo_dual_mesh.cc
nodes/node_geo_duplicate_elements.cc

View File

@ -0,0 +1,285 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifdef WITH_OPENVDB
# include <openvdb/openvdb.h>
# include <openvdb/tools/Interpolation.h>
# include <openvdb/tools/PointScatter.h>
#endif
#include "DNA_node_types.h"
#include "DNA_pointcloud_types.h"
#include "BKE_pointcloud.h"
#include "BKE_volume.h"
#include "UI_interface.h"
#include "UI_resources.h"
#include "node_geometry_util.hh"
namespace blender::nodes {
NODE_STORAGE_FUNCS(NodeGeometryDistributePointsInVolume)
static void geo_node_distribute_points_in_volume_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Geometry>(N_("Volume")).supported_type(GEO_COMPONENT_TYPE_VOLUME);
b.add_input<decl::Float>(N_("Density"))
.default_value(1.0f)
.min(0.0f)
.max(100000.0f)
.subtype(PROP_NONE)
.description(N_("Number of points to sample per unit volume"));
b.add_input<decl::Int>(N_("Seed"))
.min(-10000)
.max(10000)
.description(N_("Seed used by the random number generator to generate random points"));
b.add_input<decl::Vector>(N_("Spacing"))
.default_value({0.3, 0.3, 0.3})
.min(0.0001f)
.subtype(PROP_XYZ)
.description(N_("Spacing between grid points"));
b.add_input<decl::Float>(N_("Threshold"))
.default_value(0.1f)
.min(0.0f)
.max(FLT_MAX)
.description(N_("Minimum density of a volume cell to contain a grid point"));
b.add_output<decl::Geometry>(N_("Points"));
}
static void geo_node_distribute_points_in_volume_layout(uiLayout *layout,
bContext *UNUSED(C),
PointerRNA *ptr)
{
uiItemR(layout, ptr, "mode", 0, "", ICON_NONE);
}
static void node_distribute_points_in_volume_init(bNodeTree *UNUSED(ntree), bNode *node)
{
NodeGeometryDistributePointsInVolume *data = MEM_cnew<NodeGeometryDistributePointsInVolume>(
__func__);
data->mode = GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM;
node->storage = data;
}
static void node_distribute_points_in_volume_update(bNodeTree *ntree, bNode *node)
{
const NodeGeometryDistributePointsInVolume &storage = node_storage(*node);
GeometryNodeDistributePointsInVolumeMode mode =
static_cast<GeometryNodeDistributePointsInVolumeMode>(storage.mode);
bNodeSocket *sock_density = ((bNodeSocket *)(node->inputs.first))->next;
bNodeSocket *sock_seed = sock_density->next;
bNodeSocket *sock_spacing = sock_seed->next;
bNodeSocket *sock_threshold = sock_spacing->next;
nodeSetSocketAvailability(
ntree, sock_density, mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM);
nodeSetSocketAvailability(
ntree, sock_seed, mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM);
nodeSetSocketAvailability(
ntree, sock_spacing, mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID);
nodeSetSocketAvailability(
ntree, sock_threshold, mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID);
}
#ifdef WITH_OPENVDB
/* Implements the interface required by #openvdb::tools::NonUniformPointScatter. */
class PositionsVDBWrapper {
private:
float3 offset_fix_;
Vector<float3> &vector_;
public:
PositionsVDBWrapper(Vector<float3> &vector, const float3 offset_fix)
: offset_fix_(offset_fix), vector_(vector)
{
}
PositionsVDBWrapper(const PositionsVDBWrapper &wrapper) = default;
void add(const openvdb::Vec3R &pos)
{
vector_.append((float3((float)pos[0], (float)pos[1], (float)pos[2]) + offset_fix_));
}
};
/* Use #std::mt19937 as a random number generator,
* it has a very long period and thus there should be no visible patterns in the generated points.
*/
using RNGType = std::mt19937;
/* Non-uniform scatter allows the amount of points to be scaled with the volume's density. */
using NonUniformPointScatterVDB =
openvdb::tools::NonUniformPointScatter<PositionsVDBWrapper, RNGType>;
static void point_scatter_density_random(const openvdb::FloatGrid &grid,
const float density,
const int seed,
Vector<float3> &r_positions)
{
/* Offset points by half a voxel so that grid points are aligned with world grid points. */
const float3 offset_fix = {0.5f * (float)grid.voxelSize().x(),
0.5f * (float)grid.voxelSize().y(),
0.5f * (float)grid.voxelSize().z()};
/* Setup and call into OpenVDB's point scatter API. */
PositionsVDBWrapper vdb_position_wrapper = PositionsVDBWrapper(r_positions, offset_fix);
RNGType random_generator(seed);
NonUniformPointScatterVDB point_scatter(vdb_position_wrapper, density, random_generator);
point_scatter(grid);
}
static void point_scatter_density_grid(const openvdb::FloatGrid &grid,
const float3 spacing,
const float threshold,
Vector<float3> &r_positions)
{
const openvdb::Vec3d half_voxel(0.5, 0.5, 0.5);
const openvdb::Vec3d voxel_spacing((double)spacing.x / grid.voxelSize().x(),
(double)spacing.y / grid.voxelSize().y(),
(double)spacing.z / grid.voxelSize().z());
/* Abort if spacing is zero. */
const double min_spacing = std::min(voxel_spacing.x(),
std::min(voxel_spacing.y(), voxel_spacing.z()));
if (std::abs(min_spacing) < 0.0001) {
return;
}
/* Iterate through tiles and voxels on the grid. */
for (openvdb::FloatGrid::ValueOnCIter cell = grid.cbeginValueOn(); cell; ++cell) {
/* Check if the cell's value meets the minimum threshold. */
if (cell.getValue() < threshold) {
continue;
}
/* Compute the bounding box of each tile/voxel. */
const openvdb::CoordBBox bbox = cell.getBoundingBox();
const openvdb::Vec3d box_min = bbox.min().asVec3d() - half_voxel;
const openvdb::Vec3d box_max = bbox.max().asVec3d() + half_voxel;
/* Pick a starting point rounded up to the nearest possible point. */
double abs_spacing_x = std::abs(voxel_spacing.x());
double abs_spacing_y = std::abs(voxel_spacing.y());
double abs_spacing_z = std::abs(voxel_spacing.z());
const openvdb::Vec3d start(ceil(box_min.x() / abs_spacing_x) * abs_spacing_x,
ceil(box_min.y() / abs_spacing_y) * abs_spacing_y,
ceil(box_min.z() / abs_spacing_z) * abs_spacing_z);
/* Iterate through all possible points in box. */
for (double x = start.x(); x < box_max.x(); x += abs_spacing_x) {
for (double y = start.y(); y < box_max.y(); y += abs_spacing_y) {
for (double z = start.z(); z < box_max.z(); z += abs_spacing_z) {
/* Transform with grid matrix and add point. */
const openvdb::Vec3d idx_pos(x, y, z);
const openvdb::Vec3d local_pos = grid.indexToWorld(idx_pos + half_voxel);
r_positions.append({(float)local_pos.x(), (float)local_pos.y(), (float)local_pos.z()});
}
}
}
}
}
#endif /* WITH_OPENVDB */
static void geo_node_distribute_points_in_volume_exec(GeoNodeExecParams params)
{
GeometrySet geometry_set_in = params.extract_input<GeometrySet>("Volume");
const NodeGeometryDistributePointsInVolume &storage = node_storage(params.node());
const GeometryNodeDistributePointsInVolumeMode mode =
static_cast<GeometryNodeDistributePointsInVolumeMode>(storage.mode);
#ifdef WITH_OPENVDB
float density;
int seed;
float3 spacing{0, 0, 0};
float threshold;
if (mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM) {
density = params.extract_input<float>("Density");
seed = params.extract_input<int>("Seed");
}
else if (mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID) {
spacing = params.extract_input<float3>("Spacing");
threshold = params.extract_input<float>("Threshold");
}
geometry_set_in.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (!geometry_set.has_volume()) {
geometry_set.keep_only({GEO_COMPONENT_TYPE_POINT_CLOUD, GEO_COMPONENT_TYPE_INSTANCES});
return;
}
const VolumeComponent *component = geometry_set.get_component_for_read<VolumeComponent>();
const Volume *volume = component->get_for_read();
Vector<float3> positions;
for (const int i : IndexRange(BKE_volume_num_grids(volume))) {
const VolumeGrid *volume_grid = BKE_volume_grid_get_for_read(volume, i);
if (volume_grid == nullptr) {
continue;
}
openvdb::GridBase::ConstPtr base_grid = BKE_volume_grid_openvdb_for_read(volume,
volume_grid);
if (!base_grid) {
continue;
}
if (!base_grid->isType<openvdb::FloatGrid>()) {
continue;
}
const openvdb::FloatGrid::ConstPtr grid = openvdb::gridConstPtrCast<openvdb::FloatGrid>(
base_grid);
if (mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_RANDOM) {
point_scatter_density_random(*grid, density, seed, positions);
}
else if (mode == GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME_DENSITY_GRID) {
point_scatter_density_grid(*grid, spacing, threshold, positions);
}
}
PointCloud *pointcloud = BKE_pointcloud_new_nomain(positions.size());
bke::MutableAttributeAccessor point_attributes = pointcloud->attributes_for_write();
bke::SpanAttributeWriter<float3> point_positions =
point_attributes.lookup_or_add_for_write_only_span<float3>("position", ATTR_DOMAIN_POINT);
bke::SpanAttributeWriter<float> point_radii =
point_attributes.lookup_or_add_for_write_only_span<float>("radius", ATTR_DOMAIN_POINT);
point_positions.span.copy_from(positions);
point_radii.span.fill(0.05f);
point_positions.finish();
point_radii.finish();
geometry_set.replace_pointcloud(pointcloud);
geometry_set.keep_only({GEO_COMPONENT_TYPE_POINT_CLOUD, GEO_COMPONENT_TYPE_INSTANCES});
});
params.set_output("Points", std::move(geometry_set_in));
#else /* WITH_OPENVDB */
params.error_message_add(NodeWarningType::Error, TIP_("Blender is compiled without OpenVDB"));
#endif /* !WITH_OPENVDB */
}
} // namespace blender::nodes
void register_node_type_geo_distribute_points_in_volume()
{
static bNodeType ntype;
geo_node_type_base(&ntype,
GEO_NODE_DISTRIBUTE_POINTS_IN_VOLUME,
"Distribute Points in Volume",
NODE_CLASS_GEOMETRY);
node_type_storage(&ntype,
"NodeGeometryDistributePointsInVolume",
node_free_standard_storage,
node_copy_standard_storage);
node_type_init(&ntype, blender::nodes::node_distribute_points_in_volume_init);
node_type_update(&ntype, blender::nodes::node_distribute_points_in_volume_update);
node_type_size(&ntype, 170, 100, 320);
ntype.declare = blender::nodes::geo_node_distribute_points_in_volume_declare;
ntype.geometry_node_execute = blender::nodes::geo_node_distribute_points_in_volume_exec;
ntype.draw_buttons = blender::nodes::geo_node_distribute_points_in_volume_layout;
nodeRegisterType(&ntype);
}