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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user