This repository has been archived on 2023-10-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
blender-archive/source/blender/blenkernel/intern/curves_geometry.cc

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1566 lines
59 KiB
C++
Raw Normal View History

2022-02-17 16:47:37 +11:00
/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*/
#include <mutex>
#include <utility>
#include "MEM_guardedalloc.h"
#include "BLI_array_utils.hh"
#include "BLI_bounds.hh"
#include "BLI_index_mask_ops.hh"
#include "BLI_length_parameterize.hh"
#include "BLI_math_rotation_legacy.hh"
#include "BLI_task.hh"
#include "DNA_curves_types.h"
#include "BKE_attribute_math.hh"
#include "BKE_curves.hh"
#include "BKE_curves_utils.hh"
namespace blender::bke {
static const std::string ATTR_POSITION = "position";
static const std::string ATTR_RADIUS = "radius";
static const std::string ATTR_TILT = "tilt";
static const std::string ATTR_CURVE_TYPE = "curve_type";
static const std::string ATTR_CYCLIC = "cyclic";
static const std::string ATTR_RESOLUTION = "resolution";
static const std::string ATTR_NORMAL_MODE = "normal_mode";
static const std::string ATTR_HANDLE_TYPE_LEFT = "handle_type_left";
static const std::string ATTR_HANDLE_TYPE_RIGHT = "handle_type_right";
static const std::string ATTR_HANDLE_POSITION_LEFT = "handle_left";
static const std::string ATTR_HANDLE_POSITION_RIGHT = "handle_right";
static const std::string ATTR_NURBS_ORDER = "nurbs_order";
static const std::string ATTR_NURBS_WEIGHT = "nurbs_weight";
static const std::string ATTR_NURBS_KNOTS_MODE = "knots_mode";
static const std::string ATTR_SURFACE_UV_COORDINATE = "surface_uv_coordinate";
/* -------------------------------------------------------------------- */
/** \name Constructors/Destructor
* \{ */
CurvesGeometry::CurvesGeometry() : CurvesGeometry(0, 0)
{
}
CurvesGeometry::CurvesGeometry(const int point_num, const int curve_num)
{
this->point_num = point_num;
this->curve_num = curve_num;
CustomData_reset(&this->point_data);
CustomData_reset(&this->curve_data);
CustomData_add_layer_named(&this->point_data,
CD_PROP_FLOAT3,
CD_CONSTRUCT,
nullptr,
this->point_num,
ATTR_POSITION.c_str());
this->curve_offsets = (int *)MEM_malloc_arrayN(this->curve_num + 1, sizeof(int), __func__);
#ifdef DEBUG
this->offsets_for_write().fill(-1);
#endif
this->offsets_for_write().first() = 0;
this->runtime = MEM_new<CurvesGeometryRuntime>(__func__);
/* Fill the type counts with the default so they're in a valid state. */
this->runtime->type_counts[CURVE_TYPE_CATMULL_ROM] = curve_num;
}
/**
* \note Expects `dst` to be initialized, since the original attributes must be freed.
*/
static void copy_curves_geometry(CurvesGeometry &dst, const CurvesGeometry &src)
{
CustomData_free(&dst.point_data, dst.point_num);
CustomData_free(&dst.curve_data, dst.curve_num);
dst.point_num = src.point_num;
dst.curve_num = src.curve_num;
CustomData_copy(&src.point_data, &dst.point_data, CD_MASK_ALL, CD_DUPLICATE, dst.point_num);
CustomData_copy(&src.curve_data, &dst.curve_data, CD_MASK_ALL, CD_DUPLICATE, dst.curve_num);
MEM_SAFE_FREE(dst.curve_offsets);
dst.curve_offsets = (int *)MEM_malloc_arrayN(dst.point_num + 1, sizeof(int), __func__);
dst.offsets_for_write().copy_from(src.offsets());
dst.tag_topology_changed();
/* Though type counts are a cache, they must be copied because they are calculated eagerly. */
dst.runtime->type_counts = src.runtime->type_counts;
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
dst.runtime->bounds_cache = src.runtime->bounds_cache;
}
CurvesGeometry::CurvesGeometry(const CurvesGeometry &other)
: CurvesGeometry(other.point_num, other.curve_num)
{
copy_curves_geometry(*this, other);
}
CurvesGeometry &CurvesGeometry::operator=(const CurvesGeometry &other)
{
if (this != &other) {
copy_curves_geometry(*this, other);
}
return *this;
}
/* The source should be empty, but in a valid state so that using it further will work. */
static void move_curves_geometry(CurvesGeometry &dst, CurvesGeometry &src)
{
dst.point_num = src.point_num;
std::swap(dst.point_data, src.point_data);
CustomData_free(&src.point_data, src.point_num);
src.point_num = 0;
dst.curve_num = src.curve_num;
std::swap(dst.curve_data, src.curve_data);
CustomData_free(&src.curve_data, src.curve_num);
src.curve_num = 0;
std::swap(dst.curve_offsets, src.curve_offsets);
MEM_SAFE_FREE(src.curve_offsets);
std::swap(dst.runtime, src.runtime);
}
CurvesGeometry::CurvesGeometry(CurvesGeometry &&other)
: CurvesGeometry(other.point_num, other.curve_num)
{
move_curves_geometry(*this, other);
}
CurvesGeometry &CurvesGeometry::operator=(CurvesGeometry &&other)
{
if (this != &other) {
move_curves_geometry(*this, other);
}
return *this;
}
CurvesGeometry::~CurvesGeometry()
{
CustomData_free(&this->point_data, this->point_num);
CustomData_free(&this->curve_data, this->curve_num);
MEM_SAFE_FREE(this->curve_offsets);
MEM_delete(this->runtime);
this->runtime = nullptr;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Accessors
* \{ */
static int domain_num(const CurvesGeometry &curves, const eAttrDomain domain)
{
return domain == ATTR_DOMAIN_POINT ? curves.points_num() : curves.curves_num();
}
static CustomData &domain_custom_data(CurvesGeometry &curves, const eAttrDomain domain)
{
return domain == ATTR_DOMAIN_POINT ? curves.point_data : curves.curve_data;
}
static const CustomData &domain_custom_data(const CurvesGeometry &curves, const eAttrDomain domain)
{
return domain == ATTR_DOMAIN_POINT ? curves.point_data : curves.curve_data;
}
template<typename T>
static VArray<T> get_varray_attribute(const CurvesGeometry &curves,
const eAttrDomain domain,
const StringRefNull name,
const T default_value)
{
const int num = domain_num(curves, domain);
const eCustomDataType type = cpp_type_to_custom_data_type(CPPType::get<T>());
const CustomData &custom_data = domain_custom_data(curves, domain);
const T *data = (const T *)CustomData_get_layer_named(&custom_data, type, name.c_str());
if (data != nullptr) {
return VArray<T>::ForSpan(Span<T>(data, num));
}
return VArray<T>::ForSingle(default_value, num);
}
template<typename T>
static Span<T> get_span_attribute(const CurvesGeometry &curves,
const eAttrDomain domain,
const StringRefNull name)
{
const int num = domain_num(curves, domain);
const CustomData &custom_data = domain_custom_data(curves, domain);
const eCustomDataType type = cpp_type_to_custom_data_type(CPPType::get<T>());
T *data = (T *)CustomData_get_layer_named(&custom_data, type, name.c_str());
if (data == nullptr) {
return {};
}
return {data, num};
}
template<typename T>
static MutableSpan<T> get_mutable_attribute(CurvesGeometry &curves,
const eAttrDomain domain,
const StringRefNull name,
const T default_value = T())
{
const int num = domain_num(curves, domain);
const eCustomDataType type = cpp_type_to_custom_data_type(CPPType::get<T>());
CustomData &custom_data = domain_custom_data(curves, domain);
T *data = (T *)CustomData_get_layer_named_for_write(&custom_data, type, name.c_str(), num);
if (data != nullptr) {
return {data, num};
}
data = (T *)CustomData_add_layer_named(
2022-08-30 14:54:53 -05:00
&custom_data, type, CD_SET_DEFAULT, nullptr, num, name.c_str());
MutableSpan<T> span = {data, num};
if (num > 0 && span.first() != default_value) {
span.fill(default_value);
}
return span;
}
VArray<int8_t> CurvesGeometry::curve_types() const
{
return get_varray_attribute<int8_t>(
*this, ATTR_DOMAIN_CURVE, ATTR_CURVE_TYPE, CURVE_TYPE_CATMULL_ROM);
}
MutableSpan<int8_t> CurvesGeometry::curve_types_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_CURVE_TYPE);
}
void CurvesGeometry::fill_curve_types(const CurveType type)
{
if (type == CURVE_TYPE_CATMULL_ROM) {
/* Avoid creating the attribute for Catmull Rom which is the default when the attribute doesn't
* exist anyway. */
this->attributes_for_write().remove("curve_type");
}
else {
this->curve_types_for_write().fill(type);
}
this->runtime->type_counts.fill(0);
this->runtime->type_counts[type] = this->curves_num();
this->tag_topology_changed();
}
void CurvesGeometry::fill_curve_types(const IndexMask selection, const CurveType type)
{
if (selection.size() == this->curves_num()) {
this->fill_curve_types(type);
return;
}
if (std::optional<int8_t> single_type = this->curve_types().get_if_single()) {
if (single_type == type) {
this->fill_curve_types(type);
return;
}
}
/* A potential performance optimization is only counting the changed indices. */
this->curve_types_for_write().fill_indices(selection, type);
this->update_curve_types();
this->tag_topology_changed();
}
std::array<int, CURVE_TYPES_NUM> calculate_type_counts(const VArray<int8_t> &types)
{
using CountsType = std::array<int, CURVE_TYPES_NUM>;
CountsType counts;
counts.fill(0);
if (types.is_single()) {
counts[types.get_internal_single()] = types.size();
return counts;
}
Span<int8_t> types_span = types.get_internal_span();
return threading::parallel_reduce(
types.index_range(),
2048,
counts,
[&](const IndexRange curves_range, const CountsType &init) {
CountsType result = init;
for (const int curve_index : curves_range) {
result[types_span[curve_index]]++;
}
return result;
},
[](const CountsType &a, const CountsType &b) {
CountsType result = a;
for (const int i : IndexRange(CURVE_TYPES_NUM)) {
result[i] += b[i];
}
return result;
});
}
void CurvesGeometry::update_curve_types()
{
this->runtime->type_counts = calculate_type_counts(this->curve_types());
}
Span<float3> CurvesGeometry::positions() const
{
return get_span_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_POSITION);
}
MutableSpan<float3> CurvesGeometry::positions_for_write()
{
return get_mutable_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_POSITION);
}
Span<int> CurvesGeometry::offsets() const
{
return {this->curve_offsets, this->curve_num + 1};
}
MutableSpan<int> CurvesGeometry::offsets_for_write()
{
return {this->curve_offsets, this->curve_num + 1};
}
VArray<bool> CurvesGeometry::cyclic() const
{
return get_varray_attribute<bool>(*this, ATTR_DOMAIN_CURVE, ATTR_CYCLIC, false);
}
MutableSpan<bool> CurvesGeometry::cyclic_for_write()
{
return get_mutable_attribute<bool>(*this, ATTR_DOMAIN_CURVE, ATTR_CYCLIC, false);
}
VArray<int> CurvesGeometry::resolution() const
{
return get_varray_attribute<int>(*this, ATTR_DOMAIN_CURVE, ATTR_RESOLUTION, 12);
}
MutableSpan<int> CurvesGeometry::resolution_for_write()
{
return get_mutable_attribute<int>(*this, ATTR_DOMAIN_CURVE, ATTR_RESOLUTION, 12);
}
VArray<int8_t> CurvesGeometry::normal_mode() const
{
return get_varray_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NORMAL_MODE, 0);
}
MutableSpan<int8_t> CurvesGeometry::normal_mode_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NORMAL_MODE);
}
VArray<float> CurvesGeometry::tilt() const
{
return get_varray_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_TILT, 0.0f);
}
MutableSpan<float> CurvesGeometry::tilt_for_write()
{
return get_mutable_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_TILT);
}
VArray<int8_t> CurvesGeometry::handle_types_left() const
{
return get_varray_attribute<int8_t>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_TYPE_LEFT, 0);
}
MutableSpan<int8_t> CurvesGeometry::handle_types_left_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_TYPE_LEFT, 0);
}
VArray<int8_t> CurvesGeometry::handle_types_right() const
{
return get_varray_attribute<int8_t>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_TYPE_RIGHT, 0);
}
MutableSpan<int8_t> CurvesGeometry::handle_types_right_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_TYPE_RIGHT, 0);
}
Span<float3> CurvesGeometry::handle_positions_left() const
{
return get_span_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_POSITION_LEFT);
}
MutableSpan<float3> CurvesGeometry::handle_positions_left_for_write()
{
return get_mutable_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_POSITION_LEFT);
}
Span<float3> CurvesGeometry::handle_positions_right() const
{
return get_span_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_POSITION_RIGHT);
}
MutableSpan<float3> CurvesGeometry::handle_positions_right_for_write()
{
return get_mutable_attribute<float3>(*this, ATTR_DOMAIN_POINT, ATTR_HANDLE_POSITION_RIGHT);
}
VArray<int8_t> CurvesGeometry::nurbs_orders() const
{
return get_varray_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NURBS_ORDER, 4);
}
MutableSpan<int8_t> CurvesGeometry::nurbs_orders_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NURBS_ORDER, 4);
}
Span<float> CurvesGeometry::nurbs_weights() const
{
return get_span_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_NURBS_WEIGHT);
}
MutableSpan<float> CurvesGeometry::nurbs_weights_for_write()
{
return get_mutable_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_NURBS_WEIGHT);
}
VArray<int8_t> CurvesGeometry::nurbs_knots_modes() const
{
return get_varray_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NURBS_KNOTS_MODE, 0);
}
MutableSpan<int8_t> CurvesGeometry::nurbs_knots_modes_for_write()
{
return get_mutable_attribute<int8_t>(*this, ATTR_DOMAIN_CURVE, ATTR_NURBS_KNOTS_MODE, 0);
}
Span<float2> CurvesGeometry::surface_uv_coords() const
{
return get_span_attribute<float2>(*this, ATTR_DOMAIN_CURVE, ATTR_SURFACE_UV_COORDINATE);
}
MutableSpan<float2> CurvesGeometry::surface_uv_coords_for_write()
{
return get_mutable_attribute<float2>(*this, ATTR_DOMAIN_CURVE, ATTR_SURFACE_UV_COORDINATE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Evaluation
* \{ */
template<typename CountFn> void build_offsets(MutableSpan<int> offsets, const CountFn &count_fn)
{
int offset = 0;
for (const int i : offsets.drop_back(1).index_range()) {
offsets[i] = offset;
offset += count_fn(i);
}
offsets.last() = offset;
}
static void calculate_evaluated_offsets(const CurvesGeometry &curves,
MutableSpan<int> offsets,
MutableSpan<int> all_bezier_offsets)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
const VArray<int8_t> types = curves.curve_types();
const VArray<int> resolution = curves.resolution();
const VArray<bool> cyclic = curves.cyclic();
const VArraySpan<int8_t> handle_types_left{curves.handle_types_left()};
const VArraySpan<int8_t> handle_types_right{curves.handle_types_right()};
const VArray<int8_t> nurbs_orders = curves.nurbs_orders();
const VArray<int8_t> nurbs_knots_modes = curves.nurbs_knots_modes();
build_offsets(offsets, [&](const int curve_index) -> int {
const IndexRange points = points_by_curve[curve_index];
switch (types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
return curves::catmull_rom::calculate_evaluated_num(
points.size(), cyclic[curve_index], resolution[curve_index]);
case CURVE_TYPE_POLY:
return points.size();
case CURVE_TYPE_BEZIER: {
const IndexRange offsets = curves::per_curve_point_offsets_range(points, curve_index);
curves::bezier::calculate_evaluated_offsets(handle_types_left.slice(points),
handle_types_right.slice(points),
cyclic[curve_index],
resolution[curve_index],
all_bezier_offsets.slice(offsets));
return all_bezier_offsets[offsets.last()];
}
case CURVE_TYPE_NURBS:
return curves::nurbs::calculate_evaluated_num(points.size(),
nurbs_orders[curve_index],
cyclic[curve_index],
resolution[curve_index],
KnotsMode(nurbs_knots_modes[curve_index]));
}
BLI_assert_unreachable();
return 0;
});
}
OffsetIndices<int> CurvesGeometry::evaluated_points_by_curve() const
{
this->runtime->offsets_cache_mutex.ensure([&]() {
this->runtime->evaluated_offsets_cache.resize(this->curves_num() + 1);
if (this->has_curve_with_type(CURVE_TYPE_BEZIER)) {
this->runtime->all_bezier_evaluated_offsets.resize(this->points_num() + this->curves_num());
}
else {
this->runtime->all_bezier_evaluated_offsets.clear_and_shrink();
}
calculate_evaluated_offsets(*this,
this->runtime->evaluated_offsets_cache,
this->runtime->all_bezier_evaluated_offsets);
});
return OffsetIndices<int>(this->runtime->evaluated_offsets_cache);
}
IndexMask CurvesGeometry::indices_for_curve_type(const CurveType type,
Vector<int64_t> &r_indices) const
{
return this->indices_for_curve_type(type, this->curves_range(), r_indices);
}
IndexMask CurvesGeometry::indices_for_curve_type(const CurveType type,
const IndexMask selection,
Vector<int64_t> &r_indices) const
{
return curves::indices_for_type(
this->curve_types(), this->curve_type_counts(), type, selection, r_indices);
}
Geometry Nodes: Curve and mesh topology access nodes This patch contains an initial set of nodes to access basic mesh topology information, as explored in T100020. The nodes allow six direct topology mappings for meshes: - **Corner -> Face** The face a corner is in, the index in the face - **Vertex -> Edge** Choose an edge attached to the vertex - **Vertex -> Corner** Choose a corner attached to the vertex - **Corner -> Edge** The next and previous edge at each face corner - **Corner -> Vertex** The vertex associated with a corner - **Corner -> Corner** Offset a corner index within a face And two new topology mappings for curves: - **Curve -> Points** Choose a point within a curve - **Point -> Curve** The curve a point is in, the index in the curve The idea is that some of the 16 possible mesh mappings are more important, and that this is a useful set of nodes to start exploring this area. For mappings with an arbitrary number of connections, we must sort them and use an index to choose a single element, because geometry nodes does not support list fields. Note that the sort index has repeating behavior as it goes over the "Total" number of connections, and negative sort indices choose from the end. Currently which of the "start" elements is used is determined by the field context, so the "Field at Index" and "Interpolate Domain" nodes will be quite important. Also, currently the "Sort Index" inputs are clamped to the number of connections. One important feature that isn't implemented here is using the winding order for the output elements. This can be a separate mode for some of these nodes. It will be optional because of the performance impact. There are several todos for separate commits after this: - Rename "Control Point Neighbors" to be consistent with this naming - Version away the "Vertex Neighbors" node which is fully redundant now - Implement a special case for when no weights are used for performance - De-duplicating some of the sorting logic between the nodes - Improve performance and memory use of topology mappings - Look into caching some of the mappings on meshes Differential Revision: https://developer.blender.org/D16029
2022-09-28 14:38:27 -05:00
Array<int> CurvesGeometry::point_to_curve_map() const
{
const OffsetIndices points_by_curve = this->points_by_curve();
Geometry Nodes: Curve and mesh topology access nodes This patch contains an initial set of nodes to access basic mesh topology information, as explored in T100020. The nodes allow six direct topology mappings for meshes: - **Corner -> Face** The face a corner is in, the index in the face - **Vertex -> Edge** Choose an edge attached to the vertex - **Vertex -> Corner** Choose a corner attached to the vertex - **Corner -> Edge** The next and previous edge at each face corner - **Corner -> Vertex** The vertex associated with a corner - **Corner -> Corner** Offset a corner index within a face And two new topology mappings for curves: - **Curve -> Points** Choose a point within a curve - **Point -> Curve** The curve a point is in, the index in the curve The idea is that some of the 16 possible mesh mappings are more important, and that this is a useful set of nodes to start exploring this area. For mappings with an arbitrary number of connections, we must sort them and use an index to choose a single element, because geometry nodes does not support list fields. Note that the sort index has repeating behavior as it goes over the "Total" number of connections, and negative sort indices choose from the end. Currently which of the "start" elements is used is determined by the field context, so the "Field at Index" and "Interpolate Domain" nodes will be quite important. Also, currently the "Sort Index" inputs are clamped to the number of connections. One important feature that isn't implemented here is using the winding order for the output elements. This can be a separate mode for some of these nodes. It will be optional because of the performance impact. There are several todos for separate commits after this: - Rename "Control Point Neighbors" to be consistent with this naming - Version away the "Vertex Neighbors" node which is fully redundant now - Implement a special case for when no weights are used for performance - De-duplicating some of the sorting logic between the nodes - Improve performance and memory use of topology mappings - Look into caching some of the mappings on meshes Differential Revision: https://developer.blender.org/D16029
2022-09-28 14:38:27 -05:00
Array<int> map(this->points_num());
threading::parallel_for(this->curves_range(), 1024, [&](const IndexRange range) {
for (const int i_curve : range) {
map.as_mutable_span().slice(points_by_curve[i_curve]).fill(i_curve);
}
});
Geometry Nodes: Curve and mesh topology access nodes This patch contains an initial set of nodes to access basic mesh topology information, as explored in T100020. The nodes allow six direct topology mappings for meshes: - **Corner -> Face** The face a corner is in, the index in the face - **Vertex -> Edge** Choose an edge attached to the vertex - **Vertex -> Corner** Choose a corner attached to the vertex - **Corner -> Edge** The next and previous edge at each face corner - **Corner -> Vertex** The vertex associated with a corner - **Corner -> Corner** Offset a corner index within a face And two new topology mappings for curves: - **Curve -> Points** Choose a point within a curve - **Point -> Curve** The curve a point is in, the index in the curve The idea is that some of the 16 possible mesh mappings are more important, and that this is a useful set of nodes to start exploring this area. For mappings with an arbitrary number of connections, we must sort them and use an index to choose a single element, because geometry nodes does not support list fields. Note that the sort index has repeating behavior as it goes over the "Total" number of connections, and negative sort indices choose from the end. Currently which of the "start" elements is used is determined by the field context, so the "Field at Index" and "Interpolate Domain" nodes will be quite important. Also, currently the "Sort Index" inputs are clamped to the number of connections. One important feature that isn't implemented here is using the winding order for the output elements. This can be a separate mode for some of these nodes. It will be optional because of the performance impact. There are several todos for separate commits after this: - Rename "Control Point Neighbors" to be consistent with this naming - Version away the "Vertex Neighbors" node which is fully redundant now - Implement a special case for when no weights are used for performance - De-duplicating some of the sorting logic between the nodes - Improve performance and memory use of topology mappings - Look into caching some of the mappings on meshes Differential Revision: https://developer.blender.org/D16029
2022-09-28 14:38:27 -05:00
return map;
}
void CurvesGeometry::ensure_nurbs_basis_cache() const
{
this->runtime->nurbs_basis_cache_mutex.ensure([&]() {
Vector<int64_t> nurbs_indices;
const IndexMask nurbs_mask = this->indices_for_curve_type(CURVE_TYPE_NURBS, nurbs_indices);
if (nurbs_mask.is_empty()) {
return;
}
this->runtime->nurbs_basis_cache.resize(this->curves_num());
MutableSpan<curves::nurbs::BasisCache> basis_caches(this->runtime->nurbs_basis_cache);
const OffsetIndices<int> points_by_curve = this->points_by_curve();
const OffsetIndices<int> evaluated_points_by_curve = this->evaluated_points_by_curve();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> orders = this->nurbs_orders();
const VArray<int8_t> knots_modes = this->nurbs_knots_modes();
threading::parallel_for(nurbs_mask.index_range(), 64, [&](const IndexRange range) {
for (const int curve_index : nurbs_mask.slice(range)) {
const IndexRange points = points_by_curve[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
const int8_t order = orders[curve_index];
const bool is_cyclic = cyclic[curve_index];
const KnotsMode mode = KnotsMode(knots_modes[curve_index]);
if (!curves::nurbs::check_valid_num_and_order(points.size(), order, is_cyclic, mode)) {
basis_caches[curve_index].invalid = true;
continue;
}
const int knots_num = curves::nurbs::knots_num(points.size(), order, is_cyclic);
Array<float> knots(knots_num);
curves::nurbs::calculate_knots(points.size(), mode, order, is_cyclic, knots);
curves::nurbs::calculate_basis_cache(points.size(),
evaluated_points.size(),
order,
is_cyclic,
knots,
basis_caches[curve_index]);
}
});
});
}
Span<float3> CurvesGeometry::evaluated_positions() const
{
this->runtime->position_cache_mutex.ensure([&]() {
if (this->is_single_type(CURVE_TYPE_POLY)) {
this->runtime->evaluated_positions_span = this->positions();
this->runtime->evaluated_position_cache.clear_and_shrink();
return;
}
this->runtime->evaluated_position_cache.resize(this->evaluated_points_num());
MutableSpan<float3> evaluated_positions = this->runtime->evaluated_position_cache;
this->runtime->evaluated_positions_span = evaluated_positions;
const OffsetIndices<int> points_by_curve = this->points_by_curve();
const OffsetIndices<int> evaluated_points_by_curve = this->evaluated_points_by_curve();
const VArray<int8_t> types = this->curve_types();
const VArray<bool> cyclic = this->cyclic();
const VArray<int> resolution = this->resolution();
const Span<float3> positions = this->positions();
const Span<float3> handle_positions_left = this->handle_positions_left();
const Span<float3> handle_positions_right = this->handle_positions_right();
const Span<int> all_bezier_evaluated_offsets = this->runtime->all_bezier_evaluated_offsets;
const VArray<int8_t> nurbs_orders = this->nurbs_orders();
const Span<float> nurbs_weights = this->nurbs_weights();
this->ensure_nurbs_basis_cache();
threading::parallel_for(this->curves_range(), 128, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const IndexRange points = points_by_curve[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
switch (types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
curves::catmull_rom::interpolate_to_evaluated(
positions.slice(points),
cyclic[curve_index],
resolution[curve_index],
evaluated_positions.slice(evaluated_points));
break;
case CURVE_TYPE_POLY:
evaluated_positions.slice(evaluated_points).copy_from(positions.slice(points));
break;
case CURVE_TYPE_BEZIER: {
const IndexRange offsets = curves::per_curve_point_offsets_range(points, curve_index);
curves::bezier::calculate_evaluated_positions(
positions.slice(points),
handle_positions_left.slice(points),
handle_positions_right.slice(points),
all_bezier_evaluated_offsets.slice(offsets),
evaluated_positions.slice(evaluated_points));
break;
}
case CURVE_TYPE_NURBS:
curves::nurbs::interpolate_to_evaluated(this->runtime->nurbs_basis_cache[curve_index],
nurbs_orders[curve_index],
nurbs_weights.slice_safe(points),
positions.slice(points),
evaluated_positions.slice(evaluated_points));
break;
default:
BLI_assert_unreachable();
break;
}
}
});
});
return this->runtime->evaluated_positions_span;
}
Span<float3> CurvesGeometry::evaluated_tangents() const
{
this->runtime->tangent_cache_mutex.ensure([&]() {
const OffsetIndices<int> evaluated_points_by_curve = this->evaluated_points_by_curve();
const Span<float3> evaluated_positions = this->evaluated_positions();
const VArray<bool> cyclic = this->cyclic();
this->runtime->evaluated_tangent_cache.resize(this->evaluated_points_num());
MutableSpan<float3> tangents = this->runtime->evaluated_tangent_cache;
threading::parallel_for(this->curves_range(), 128, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
curves::poly::calculate_tangents(evaluated_positions.slice(evaluated_points),
cyclic[curve_index],
tangents.slice(evaluated_points));
}
});
/* Correct the first and last tangents of non-cyclic Bezier curves so that they align with
* the inner handles. This is a separate loop to avoid the cost when Bezier type curves are
* not used. */
Vector<int64_t> bezier_indices;
const IndexMask bezier_mask = this->indices_for_curve_type(CURVE_TYPE_BEZIER, bezier_indices);
if (!bezier_mask.is_empty()) {
const OffsetIndices<int> points_by_curve = this->points_by_curve();
const Span<float3> positions = this->positions();
const Span<float3> handles_left = this->handle_positions_left();
const Span<float3> handles_right = this->handle_positions_right();
threading::parallel_for(bezier_mask.index_range(), 1024, [&](IndexRange range) {
for (const int curve_index : bezier_mask.slice(range)) {
if (cyclic[curve_index]) {
continue;
}
const IndexRange points = points_by_curve[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
const float epsilon = 1e-6f;
if (!math::almost_equal_relative(
handles_right[points.first()], positions[points.first()], epsilon)) {
tangents[evaluated_points.first()] = math::normalize(handles_right[points.first()] -
positions[points.first()]);
}
if (!math::almost_equal_relative(
handles_left[points.last()], positions[points.last()], epsilon)) {
tangents[evaluated_points.last()] = math::normalize(positions[points.last()] -
handles_left[points.last()]);
}
}
});
}
});
return this->runtime->evaluated_tangent_cache;
}
static void rotate_directions_around_axes(MutableSpan<float3> directions,
const Span<float3> axes,
const Span<float> angles)
{
for (const int i : directions.index_range()) {
directions[i] = math::rotate_direction_around_axis(directions[i], axes[i], angles[i]);
}
}
static void evaluate_generic_data_for_curve(
const int curve_index,
const IndexRange points,
const VArray<int8_t> &types,
const VArray<bool> &cyclic,
const VArray<int> &resolution,
const Span<int> all_bezier_evaluated_offsets,
const Span<curves::nurbs::BasisCache> nurbs_basis_cache,
const VArray<int8_t> &nurbs_orders,
const Span<float> nurbs_weights,
const GSpan src,
GMutableSpan dst)
{
switch (types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
curves::catmull_rom::interpolate_to_evaluated(
src, cyclic[curve_index], resolution[curve_index], dst);
break;
case CURVE_TYPE_POLY:
dst.copy_from(src);
break;
case CURVE_TYPE_BEZIER: {
const IndexRange offsets = curves::per_curve_point_offsets_range(points, curve_index);
curves::bezier::interpolate_to_evaluated(
src, all_bezier_evaluated_offsets.slice(offsets), dst);
break;
}
case CURVE_TYPE_NURBS:
curves::nurbs::interpolate_to_evaluated(nurbs_basis_cache[curve_index],
nurbs_orders[curve_index],
nurbs_weights.slice_safe(points),
src,
dst);
break;
}
}
Span<float3> CurvesGeometry::evaluated_normals() const
{
this->runtime->normal_cache_mutex.ensure([&]() {
const OffsetIndices<int> points_by_curve = this->points_by_curve();
const OffsetIndices<int> evaluated_points_by_curve = this->evaluated_points_by_curve();
const VArray<int8_t> types = this->curve_types();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> normal_mode = this->normal_mode();
const VArray<int> resolution = this->resolution();
const VArray<int8_t> nurbs_orders = this->nurbs_orders();
const Span<float> nurbs_weights = this->nurbs_weights();
const Span<float3> evaluated_tangents = this->evaluated_tangents();
const VArray<float> tilt = this->tilt();
VArraySpan<float> tilt_span;
const bool use_tilt = !(tilt.is_single() && tilt.get_internal_single() == 0.0f);
if (use_tilt) {
tilt_span = tilt;
}
this->runtime->evaluated_normal_cache.resize(this->evaluated_points_num());
MutableSpan<float3> evaluated_normals = this->runtime->evaluated_normal_cache;
threading::parallel_for(this->curves_range(), 128, [&](IndexRange curves_range) {
/* Reuse a buffer for the evaluated tilts. */
Vector<float> evaluated_tilts;
for (const int curve_index : curves_range) {
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
switch (normal_mode[curve_index]) {
case NORMAL_MODE_Z_UP:
curves::poly::calculate_normals_z_up(evaluated_tangents.slice(evaluated_points),
evaluated_normals.slice(evaluated_points));
break;
case NORMAL_MODE_MINIMUM_TWIST:
curves::poly::calculate_normals_minimum(evaluated_tangents.slice(evaluated_points),
cyclic[curve_index],
evaluated_normals.slice(evaluated_points));
break;
}
/* If the "tilt" attribute exists, rotate the normals around the tangents by the
* evaluated angles. We can avoid copying the tilts to evaluate them for poly curves. */
if (use_tilt) {
const IndexRange points = points_by_curve[curve_index];
if (types[curve_index] == CURVE_TYPE_POLY) {
rotate_directions_around_axes(evaluated_normals.slice(evaluated_points),
evaluated_tangents.slice(evaluated_points),
tilt_span.slice(points));
}
else {
evaluated_tilts.reinitialize(evaluated_points.size());
evaluate_generic_data_for_curve(curve_index,
points,
types,
cyclic,
resolution,
this->runtime->all_bezier_evaluated_offsets.as_span(),
this->runtime->nurbs_basis_cache.as_span(),
nurbs_orders,
nurbs_weights,
tilt_span.slice(points),
evaluated_tilts.as_mutable_span());
rotate_directions_around_axes(evaluated_normals.slice(evaluated_points),
evaluated_tangents.slice(evaluated_points),
evaluated_tilts.as_span());
}
}
}
});
});
return this->runtime->evaluated_normal_cache;
}
void CurvesGeometry::interpolate_to_evaluated(const int curve_index,
const GSpan src,
GMutableSpan dst) const
{
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
BLI_assert(this->runtime->nurbs_basis_cache_mutex.is_cached());
const OffsetIndices points_by_curve = this->points_by_curve();
const IndexRange points = points_by_curve[curve_index];
BLI_assert(src.size() == points.size());
BLI_assert(dst.size() == this->evaluated_points_by_curve().size(curve_index));
evaluate_generic_data_for_curve(curve_index,
points,
this->curve_types(),
this->cyclic(),
this->resolution(),
this->runtime->all_bezier_evaluated_offsets.as_span(),
this->runtime->nurbs_basis_cache.as_span(),
this->nurbs_orders(),
this->nurbs_weights(),
src,
dst);
}
Curves: Port curve to mesh node to the new data-block This commit changes the Curve to Mesh node to work with `Curves` instead of `CurveEval`. The change ends up basically completely rewriting the node, since the different attribute storage means that the decisions made previously don't make much sense anymore. The main loops are now "for each attribute: for each curve combination" rather than the other way around, with the goal of taking advantage of the locality of curve attributes. This improvement is quite noticeable with many small curves; I measured a 4-5x improvement (around 4-5s to <1s) when converting millions of curves to tens of millions of faces. I didn't obverse any change in performance compared to 3.1 with fewer curves though. The changes also solve an algorithmic flaw where any interpolated attributes would be evaluated for every curve combination instead of just once per curve. This can be a large improvement when there are many profile curves. The code relies heavily on a function `foreach_curve_combination` which calculates some basic information about each combination and calls a templated function. I made assumptions about unnecessary reads being removed by compiler optimizations. For further performance improvements in the future that might be an area to investigate. Another might be using a "for a group of curves: for each attribute: for each curve" pattern to increase the locality of memory access. Differential Revision: https://developer.blender.org/D14642
2022-04-15 10:14:54 -05:00
void CurvesGeometry::interpolate_to_evaluated(const GSpan src, GMutableSpan dst) const
{
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
BLI_assert(this->runtime->nurbs_basis_cache_mutex.is_cached());
const OffsetIndices points_by_curve = this->points_by_curve();
const OffsetIndices evaluated_points_by_curve = this->evaluated_points_by_curve();
Curves: Port curve to mesh node to the new data-block This commit changes the Curve to Mesh node to work with `Curves` instead of `CurveEval`. The change ends up basically completely rewriting the node, since the different attribute storage means that the decisions made previously don't make much sense anymore. The main loops are now "for each attribute: for each curve combination" rather than the other way around, with the goal of taking advantage of the locality of curve attributes. This improvement is quite noticeable with many small curves; I measured a 4-5x improvement (around 4-5s to <1s) when converting millions of curves to tens of millions of faces. I didn't obverse any change in performance compared to 3.1 with fewer curves though. The changes also solve an algorithmic flaw where any interpolated attributes would be evaluated for every curve combination instead of just once per curve. This can be a large improvement when there are many profile curves. The code relies heavily on a function `foreach_curve_combination` which calculates some basic information about each combination and calls a templated function. I made assumptions about unnecessary reads being removed by compiler optimizations. For further performance improvements in the future that might be an area to investigate. Another might be using a "for a group of curves: for each attribute: for each curve" pattern to increase the locality of memory access. Differential Revision: https://developer.blender.org/D14642
2022-04-15 10:14:54 -05:00
const VArray<int8_t> types = this->curve_types();
const VArray<int> resolution = this->resolution();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> nurbs_orders = this->nurbs_orders();
const Span<float> nurbs_weights = this->nurbs_weights();
threading::parallel_for(this->curves_range(), 512, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const IndexRange points = points_by_curve[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
evaluate_generic_data_for_curve(curve_index,
points,
types,
cyclic,
resolution,
this->runtime->all_bezier_evaluated_offsets,
this->runtime->nurbs_basis_cache,
nurbs_orders,
nurbs_weights,
src.slice(points),
dst.slice(evaluated_points));
Curves: Port curve to mesh node to the new data-block This commit changes the Curve to Mesh node to work with `Curves` instead of `CurveEval`. The change ends up basically completely rewriting the node, since the different attribute storage means that the decisions made previously don't make much sense anymore. The main loops are now "for each attribute: for each curve combination" rather than the other way around, with the goal of taking advantage of the locality of curve attributes. This improvement is quite noticeable with many small curves; I measured a 4-5x improvement (around 4-5s to <1s) when converting millions of curves to tens of millions of faces. I didn't obverse any change in performance compared to 3.1 with fewer curves though. The changes also solve an algorithmic flaw where any interpolated attributes would be evaluated for every curve combination instead of just once per curve. This can be a large improvement when there are many profile curves. The code relies heavily on a function `foreach_curve_combination` which calculates some basic information about each combination and calls a templated function. I made assumptions about unnecessary reads being removed by compiler optimizations. For further performance improvements in the future that might be an area to investigate. Another might be using a "for a group of curves: for each attribute: for each curve" pattern to increase the locality of memory access. Differential Revision: https://developer.blender.org/D14642
2022-04-15 10:14:54 -05:00
}
});
}
void CurvesGeometry::ensure_evaluated_lengths() const
{
this->runtime->length_cache_mutex.ensure([&]() {
/* Use an extra length value for the final cyclic segment for a consistent size
* (see comment on #evaluated_length_cache). */
const int total_num = this->evaluated_points_num() + this->curves_num();
this->runtime->evaluated_length_cache.resize(total_num);
MutableSpan<float> evaluated_lengths = this->runtime->evaluated_length_cache;
const OffsetIndices<int> evaluated_points_by_curve = this->evaluated_points_by_curve();
const Span<float3> evaluated_positions = this->evaluated_positions();
const VArray<bool> curves_cyclic = this->cyclic();
threading::parallel_for(this->curves_range(), 128, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const bool cyclic = curves_cyclic[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
const IndexRange lengths_range = this->lengths_range_for_curve(curve_index, cyclic);
length_parameterize::accumulate_lengths(evaluated_positions.slice(evaluated_points),
cyclic,
evaluated_lengths.slice(lengths_range));
}
});
});
}
void CurvesGeometry::ensure_can_interpolate_to_evaluated() const
{
this->evaluated_points_by_curve();
this->ensure_nurbs_basis_cache();
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operations
* \{ */
void CurvesGeometry::resize(const int points_num, const int curves_num)
{
if (points_num != this->point_num) {
CustomData_realloc(&this->point_data, this->points_num(), points_num);
this->point_num = points_num;
}
if (curves_num != this->curve_num) {
CustomData_realloc(&this->curve_data, this->curves_num(), curves_num);
this->curve_num = curves_num;
this->curve_offsets = (int *)MEM_reallocN(this->curve_offsets, sizeof(int) * (curves_num + 1));
}
this->tag_topology_changed();
}
void CurvesGeometry::tag_positions_changed()
{
this->runtime->position_cache_mutex.tag_dirty();
this->runtime->tangent_cache_mutex.tag_dirty();
this->runtime->normal_cache_mutex.tag_dirty();
this->runtime->length_cache_mutex.tag_dirty();
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
this->runtime->bounds_cache.tag_dirty();
}
void CurvesGeometry::tag_topology_changed()
{
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
this->tag_positions_changed();
this->runtime->offsets_cache_mutex.tag_dirty();
this->runtime->nurbs_basis_cache_mutex.tag_dirty();
}
void CurvesGeometry::tag_normals_changed()
{
this->runtime->normal_cache_mutex.tag_dirty();
}
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
void CurvesGeometry::tag_radii_changed()
{
this->runtime->bounds_cache.tag_dirty();
}
static void translate_positions(MutableSpan<float3> positions, const float3 &translation)
{
threading::parallel_for(positions.index_range(), 2048, [&](const IndexRange range) {
for (float3 &position : positions.slice(range)) {
position += translation;
}
});
}
static void transform_positions(MutableSpan<float3> positions, const float4x4 &matrix)
{
threading::parallel_for(positions.index_range(), 1024, [&](const IndexRange range) {
for (float3 &position : positions.slice(range)) {
position = matrix * position;
}
});
}
void CurvesGeometry::calculate_bezier_auto_handles()
{
if (!this->has_curve_with_type(CURVE_TYPE_BEZIER)) {
return;
}
if (this->handle_positions_left().is_empty() || this->handle_positions_right().is_empty()) {
return;
}
const OffsetIndices points_by_curve = this->points_by_curve();
const VArray<int8_t> types = this->curve_types();
const VArray<bool> cyclic = this->cyclic();
const VArraySpan<int8_t> types_left{this->handle_types_left()};
const VArraySpan<int8_t> types_right{this->handle_types_right()};
const Span<float3> positions = this->positions();
MutableSpan<float3> positions_left = this->handle_positions_left_for_write();
MutableSpan<float3> positions_right = this->handle_positions_right_for_write();
threading::parallel_for(this->curves_range(), 128, [&](IndexRange range) {
for (const int i_curve : range) {
if (types[i_curve] == CURVE_TYPE_BEZIER) {
const IndexRange points = points_by_curve[i_curve];
curves::bezier::calculate_auto_handles(cyclic[i_curve],
types_left.slice(points),
types_right.slice(points),
positions.slice(points),
positions_left.slice(points),
positions_right.slice(points));
}
}
});
}
void CurvesGeometry::translate(const float3 &translation)
{
translate_positions(this->positions_for_write(), translation);
if (!this->handle_positions_left().is_empty()) {
translate_positions(this->handle_positions_left_for_write(), translation);
}
if (!this->handle_positions_right().is_empty()) {
translate_positions(this->handle_positions_right_for_write(), translation);
}
this->tag_positions_changed();
}
void CurvesGeometry::transform(const float4x4 &matrix)
{
transform_positions(this->positions_for_write(), matrix);
if (!this->handle_positions_left().is_empty()) {
transform_positions(this->handle_positions_left_for_write(), matrix);
}
if (!this->handle_positions_right().is_empty()) {
transform_positions(this->handle_positions_right_for_write(), matrix);
}
this->tag_positions_changed();
}
bool CurvesGeometry::bounds_min_max(float3 &min, float3 &max) const
{
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
if (this->points_num() == 0) {
return false;
}
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
this->runtime->bounds_cache.ensure([&](Bounds<float3> &r_bounds) {
const Span<float3> positions = this->evaluated_positions();
if (this->attributes().contains("radius")) {
const VArraySpan<float> radii = this->attributes().lookup<float>("radius");
Array<float> evaluated_radii(this->evaluated_points_num());
this->ensure_can_interpolate_to_evaluated();
Geometry: Cache bounds min and max, share between data-blocks Bounding box calculation can be a large in some situations, especially instancing. This patch caches the min and max of the bounding box in runtime data of meshes, point clouds, and curves, implementing part of T96968. Bounds are now calculated lazily-- only after they are tagged dirty. Also, cached bounds are also shared when copying geometry data-blocks that have equivalent data. When bounds are calculated on an evaluated data-block, they are also accessible on the original, and the next evaluated ID will also share them. A geometry will stop sharing bounds as soon as its positions (or radii) are changed. Just caching the bounds gave a 2-3x speedup with thousands of mesh geometry instances in the viewport. Sharing the bounds can eliminate recalculations entirely in cases like copying meshes in geometry nodes or the selection paint brush in curves sculpt mode, which causes a reevaluation but doesn't change the positions. **Implementation** The sharing is achieved with a `shared_ptr` that points to a cache mutex (from D16419) and the cached bounds data. When geometries are copied, the bounds are shared by default, and only "un-shared" when the bounds are tagged dirty. Point clouds have a new runtime struct to store this data. Functions for tagging the data dirty are improved for added for point clouds and improved for curves. A missing tag has also been fixed for mesh sculpt mode. **Future** There are further improvements which can be worked on next - Apply changes to volume objects and other types where it makes sense - Continue cleanup changes described in T96968 - Apply shared cache design to more expensive data like triangulation or normals Differential Revision: https://developer.blender.org/D16204
2022-11-15 13:46:55 -06:00
this->interpolate_to_evaluated(radii, evaluated_radii.as_mutable_span());
r_bounds = *bounds::min_max_with_radii(positions, evaluated_radii.as_span());
}
else {
r_bounds = *bounds::min_max(positions);
}
});
const Bounds<float3> &bounds = this->runtime->bounds_cache.data();
min = math::min(bounds.min, min);
max = math::max(bounds.max, max);
return true;
}
static void copy_between_buffers(const CPPType &type,
const void *src_buffer,
void *dst_buffer,
const IndexRange src_range,
const IndexRange dst_range)
{
BLI_assert(src_range.size() == dst_range.size());
type.copy_construct_n(POINTER_OFFSET(src_buffer, type.size() * src_range.start()),
POINTER_OFFSET(dst_buffer, type.size() * dst_range.start()),
src_range.size());
}
static void copy_with_map(const GSpan src, const Span<int> map, GMutableSpan dst)
{
attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
using T = decltype(dummy);
array_utils::gather(src.typed<T>(), map, dst.typed<T>());
});
}
static CurvesGeometry copy_with_removed_points(
const CurvesGeometry &curves,
const IndexMask points_to_delete,
const AnonymousAttributePropagationInfo &propagation_info)
{
/* Use a map from points to curves to facilitate using an #IndexMask input. */
const Array<int> point_to_curve_map = curves.point_to_curve_map();
const Vector<IndexRange> copy_point_ranges = points_to_delete.extract_ranges_invert(
curves.points_range());
/* For every range of points to copy, find the offset in the result curves point layers. */
int new_point_count = 0;
Array<int> copy_point_range_dst_offsets(copy_point_ranges.size());
for (const int i : copy_point_ranges.index_range()) {
copy_point_range_dst_offsets[i] = new_point_count;
new_point_count += copy_point_ranges[i].size();
}
BLI_assert(new_point_count == (curves.points_num() - points_to_delete.size()));
/* Find out how many non-deleted points there are in every curve. */
Array<int> curve_point_counts(curves.curves_num(), 0);
for (const IndexRange range : copy_point_ranges) {
for (const int point_i : range) {
curve_point_counts[point_to_curve_map[point_i]]++;
}
}
/* Build the offsets for the new curve points, skipping curves that had all points deleted.
* Also store the original indices of the corresponding input curves, to facilitate parallel
* copying of curve domain data. */
int new_curve_count = 0;
int curve_point_offset = 0;
Vector<int> new_curve_offsets;
Vector<int> new_curve_orig_indices;
new_curve_offsets.append(0);
for (const int i : curve_point_counts.index_range()) {
if (curve_point_counts[i] > 0) {
curve_point_offset += curve_point_counts[i];
new_curve_offsets.append(curve_point_offset);
new_curve_count++;
new_curve_orig_indices.append(i);
}
}
CurvesGeometry new_curves{new_point_count, new_curve_count};
Vector<bke::AttributeTransferData> point_attributes = bke::retrieve_attributes_for_transfer(
curves.attributes(),
new_curves.attributes_for_write(),
ATTR_DOMAIN_MASK_POINT,
propagation_info);
Vector<bke::AttributeTransferData> curve_attributes = bke::retrieve_attributes_for_transfer(
curves.attributes(),
new_curves.attributes_for_write(),
ATTR_DOMAIN_MASK_CURVE,
propagation_info);
threading::parallel_invoke(
256 < new_point_count * new_curve_count,
/* Initialize curve offsets. */
[&]() { new_curves.offsets_for_write().copy_from(new_curve_offsets); },
[&]() {
/* Copy over point attributes. */
for (bke::AttributeTransferData &attribute : point_attributes) {
threading::parallel_for(copy_point_ranges.index_range(), 128, [&](IndexRange range) {
for (const int range_i : range) {
const IndexRange src_range = copy_point_ranges[range_i];
copy_between_buffers(attribute.src.type(),
attribute.src.data(),
attribute.dst.span.data(),
src_range,
{copy_point_range_dst_offsets[range_i], src_range.size()});
}
});
}
},
[&]() {
/* Copy over curve attributes.
2022-08-12 12:38:54 +10:00
* In some cases points are just dissolved, so the number of
* curves will be the same. That could be optimized in the future. */
for (bke::AttributeTransferData &attribute : curve_attributes) {
if (new_curves.curves_num() == curves.curves_num()) {
attribute.dst.span.copy_from(attribute.src);
}
else {
copy_with_map(attribute.src, new_curve_orig_indices, attribute.dst.span);
}
}
});
for (bke::AttributeTransferData &attribute : point_attributes) {
attribute.dst.finish();
}
for (bke::AttributeTransferData &attribute : curve_attributes) {
attribute.dst.finish();
}
if (new_curves.curves_num() != curves.curves_num()) {
new_curves.remove_attributes_based_on_types();
}
return new_curves;
}
void CurvesGeometry::remove_points(const IndexMask points_to_delete,
const AnonymousAttributePropagationInfo &propagation_info)
{
if (points_to_delete.is_empty()) {
return;
}
if (points_to_delete.size() == this->points_num()) {
*this = {};
}
*this = copy_with_removed_points(*this, points_to_delete, propagation_info);
}
static CurvesGeometry copy_with_removed_curves(
const CurvesGeometry &curves,
const IndexMask curves_to_delete,
const AnonymousAttributePropagationInfo &propagation_info)
{
const OffsetIndices old_points_by_curve = curves.points_by_curve();
const Span<int> old_offsets = curves.offsets();
const Vector<IndexRange> old_curve_ranges = curves_to_delete.extract_ranges_invert(
curves.curves_range(), nullptr);
Vector<IndexRange> new_curve_ranges;
Vector<IndexRange> old_point_ranges;
Vector<IndexRange> new_point_ranges;
int new_tot_points = 0;
int new_tot_curves = 0;
for (const IndexRange &curve_range : old_curve_ranges) {
new_curve_ranges.append(IndexRange(new_tot_curves, curve_range.size()));
new_tot_curves += curve_range.size();
const IndexRange old_point_range = old_points_by_curve[curve_range];
old_point_ranges.append(old_point_range);
new_point_ranges.append(IndexRange(new_tot_points, old_point_range.size()));
new_tot_points += old_point_range.size();
}
CurvesGeometry new_curves{new_tot_points, new_tot_curves};
Vector<bke::AttributeTransferData> point_attributes = bke::retrieve_attributes_for_transfer(
curves.attributes(),
new_curves.attributes_for_write(),
ATTR_DOMAIN_MASK_POINT,
propagation_info);
Vector<bke::AttributeTransferData> curve_attributes = bke::retrieve_attributes_for_transfer(
curves.attributes(),
new_curves.attributes_for_write(),
ATTR_DOMAIN_MASK_CURVE,
propagation_info);
threading::parallel_invoke(
256 < new_tot_points * new_tot_curves,
/* Initialize curve offsets. */
[&]() {
MutableSpan<int> new_offsets = new_curves.offsets_for_write();
new_offsets.last() = new_tot_points;
threading::parallel_for(
old_curve_ranges.index_range(), 128, [&](const IndexRange ranges_range) {
for (const int range_i : ranges_range) {
const IndexRange old_curve_range = old_curve_ranges[range_i];
const IndexRange new_curve_range = new_curve_ranges[range_i];
const IndexRange old_point_range = old_point_ranges[range_i];
const IndexRange new_point_range = new_point_ranges[range_i];
const int offset_shift = new_point_range.start() - old_point_range.start();
const int curves_in_range = old_curve_range.size();
threading::parallel_for(
IndexRange(curves_in_range), 512, [&](const IndexRange range) {
for (const int i : range) {
const int old_curve_i = old_curve_range[i];
const int new_curve_i = new_curve_range[i];
const int old_offset = old_offsets[old_curve_i];
const int new_offset = old_offset + offset_shift;
new_offsets[new_curve_i] = new_offset;
}
});
}
});
},
[&]() {
/* Copy over point attributes. */
for (bke::AttributeTransferData &attribute : point_attributes) {
threading::parallel_for(old_curve_ranges.index_range(), 128, [&](IndexRange range) {
for (const int range_i : range) {
copy_between_buffers(attribute.src.type(),
attribute.src.data(),
attribute.dst.span.data(),
old_point_ranges[range_i],
new_point_ranges[range_i]);
}
});
}
},
[&]() {
/* Copy over curve attributes. */
for (bke::AttributeTransferData &attribute : curve_attributes) {
threading::parallel_for(old_curve_ranges.index_range(), 128, [&](IndexRange range) {
for (const int range_i : range) {
copy_between_buffers(attribute.src.type(),
attribute.src.data(),
attribute.dst.span.data(),
old_curve_ranges[range_i],
new_curve_ranges[range_i]);
}
});
}
});
for (bke::AttributeTransferData &attribute : point_attributes) {
attribute.dst.finish();
}
for (bke::AttributeTransferData &attribute : curve_attributes) {
attribute.dst.finish();
}
new_curves.remove_attributes_based_on_types();
return new_curves;
}
void CurvesGeometry::remove_curves(const IndexMask curves_to_delete,
const AnonymousAttributePropagationInfo &propagation_info)
{
if (curves_to_delete.is_empty()) {
return;
}
if (curves_to_delete.size() == this->curves_num()) {
*this = {};
2022-06-20 16:21:31 +02:00
return;
}
*this = copy_with_removed_curves(*this, curves_to_delete, propagation_info);
}
template<typename T>
static void reverse_curve_point_data(const CurvesGeometry &curves,
const IndexMask curve_selection,
MutableSpan<T> data)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
threading::parallel_for(curve_selection.index_range(), 256, [&](IndexRange range) {
for (const int curve_i : curve_selection.slice(range)) {
data.slice(points_by_curve[curve_i]).reverse();
}
});
}
template<typename T>
static void reverse_swap_curve_point_data(const CurvesGeometry &curves,
const IndexMask curve_selection,
MutableSpan<T> data_a,
MutableSpan<T> data_b)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
threading::parallel_for(curve_selection.index_range(), 256, [&](IndexRange range) {
for (const int curve_i : curve_selection.slice(range)) {
const IndexRange points = points_by_curve[curve_i];
MutableSpan<T> a = data_a.slice(points);
MutableSpan<T> b = data_b.slice(points);
for (const int i : IndexRange(points.size() / 2)) {
const int end_index = points.size() - 1 - i;
std::swap(a[end_index], b[i]);
std::swap(b[end_index], a[i]);
}
if (points.size() % 2) {
const int64_t middle_index = points.size() / 2;
std::swap(a[middle_index], b[middle_index]);
}
}
});
}
void CurvesGeometry::reverse_curves(const IndexMask curves_to_reverse)
{
Set<StringRef> bezier_handle_names{{ATTR_HANDLE_POSITION_LEFT,
ATTR_HANDLE_POSITION_RIGHT,
ATTR_HANDLE_TYPE_LEFT,
ATTR_HANDLE_TYPE_RIGHT}};
MutableAttributeAccessor attributes = this->attributes_for_write();
attributes.for_all([&](const AttributeIDRef &id, AttributeMetaData meta_data) {
if (meta_data.domain != ATTR_DOMAIN_POINT) {
return true;
}
if (meta_data.data_type == CD_PROP_STRING) {
return true;
}
if (bezier_handle_names.contains(id.name())) {
return true;
}
GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id);
attribute_math::convert_to_static_type(attribute.span.type(), [&](auto dummy) {
using T = decltype(dummy);
reverse_curve_point_data<T>(*this, curves_to_reverse, attribute.span.typed<T>());
});
attribute.finish();
return true;
});
/* In order to maintain the shape of Bezier curves, handle attributes must reverse, but also the
* values for the left and right must swap. Use a utility to swap and reverse at the same time,
* to avoid loading the attribute twice. Generally we can expect the right layer to exist when
* the left does, but there's no need to count on it, so check for both attributes. */
if (attributes.contains(ATTR_HANDLE_POSITION_LEFT) &&
attributes.contains(ATTR_HANDLE_POSITION_RIGHT)) {
reverse_swap_curve_point_data(*this,
curves_to_reverse,
this->handle_positions_left_for_write(),
this->handle_positions_right_for_write());
}
if (attributes.contains(ATTR_HANDLE_TYPE_LEFT) && attributes.contains(ATTR_HANDLE_TYPE_RIGHT)) {
reverse_swap_curve_point_data(*this,
curves_to_reverse,
this->handle_types_left_for_write(),
this->handle_types_right_for_write());
}
this->tag_topology_changed();
}
void CurvesGeometry::remove_attributes_based_on_types()
{
MutableAttributeAccessor attributes = this->attributes_for_write();
if (!this->has_curve_with_type(CURVE_TYPE_BEZIER)) {
attributes.remove(ATTR_HANDLE_TYPE_LEFT);
attributes.remove(ATTR_HANDLE_TYPE_RIGHT);
attributes.remove(ATTR_HANDLE_POSITION_LEFT);
attributes.remove(ATTR_HANDLE_POSITION_RIGHT);
}
if (!this->has_curve_with_type(CURVE_TYPE_NURBS)) {
attributes.remove(ATTR_NURBS_WEIGHT);
attributes.remove(ATTR_NURBS_ORDER);
attributes.remove(ATTR_NURBS_KNOTS_MODE);
}
if (!this->has_curve_with_type({CURVE_TYPE_BEZIER, CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_NURBS})) {
attributes.remove(ATTR_RESOLUTION);
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Domain Interpolation
* \{ */
/**
* Mix together all of a curve's control point values.
*
* \note Theoretically this interpolation does not need to compute all values at once.
* However, doing that makes the implementation simpler, and this can be optimized in the future if
* only some values are required.
*/
template<typename T>
static void adapt_curve_domain_point_to_curve_impl(const CurvesGeometry &curves,
const VArray<T> &old_values,
MutableSpan<T> r_values)
{
attribute_math::DefaultMixer<T> mixer(r_values);
const OffsetIndices points_by_curve = curves.points_by_curve();
threading::parallel_for(curves.curves_range(), 128, [&](const IndexRange range) {
for (const int i_curve : range) {
for (const int i_point : points_by_curve[i_curve]) {
mixer.mix_in(i_curve, old_values[i_point]);
}
}
mixer.finalize(range);
});
}
/**
* A curve is selected if all of its control points were selected.
*
* \note Theoretically this interpolation does not need to compute all values at once.
* However, doing that makes the implementation simpler, and this can be optimized in the future if
* only some values are required.
*/
template<>
void adapt_curve_domain_point_to_curve_impl(const CurvesGeometry &curves,
const VArray<bool> &old_values,
MutableSpan<bool> r_values)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
r_values.fill(true);
for (const int i_curve : IndexRange(curves.curves_num())) {
for (const int i_point : points_by_curve[i_curve]) {
if (!old_values[i_point]) {
r_values[i_curve] = false;
break;
}
}
}
}
static GVArray adapt_curve_domain_point_to_curve(const CurvesGeometry &curves,
const GVArray &varray)
{
GVArray new_varray;
attribute_math::convert_to_static_type(varray.type(), [&](auto dummy) {
using T = decltype(dummy);
if constexpr (!std::is_void_v<attribute_math::DefaultMixer<T>>) {
Array<T> values(curves.curves_num());
adapt_curve_domain_point_to_curve_impl<T>(curves, varray.typed<T>(), values);
new_varray = VArray<T>::ForContainer(std::move(values));
}
});
return new_varray;
}
/**
* Copy the value from a curve to all of its points.
*
* \note Theoretically this interpolation does not need to compute all values at once.
* However, doing that makes the implementation simpler, and this can be optimized in the future if
* only some values are required.
*/
template<typename T>
static void adapt_curve_domain_curve_to_point_impl(const CurvesGeometry &curves,
const VArray<T> &old_values,
MutableSpan<T> r_values)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
for (const int i_curve : IndexRange(curves.curves_num())) {
r_values.slice(points_by_curve[i_curve]).fill(old_values[i_curve]);
}
}
static GVArray adapt_curve_domain_curve_to_point(const CurvesGeometry &curves,
const GVArray &varray)
{
GVArray new_varray;
attribute_math::convert_to_static_type(varray.type(), [&](auto dummy) {
using T = decltype(dummy);
Array<T> values(curves.points_num());
adapt_curve_domain_curve_to_point_impl<T>(curves, varray.typed<T>(), values);
new_varray = VArray<T>::ForContainer(std::move(values));
});
return new_varray;
}
GVArray CurvesGeometry::adapt_domain(const GVArray &varray,
const eAttrDomain from,
const eAttrDomain to) const
{
if (!varray) {
return {};
}
if (varray.is_empty()) {
return {};
}
if (from == to) {
return varray;
}
if (varray.is_single()) {
BUFFER_FOR_CPP_TYPE_VALUE(varray.type(), value);
varray.get_internal_single(value);
return GVArray::ForSingle(varray.type(), this->attributes().domain_size(to), value);
}
if (from == ATTR_DOMAIN_POINT && to == ATTR_DOMAIN_CURVE) {
return adapt_curve_domain_point_to_curve(*this, varray);
}
if (from == ATTR_DOMAIN_CURVE && to == ATTR_DOMAIN_POINT) {
return adapt_curve_domain_curve_to_point(*this, varray);
}
BLI_assert_unreachable();
return {};
}
/** \} */
} // namespace blender::bke