Curves: Fix bounding box ignores radius, add option to geometry node

Unlike the legacy type, the radius isn't included in the bounds for the new
curves type. This hasn't been obvious because the drawing is quite broken
and doesn't use the radius properly.

This commit adds a separate cache for the bounds with the radius, which
is now used by default. The old cache is kept around for backward
compatibility in the bounding box geometry node, where a new
"Use Radius" option accesses the old behavior.

Pull Request: https://projects.blender.org/blender/blender/pulls/135584
This commit is contained in:
Hans Goudey
2025-03-07 17:38:29 +01:00
committed by Hans Goudey
parent 32d49541c0
commit 06f6d77979
13 changed files with 138 additions and 45 deletions

View File

@@ -104,6 +104,7 @@ class CurvesGeometryRuntime {
* See #SharedCache comments.
*/
mutable SharedCache<Bounds<float3>> bounds_cache;
mutable SharedCache<Bounds<float3>> bounds_with_radius_cache;
/**
* Cache of lengths along each evaluated curve for each evaluated point. If a curve is
@@ -304,7 +305,7 @@ class CurvesGeometry : public ::CurvesGeometry {
/**
* The largest and smallest position values of evaluated points.
*/
std::optional<Bounds<float3>> bounds_min_max() const;
std::optional<Bounds<float3>> bounds_min_max(bool use_radius = true) const;
void count_memory(MemoryCounter &memory) const;

View File

@@ -224,7 +224,7 @@ struct GeometrySet {
*/
Vector<const GeometryComponent *> get_components() const;
std::optional<Bounds<float3>> compute_boundbox_without_instances() const;
std::optional<Bounds<float3>> compute_boundbox_without_instances(bool use_radius = true) const;
friend std::ostream &operator<<(std::ostream &stream, const GeometrySet &geometry_set);

View File

@@ -35,6 +35,7 @@ struct PointCloudRuntime {
* See #SharedCache comments.
*/
mutable SharedCache<Bounds<float3>> bounds_cache;
mutable SharedCache<Bounds<float3>> bounds_with_radius_cache;
/** Stores weak references to material data blocks. */
std::unique_ptr<bake::BakeMaterialsList> bake_materials;

View File

@@ -115,6 +115,7 @@ CurvesGeometry::CurvesGeometry(const CurvesGeometry &other)
other.runtime->nurbs_basis_cache,
other.runtime->evaluated_position_cache,
other.runtime->bounds_cache,
other.runtime->bounds_with_radius_cache,
other.runtime->evaluated_length_cache,
other.runtime->evaluated_tangent_cache,
other.runtime->evaluated_normal_cache,
@@ -1078,6 +1079,7 @@ void CurvesGeometry::tag_positions_changed()
this->runtime->evaluated_normal_cache.tag_dirty();
this->runtime->evaluated_length_cache.tag_dirty();
this->runtime->bounds_cache.tag_dirty();
this->runtime->bounds_with_radius_cache.tag_dirty();
}
void CurvesGeometry::tag_topology_changed()
{
@@ -1091,7 +1093,10 @@ void CurvesGeometry::tag_normals_changed()
{
this->runtime->evaluated_normal_cache.tag_dirty();
}
void CurvesGeometry::tag_radii_changed() {}
void CurvesGeometry::tag_radii_changed()
{
this->runtime->bounds_with_radius_cache.tag_dirty();
}
void CurvesGeometry::tag_material_index_changed()
{
this->runtime->max_material_index_cache.tag_dirty();
@@ -1201,14 +1206,37 @@ void CurvesGeometry::transform(const float4x4 &matrix)
this->tag_positions_changed();
}
std::optional<Bounds<float3>> CurvesGeometry::bounds_min_max() const
std::optional<Bounds<float3>> CurvesGeometry::bounds_min_max(const bool use_radius) const
{
if (this->is_empty()) {
return std::nullopt;
}
this->runtime->bounds_cache.ensure(
[&](Bounds<float3> &r_bounds) { r_bounds = *bounds::min_max(this->evaluated_positions()); });
return this->runtime->bounds_cache.data();
if (use_radius) {
this->runtime->bounds_with_radius_cache.ensure([&](Bounds<float3> &r_bounds) {
const VArray<float> radius = this->radius();
if (const std::optional radius_single = radius.get_if_single()) {
r_bounds = *this->bounds_min_max(false);
r_bounds.pad(*radius_single);
return;
}
const Span radius_span = radius.get_internal_span();
if (this->is_single_type(CURVE_TYPE_POLY)) {
r_bounds = *bounds::min_max_with_radii(this->positions(), radius_span);
return;
}
Array<float> radii_eval(this->evaluated_points_num());
this->ensure_can_interpolate_to_evaluated();
this->interpolate_to_evaluated(radius_span, radii_eval.as_mutable_span());
r_bounds = *bounds::min_max_with_radii(this->evaluated_positions(), radii_eval.as_span());
});
}
else {
this->runtime->bounds_cache.ensure([&](Bounds<float3> &r_bounds) {
r_bounds = *bounds::min_max(this->evaluated_positions());
});
}
return use_radius ? this->runtime->bounds_with_radius_cache.data() :
this->runtime->bounds_cache.data();
}
std::optional<int> CurvesGeometry::material_index_max() const

View File

@@ -193,11 +193,12 @@ Vector<const GeometryComponent *> GeometrySet::get_components() const
return components;
}
std::optional<Bounds<float3>> GeometrySet::compute_boundbox_without_instances() const
std::optional<Bounds<float3>> GeometrySet::compute_boundbox_without_instances(
const bool use_radius) const
{
std::optional<Bounds<float3>> bounds;
if (const PointCloud *pointcloud = this->get_pointcloud()) {
bounds = bounds::merge(bounds, pointcloud->bounds_min_max());
bounds = bounds::merge(bounds, pointcloud->bounds_min_max(use_radius));
}
if (const Mesh *mesh = this->get_mesh()) {
bounds = bounds::merge(bounds, mesh->bounds_min_max());
@@ -206,10 +207,10 @@ std::optional<Bounds<float3>> GeometrySet::compute_boundbox_without_instances()
bounds = bounds::merge(bounds, BKE_volume_min_max(volume));
}
if (const Curves *curves_id = this->get_curves()) {
bounds = bounds::merge(bounds, curves_id->geometry.wrap().bounds_min_max());
bounds = bounds::merge(bounds, curves_id->geometry.wrap().bounds_min_max(use_radius));
}
if (const GreasePencil *grease_pencil = this->get_grease_pencil()) {
bounds = bounds::merge(bounds, grease_pencil->bounds_min_max_eval());
bounds = bounds::merge(bounds, grease_pencil->bounds_min_max_eval(use_radius));
}
return bounds;
}

View File

@@ -3285,7 +3285,8 @@ static void transform_positions(const Span<blender::float3> src,
});
}
std::optional<blender::Bounds<blender::float3>> GreasePencil::bounds_min_max(const int frame) const
std::optional<blender::Bounds<blender::float3>> GreasePencil::bounds_min_max(
const int frame, const bool use_radius) const
{
using namespace blender;
std::optional<Bounds<float3>> bounds;
@@ -3296,20 +3297,53 @@ std::optional<blender::Bounds<blender::float3>> GreasePencil::bounds_min_max(con
if (!layer.is_visible()) {
continue;
}
if (const bke::greasepencil::Drawing *drawing = this->get_drawing_at(layer, frame)) {
const bke::CurvesGeometry &curves = drawing->strokes();
Array<float3> world_pos(curves.evaluated_positions().size());
transform_positions(curves.evaluated_positions(), layer_to_object, world_pos);
bounds = bounds::merge(bounds, bounds::min_max(world_pos.as_span()));
const bke::greasepencil::Drawing *drawing = this->get_drawing_at(layer, frame);
if (!drawing) {
continue;
}
const bke::CurvesGeometry &curves = drawing->strokes();
if (curves.is_empty()) {
continue;
}
if (layer_to_object == float4x4::identity()) {
bounds = bounds::merge(bounds, curves.bounds_min_max(use_radius));
continue;
}
const VArray<float> radius = curves.radius();
Array<float3> positions_world(curves.evaluated_points_num());
transform_positions(curves.evaluated_positions(), layer_to_object, positions_world);
if (!use_radius) {
const Bounds<float3> drawing_bounds = *bounds::min_max(positions_world.as_span());
bounds = bounds::merge(bounds, drawing_bounds);
continue;
}
if (const std::optional radius_single = radius.get_if_single()) {
Bounds<float3> drawing_bounds = *curves.bounds_min_max(false);
drawing_bounds.pad(*radius_single);
bounds = bounds::merge(bounds, drawing_bounds);
continue;
}
const Span radius_span = radius.get_internal_span();
if (curves.is_single_type(CURVE_TYPE_POLY)) {
const Bounds<float3> drawing_bounds = *bounds::min_max_with_radii(positions_world.as_span(),
radius_span);
bounds = bounds::merge(bounds, drawing_bounds);
continue;
}
curves.ensure_can_interpolate_to_evaluated();
Array<float> radii_eval(curves.evaluated_points_num());
curves.interpolate_to_evaluated(radius_span, radii_eval.as_mutable_span());
const Bounds<float3> drawing_bounds = *bounds::min_max_with_radii(positions_world.as_span(),
radii_eval.as_span());
bounds = bounds::merge(bounds, drawing_bounds);
}
return bounds;
}
std::optional<blender::Bounds<blender::float3>> GreasePencil::bounds_min_max_eval() const
std::optional<blender::Bounds<blender::float3>> GreasePencil::bounds_min_max_eval(
const bool use_radius) const
{
return this->bounds_min_max(this->runtime->eval_frame);
return this->bounds_min_max(this->runtime->eval_frame, use_radius);
}
void GreasePencil::count_memory(blender::MemoryCounter &memory) const

View File

@@ -85,6 +85,8 @@ static void pointcloud_copy_data(Main * /*bmain*/,
pointcloud_dst->runtime = new blender::bke::PointCloudRuntime();
pointcloud_dst->runtime->bounds_cache = pointcloud_src->runtime->bounds_cache;
pointcloud_dst->runtime->bounds_with_radius_cache =
pointcloud_src->runtime->bounds_with_radius_cache;
pointcloud_dst->runtime->bvh_cache = pointcloud_src->runtime->bvh_cache;
if (pointcloud_src->runtime->bake_materials) {
pointcloud_dst->runtime->bake_materials =
@@ -317,25 +319,31 @@ void BKE_pointcloud_nomain_to_pointcloud(PointCloud *pointcloud_src, PointCloud
BKE_id_free(nullptr, pointcloud_src);
}
std::optional<blender::Bounds<blender::float3>> PointCloud::bounds_min_max() const
std::optional<blender::Bounds<float3>> PointCloud::bounds_min_max(const bool use_radius) const
{
using namespace blender;
using namespace blender::bke;
if (this->totpoint == 0) {
return std::nullopt;
}
this->runtime->bounds_cache.ensure([&](Bounds<float3> &r_bounds) {
const AttributeAccessor attributes = this->attributes();
const Span<float3> positions = this->positions();
if (attributes.contains(ATTR_RADIUS)) {
const VArraySpan radii = *attributes.lookup<float>(ATTR_RADIUS);
r_bounds = *bounds::min_max_with_radii(positions, radii);
}
else {
r_bounds = *bounds::min_max(positions);
}
});
return this->runtime->bounds_cache.data();
if (use_radius) {
this->runtime->bounds_with_radius_cache.ensure([&](Bounds<float3> &r_bounds) {
const VArray<float> radius = this->radius();
if (const std::optional radius_single = radius.get_if_single()) {
r_bounds = *this->bounds_min_max(false);
r_bounds.pad(*radius_single);
return;
}
const Span radius_span = radius.get_internal_span();
r_bounds = *bounds::min_max_with_radii(this->positions(), radius_span);
});
}
else {
this->runtime->bounds_cache.ensure(
[&](Bounds<float3> &r_bounds) { r_bounds = *bounds::min_max(this->positions()); });
}
return use_radius ? this->runtime->bounds_with_radius_cache.data() :
this->runtime->bounds_cache.data();
}
std::optional<int> PointCloud::material_index_max() const
@@ -470,12 +478,13 @@ void BKE_pointcloud_data_update(Depsgraph *depsgraph, Scene *scene, Object *obje
void PointCloud::tag_positions_changed()
{
this->runtime->bounds_cache.tag_dirty();
this->runtime->bounds_with_radius_cache.tag_dirty();
this->runtime->bvh_cache.tag_dirty();
}
void PointCloud::tag_radii_changed()
{
this->runtime->bounds_cache.tag_dirty();
this->runtime->bounds_with_radius_cache.tag_dirty();
}
/* Draw Cache */

View File

@@ -43,6 +43,13 @@ template<typename T>
return std::nullopt;
}
template<typename T>
[[nodiscard]] inline std::optional<Bounds<T>> merge(const std::optional<Bounds<T>> &a,
const Bounds<T> &b)
{
return merge(a, std::optional<Bounds<T>>(b));
}
template<typename T>
[[nodiscard]] inline std::optional<Bounds<T>> min_max(const std::optional<Bounds<T>> &a,
const T &b)

View File

@@ -40,8 +40,9 @@ struct StepObject {
UndoRefID_Object obedit_ref = {};
CustomData custom_data = {};
int totpoint = 0;
/* Store the bounds cache because it's small. */
/* Store the bounds caches because they are small. */
SharedCache<Bounds<float3>> bounds_cache;
SharedCache<Bounds<float3>> bounds_with_radius_cache;
};
struct PointCloudUndoStep {
@@ -71,6 +72,7 @@ static bool step_encode(bContext *C, Main *bmain, UndoStep *us_p)
CustomData_init_from(
&pointcloud.pdata, &object.custom_data, CD_MASK_ALL, pointcloud.totpoint);
object.bounds_cache = pointcloud.runtime->bounds_cache;
object.bounds_with_radius_cache = pointcloud.runtime->bounds_with_radius_cache;
object.totpoint = pointcloud.totpoint;
}
});
@@ -107,6 +109,7 @@ static void step_decode(
CustomData_init_from(&object.custom_data, &pointcloud.pdata, CD_MASK_ALL, object.totpoint);
pointcloud.totpoint = object.totpoint;
pointcloud.runtime->bounds_cache = object.bounds_cache;
pointcloud.runtime->bounds_with_radius_cache = object.bounds_with_radius_cache;
if (positions_changed) {
pointcloud.runtime->bvh_cache.tag_dirty();
}

View File

@@ -718,8 +718,10 @@ typedef struct GreasePencil {
blender::bke::greasepencil::Drawing *get_eval_drawing(
const blender::bke::greasepencil::Layer &layer);
std::optional<blender::Bounds<blender::float3>> bounds_min_max(int frame) const;
std::optional<blender::Bounds<blender::float3>> bounds_min_max_eval() const;
std::optional<blender::Bounds<blender::float3>> bounds_min_max(int frame,
bool use_radius = true) const;
std::optional<blender::Bounds<blender::float3>> bounds_min_max_eval(
bool use_radius = true) const;
blender::bke::AttributeAccessor attributes() const;
blender::bke::MutableAttributeAccessor attributes_for_write();

View File

@@ -69,7 +69,7 @@ typedef struct PointCloud {
void tag_positions_changed();
void tag_radii_changed();
std::optional<blender::Bounds<blender::float3>> bounds_min_max() const;
std::optional<blender::Bounds<blender::float3>> bounds_min_max(bool use_radius = true) const;
/** Get the largest material index used by the point-cloud or `nullopt` if it is empty. */
std::optional<int> material_index_max() const;

View File

@@ -12,6 +12,11 @@ namespace blender::nodes::node_geo_bounding_box_cc {
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Geometry>("Geometry");
b.add_input<decl::Bool>("Use Radius")
.default_value(true)
.description(
"For curves, point clouds, and Grease Pencil, take the radius attribute into account "
"when computing the bounds.");
b.add_output<decl::Geometry>("Bounding Box");
b.add_output<decl::Vector>("Min");
b.add_output<decl::Vector>("Max");
@@ -20,10 +25,12 @@ static void node_declare(NodeDeclarationBuilder &b)
static void node_geo_exec(GeoNodeExecParams params)
{
GeometrySet geometry_set = params.extract_input<GeometrySet>("Geometry");
const bool use_radius = params.extract_input<bool>("Use Radius");
/* Compute the min and max of all realized geometry for the two
* vector outputs, which are only meant to consider real geometry. */
const std::optional<Bounds<float3>> bounds = geometry_set.compute_boundbox_without_instances();
const std::optional<Bounds<float3>> bounds = geometry_set.compute_boundbox_without_instances(
use_radius);
if (!bounds) {
params.set_output("Min", float3(0));
params.set_output("Max", float3(0));
@@ -45,7 +52,7 @@ static void node_geo_exec(GeoNodeExecParams params)
sub_bounds = bounds;
}
else {
sub_bounds = sub_geometry.compute_boundbox_without_instances();
sub_bounds = sub_geometry.compute_boundbox_without_instances(use_radius);
}
if (!sub_bounds) {

View File

@@ -898,23 +898,23 @@ class USDExportTest(AbstractUSDTest):
# Contains 3 CatmullRom curves
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/Cube/Curves/Curves"))
check_basis_curve(
curve, "catmullRom", "cubic", "pinned", [8, 8, 8], [[-0.3784, -0.0866, 1], [0.2714, -0.0488, 1.3]])
curve, "catmullRom", "cubic", "pinned", [8, 8, 8], [[-0.3884, -0.0966, 0.99], [0.2814, -0.0388, 1.31]])
# Contains 1 Bezier curve
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCurve/BezierCurve"))
check_basis_curve(curve, "bezier", "cubic", "nonperiodic", [7], [[-2.644, -0.0777, 0], [1, 0.9815, 0]])
check_basis_curve(curve, "bezier", "cubic", "nonperiodic", [7], [[-3.644, -1.0777, -1.0], [2.0, 1.9815, 1.0]])
# Contains 1 Bezier curve
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCircle/BezierCircle"))
check_basis_curve(curve, "bezier", "cubic", "periodic", [12], [[-1, -1, 0], [1, 1, 0]])
check_basis_curve(curve, "bezier", "cubic", "periodic", [12], [[-2.0, -2.0, -1.0], [2.0, 2.0, 1.0]])
# Contains 2 NURBS curves
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCurve/NurbsCurve"))
check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-0.75, -1.6898, -0.0117], [2.0896, 0.9583, 0.0293]])
check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-1.75, -2.6898, -1.0117], [3.0896, 1.9583, 1.0293]])
# Contains 1 NURBS curve
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCircle/NurbsCircle"))
check_nurbs_curve(curve, True, [3], [8], 13, [[-1, -1, 0], [1, 1, 0]])
check_nurbs_curve(curve, True, [3], [8], 13, [[-2.0, -2.0, -1.0], [2.0, 2.0, 1.0]])
def test_export_animation(self):
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))