Improved Surface Deform performance #116545

Open
Eugene-Kuznetsov wants to merge 3 commits from Eugene-Kuznetsov/blender:ek_surface_deform into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
16 changed files with 571 additions and 361 deletions

View File

@ -198,7 +198,7 @@ class CurvesGeometry : public ::CurvesGeometry {
Array<int> point_to_curve_map() const;
Span<float3> positions() const;
MutableSpan<float3> positions_for_write();
MutableSpan<float3> positions_for_write(bool preserve = true);
/** Whether the curve loops around to connect to itself, on the curve domain. */
VArray<bool> cyclic() const;
@ -727,7 +727,18 @@ void interpolate_to_evaluated(const GSpan src,
const OffsetIndices<int> evaluated_offsets,
GMutableSpan dst);
float4 calculate_basis(const float parameter);
inline float4 calculate_basis(const float parameter)
{
/* Adapted from Cycles #catmull_rom_basis_eval function. */
const float t = parameter;
const float s = 1.0f - parameter;
return {
-t * s * s,
2.0f + t * t * (3.0f * t - 5.0f),
2.0f + s * s * (3.0f * s - 5.0f),
-s * t * t,
};
}
/**
* Interpolate the control point values for the given parameter on the piecewise segment.

View File

@ -525,7 +525,8 @@ const void *CustomData_get_layer_named(const CustomData *data,
void *CustomData_get_layer_named_for_write(CustomData *data,
eCustomDataType type,
blender::StringRef name,
int totelem);
int totelem,
bool preserve_data = true);
int CustomData_get_offset(const CustomData *data, eCustomDataType type);
int CustomData_get_offset_named(const CustomData *data,

View File

@ -24,19 +24,6 @@ int calculate_evaluated_num(const int points_num, const bool cyclic, const int r
return eval_num + 1;
}
float4 calculate_basis(const float parameter)
{
/* Adapted from Cycles #catmull_rom_basis_eval function. */
const float t = parameter;
const float s = 1.0f - parameter;
return {
-t * s * s,
2.0f + t * t * (3.0f * t - 5.0f),
2.0f + s * s * (3.0f * s - 5.0f),
-s * t * t,
};
}
template<typename T>
static void evaluate_segment(const T &a, const T &b, const T &c, const T &d, MutableSpan<T> dst)
{

View File

@ -241,7 +241,8 @@ template<typename T>
static MutableSpan<T> get_mutable_attribute(CurvesGeometry &curves,
const AttrDomain domain,
const StringRef name,
const T default_value = T())
const T default_value = T(),
bool preserve = true)
{
const int num = domain_num(curves, domain);
if (num <= 0) {
@ -250,13 +251,13 @@ static MutableSpan<T> get_mutable_attribute(CurvesGeometry &curves,
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, num);
T *data = (T *)CustomData_get_layer_named_for_write(&custom_data, type, name, num, preserve);
if (data != nullptr) {
return {data, num};
}
data = (T *)CustomData_add_layer_named(&custom_data, type, CD_SET_DEFAULT, num, name);
MutableSpan<T> span = {data, num};
if (num > 0 && span.first() != default_value) {
if (preserve && num > 0 && span.first() != default_value) {
span.fill(default_value);
}
return span;
@ -347,9 +348,10 @@ Span<float3> CurvesGeometry::positions() const
{
return get_span_attribute<float3>(*this, AttrDomain::Point, ATTR_POSITION);
}
MutableSpan<float3> CurvesGeometry::positions_for_write()
MutableSpan<float3> CurvesGeometry::positions_for_write(bool preserve)
{
return get_mutable_attribute<float3>(*this, AttrDomain::Point, ATTR_POSITION);
return get_mutable_attribute<float3>(
*this, AttrDomain::Point, ATTR_POSITION, float3(), preserve);
}
Span<int> CurvesGeometry::offsets() const

View File

@ -2354,11 +2354,16 @@ static bool customdata_typemap_is_valid(const CustomData *data)
}
#endif
static void *copy_layer_data(const eCustomDataType type, const void *data, const int totelem)
static void *copy_layer_data(const eCustomDataType type,
const void *data,
const int totelem,
bool preserve_data = true)
{
const LayerTypeInfo &type_info = *layerType_getInfo(type);
const int64_t size_in_bytes = int64_t(totelem) * type_info.size;
void *new_data = MEM_mallocN_aligned(size_in_bytes, type_info.alignment, __func__);
if (!preserve_data)
return new_data;
if (type_info.copy) {
type_info.copy(data, new_data, totelem);
}
@ -2560,7 +2565,9 @@ static const ImplicitSharingInfo *make_implicit_sharing_info_for_layer(const eCu
/**
* If the layer data is currently shared (hence it is immutable), create a copy that can be edited.
*/
static void ensure_layer_data_is_mutable(CustomDataLayer &layer, const int totelem)
static void ensure_layer_data_is_mutable(CustomDataLayer &layer,
const int totelem,
bool preserve_data = true)
{
if (layer.data == nullptr) {
return;
@ -2577,7 +2584,7 @@ static void ensure_layer_data_is_mutable(CustomDataLayer &layer, const int totel
const void *old_data = layer.data;
/* Copy the layer before removing the user because otherwise the data might be freed while
* we're still copying from it here. */
layer.data = copy_layer_data(type, old_data, totelem);
layer.data = copy_layer_data(type, old_data, totelem, preserve_data);
layer.sharing_info->remove_user_and_delete_if_last();
layer.sharing_info = make_implicit_sharing_info_for_layer(type, layer.data, totelem);
}
@ -3773,14 +3780,15 @@ const void *CustomData_get_layer_named(const CustomData *data,
void *CustomData_get_layer_named_for_write(CustomData *data,
const eCustomDataType type,
const StringRef name,
const int totelem)
const int totelem,
bool preserve_data)
{
const int layer_index = CustomData_get_named_layer_index(data, type, name);
if (layer_index == -1) {
return nullptr;
}
CustomDataLayer &layer = data->layers[layer_index];
ensure_layer_data_is_mutable(layer, totelem);
ensure_layer_data_is_mutable(layer, totelem, preserve_data);
return layer.data;
}

View File

@ -1010,7 +1010,7 @@ void barycentric_weights_v2_quad(const float v1[2],
/**
* \return false for degenerated triangles.
*/
bool barycentric_coords_v2(
MINLINE bool barycentric_coords_v2(
const float v1[2], const float v2[2], const float v3[2], const float co[2], float w[3]);
/**
* \return

View File

@ -434,9 +434,9 @@ MINLINE bool is_zero_v2_db(const double v[2]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_zero_v3_db(const double v[3]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_zero_v4_db(const double v[4]) ATTR_WARN_UNUSED_RESULT;
bool is_finite_v2(const float v[2]) ATTR_WARN_UNUSED_RESULT;
bool is_finite_v3(const float v[3]) ATTR_WARN_UNUSED_RESULT;
bool is_finite_v4(const float v[4]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_finite_v2(const float v[2]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_finite_v3(const float v[3]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_finite_v4(const float v[4]) ATTR_WARN_UNUSED_RESULT;
MINLINE bool is_one_v3(const float v[3]) ATTR_WARN_UNUSED_RESULT;

View File

@ -3673,30 +3673,6 @@ int barycentric_inside_triangle_v2(const float w[3])
return 0;
}
bool barycentric_coords_v2(
const float v1[2], const float v2[2], const float v3[2], const float co[2], float w[3])
{
const float x = co[0], y = co[1];
const float x1 = v1[0], y1 = v1[1];
const float x2 = v2[0], y2 = v2[1];
const float x3 = v3[0], y3 = v3[1];
const float det = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
#ifndef NDEBUG /* Avoid floating point exception when debugging. */
if (det != 0.0f)
#endif
{
w[0] = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / det;
w[1] = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / det;
w[2] = 1.0f - w[0] - w[1];
if (is_finite_v3(w)) {
return true;
}
}
return false;
}
void barycentric_weights_v2(
const float v1[2], const float v2[2], const float v3[2], const float co[2], float w[3])
{

View File

@ -166,6 +166,36 @@ MINLINE float shell_v2v2_mid_normalized_to_dist(const float a[2], const float b[
return (UNLIKELY(angle_cos < SMALL_NUMBER)) ? 1.0f : (1.0f / angle_cos);
}
MINLINE bool is_finite_v3_inline(const float v[3])
{
return (isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]));
}
MINLINE bool barycentric_coords_v2(
const float v1[2], const float v2[2], const float v3[2], const float co[2], float w[3])
{
const float x = co[0], y = co[1];
const float x1 = v1[0], y1 = v1[1];
const float x2 = v2[0], y2 = v2[1];
const float x3 = v3[0], y3 = v3[1];
float det = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3);
#ifndef NDEBUG /* Avoid floating point exception when debugging. */
if (det != 0.0f)
#endif
{
det = 1.0f / det;
w[0] = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) * det;
w[1] = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) * det;
w[2] = 1.0f - w[0] - w[1];
if (is_finite_v3_inline(w)) {
return true;
}
}
return false;
}
#undef SMALL_NUMBER
#endif /* __MATH_GEOM_INLINE_C__ */

View File

@ -342,27 +342,6 @@ void flip_v2_v2v2(float v[2], const float v1[2], const float v2[2])
/** \} */
/* -------------------------------------------------------------------- */
/** \name Comparison
* \{ */
bool is_finite_v2(const float v[2])
{
return (isfinite(v[0]) && isfinite(v[1]));
}
bool is_finite_v3(const float v[3])
{
return (isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]));
}
bool is_finite_v4(const float v[4])
{
return (isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]) && isfinite(v[3]));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Angles
* \{ */

View File

@ -1305,6 +1305,21 @@ MINLINE bool is_zero_v4_db(const double v[4])
return (v[0] == 0.0 && v[1] == 0.0 && v[2] == 0.0 && v[3] == 0.0);
}
MINLINE bool is_finite_v2(const float v[2])
{
return (isfinite(v[0]) && isfinite(v[1]));
}
MINLINE bool is_finite_v3(const float v[3])
{
return (isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]));
}
MINLINE bool is_finite_v4(const float v[4])
{
return (isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]) && isfinite(v[3]));
}
MINLINE bool is_one_v3(const float v[3])
{
return (v[0] == 1.0f && v[1] == 1.0f && v[2] == 1.0f);

View File

@ -9,6 +9,8 @@
#include "BLI_math_vector_types.hh"
#include "BLI_multi_value_map.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
#include "BLI_offset_indices.hh"
namespace blender::geometry {
@ -24,12 +26,20 @@ class ReverseUVSampler {
private:
Span<float2> uv_map_;

You should not use std::vector instead of blender::Vector without strong reason.
You should not use Vector<Vector<A>> instead of Array<int> offsets; Array<A> data; GroupedSpan<A> values(offsets, data); without strong reason (see as examples: #110707. #115142, #111081, #114683).

You should not use `std::vector` instead of `blender::Vector` without strong reason. You should not use `Vector<Vector<A>>` instead of `Array<int> offsets; Array<A> data; GroupedSpan<A> values(offsets, data);` without strong reason (see as examples: #110707. #115142, #111081, #114683).

I've switched to blender::Vector. Array/GroupedSpan can't be used here.

I've switched to blender::Vector. Array/GroupedSpan can't be used here.
Span<int3> corner_tris_;
int resolution_;
std::unique_ptr<LookupGrid> lookup_grid_;
int2 resolution_;
int2 span_;
int live_count_;
Vector<int> corner_tris_by_cell_;
Vector<int> cell_populations_;
float2 uv_base_, uv_max_, supp_uv_min_, supp_uv_max_;
public:
ReverseUVSampler(Span<float2> uv_map, Span<int3> corner_tris);
~ReverseUVSampler();
ReverseUVSampler(Span<float2> uv_map,
Span<int3> corner_tris,
float2 supp_uv_min = float2{0.0f, 0.0f},
float2 supp_uv_max = float2{0.0f, 0.0f});
enum class ResultType {
None,
@ -43,8 +53,16 @@ class ReverseUVSampler {
float3 bary_weights;
};
Result sample(const float2 &query_uv) const;
void sample_many(Span<float2> query_uvs, MutableSpan<Result> r_results) const;
void sample(const float2 &query_uv, Result &result, bool hint = false) const;
Result sample(const float2 &query_uv) const
{
Result result;
sample(query_uv, result);
return result;
}
void sample_many(Span<float2> query_uvs,
MutableSpan<Result> r_results,
bool hints = false) const;
};
} // namespace blender::geometry

View File

@ -2,8 +2,9 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include <fmt/format.h>
#include <map>
#include <mutex>
#include <random>
#include "GEO_reverse_uv_sampler.hh"
@ -19,279 +20,228 @@
namespace blender::geometry {
struct Row {
/** The min and max horizontal cell index that is used in this row. */
int x_min = 0;
int x_max = 0;
/** Offsets into the array of indices below. Also see #OffsetIndices. */
Array<int> offsets;
/** A flat array containing the triangle indices contained in each cell. */
Array<int> tri_indices;
};
struct ReverseUVSampler::LookupGrid {
/** Minimum vertical cell index that contains triangles. */
int y_min = 0;
/** Information about all rows starting at `y_min`. */
Array<Row> rows;
};
struct TriWithRange {
int tri_index;
int x_min;
int x_max;
};
struct LocalRowData {
linear_allocator::ChunkedList<TriWithRange, 8> tris;
int x_min = INT32_MAX;
int x_max = INT32_MIN;
};
struct LocalData {
LinearAllocator<> allocator;
Map<int, destruct_ptr<LocalRowData>> rows;
};
static int2 uv_to_cell(const float2 &uv, const int resolution)
static inline int2 uv_to_cell_key(const float2 &uv,
const float2 &uv_base,
const int2 &resolution,
int2 span)
{
return int2{uv * resolution};
auto key = int2{int((uv.x - uv_base.x) * resolution.x), int((uv.y - uv_base.y) * resolution.y)};
return int2{std::max(0, std::min(span.x - 1, key.x)), std::max(0, std::min(span.y - 1, key.y))};
}
static Bounds<int2> tri_to_cell_bounds(const int3 &tri,
const int resolution,
const Span<float2> uv_map)
ReverseUVSampler::ReverseUVSampler(const Span<float2> uv_map,
const Span<int3> corner_tris,
Eugene-Kuznetsov marked this conversation as resolved Outdated
`uint8_t` -> `int8_t` (maybe even `bool`?) https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Integer_Types
float2 supp_uv_min,
float2 supp_uv_max)
: uv_map_(uv_map),
corner_tris_(corner_tris),
resolution_(int2{0, 0}),
supp_uv_min_(supp_uv_min),
supp_uv_max_(supp_uv_max)
{
const float2 &uv_0 = uv_map[tri[0]];
const float2 &uv_1 = uv_map[tri[1]];
const float2 &uv_2 = uv_map[tri[2]];
const int2 cell_0 = uv_to_cell(uv_0, resolution);
const int2 cell_1 = uv_to_cell(uv_1, resolution);
const int2 cell_2 = uv_to_cell(uv_2, resolution);
const int2 min_cell = math::min(math::min(cell_0, cell_1), cell_2);
const int2 max_cell = math::max(math::max(cell_0, cell_1), cell_2);
return {min_cell, max_cell};
}
/**
* Add each triangle to the rows that it is in. After this, the information about each row is still
* scattered across multiple thread-specific lists. Those separate lists are then joined in a
* separate step.
*/
static void sort_tris_into_rows(const Span<float2> uv_map,
const Span<int3> corner_tris,
const int resolution,
threading::EnumerableThreadSpecific<LocalData> &data_per_thread)
{
threading::parallel_for(corner_tris.index_range(), 256, [&](const IndexRange tris_range) {
LocalData &local_data = data_per_thread.local();
for (const int tri_i : tris_range) {
const int3 &tri = corner_tris[tri_i];
/* Compute the cells that the triangle touches approximately. */
const Bounds<int2> cell_bounds = tri_to_cell_bounds(tri, resolution, uv_map);
const TriWithRange tri_with_range{tri_i, cell_bounds.min.x, cell_bounds.max.x};
/* Go over each row that the triangle is in. */
for (int cell_y = cell_bounds.min.y; cell_y <= cell_bounds.max.y; cell_y++) {
LocalRowData &row = *local_data.rows.lookup_or_add_cb(
cell_y, [&]() { return local_data.allocator.construct<LocalRowData>(); });
row.tris.append(local_data.allocator, tri_with_range);
row.x_min = std::min<int>(row.x_min, cell_bounds.min.x);
row.x_max = std::max<int>(row.x_max, cell_bounds.max.x);
}
}
});
}
/**
* Consolidates the data that has been gather for each row so that it is each to look up which
* triangles are in each cell.
*/
static void finish_rows(const Span<int> all_ys,
const Span<const LocalData *> local_data_vec,
const Bounds<int> y_bounds,
ReverseUVSampler::LookupGrid &lookup_grid)
{
threading::parallel_for(all_ys.index_range(), 8, [&](const IndexRange all_ys_range) {
Vector<const LocalRowData *, 32> local_rows;
for (const int y : all_ys.slice(all_ys_range)) {
Row &row = lookup_grid.rows[y - y_bounds.min];
local_rows.clear();
for (const LocalData *local_data : local_data_vec) {
if (const destruct_ptr<LocalRowData> *local_row = local_data->rows.lookup_ptr(y)) {
local_rows.append(local_row->get());
}
}
int x_min = INT32_MAX;
int x_max = INT32_MIN;
for (const LocalRowData *local_row : local_rows) {
x_min = std::min(x_min, local_row->x_min);
x_max = std::max(x_max, local_row->x_max);
}
const int x_num = x_max - x_min + 1;
row.offsets.reinitialize(x_num + 1);
{
/* Count how many triangles are in each cell in the current row. */
MutableSpan<int> counts = row.offsets;
counts.fill(0);
for (const LocalRowData *local_row : local_rows) {
for (const TriWithRange &tri_with_range : local_row->tris) {
for (int x = tri_with_range.x_min; x <= tri_with_range.x_max; x++) {
counts[x - x_min]++;
}
}
}
offset_indices::accumulate_counts_to_offsets(counts);
}
const int tri_indices_num = row.offsets.last();
row.tri_indices.reinitialize(tri_indices_num);
/* Populate the array containing all triangle indices in all cells in this row. */
Array<int, 1000> current_offsets(x_num, 0);
for (const LocalRowData *local_row : local_rows) {
for (const TriWithRange &tri_with_range : local_row->tris) {
for (int x = tri_with_range.x_min; x <= tri_with_range.x_max; x++) {
const int offset_x = x - x_min;
row.tri_indices[row.offsets[offset_x] + current_offsets[offset_x]] =
tri_with_range.tri_index;
current_offsets[offset_x]++;
}
}
}
row.x_min = x_min;
row.x_max = x_max;
}
});
}
ReverseUVSampler::ReverseUVSampler(const Span<float2> uv_map, const Span<int3> corner_tris)
: uv_map_(uv_map), corner_tris_(corner_tris), lookup_grid_(std::make_unique<LookupGrid>())
{
/* A lower resolution means that there will be fewer cells and more triangles in each cell. Fewer
* cells make construction faster, but more triangles per cell make lookup slower. This value
* needs to be determined experimentally. */
resolution_ = std::max<int>(3, std::sqrt(corner_tris.size()) * 3);
if (corner_tris.is_empty()) {
if (corner_tris_.size() == 0)
return;
std::string format = "ReverseUVSampler::ReverseUVSampler(" +
std::to_string(corner_tris_.size()) + ")";
SCOPED_TIMER_AVERAGED(format);
const int3 &tri = corner_tris_[0];
const float2 &uv_0 = uv_map_[tri[0]];
float2 min_uv, max_uv;
min_uv = max_uv = uv_0;
int live_count = 0;
Vector<int8_t> mask(corner_tris_.size());
bool support_set = (supp_uv_max_.x > supp_uv_min_.x && supp_uv_max_.y > supp_uv_min_.y);
Eugene-Kuznetsov marked this conversation as resolved Outdated

math::max<int>

`math::max<int>`
/* We walk through the list of triangles three times.
* 1st pass: calculate the bounding box and determine which triangles are
* to be used for sampling. Set the cell resolution.
* 2nd pass: calculate how many triangles hit each cell. Allocate the index table.
* 3rd pass: populate the index table. */
for (const int tri_i : corner_tris_.index_range()) {
const int3 &tri = corner_tris_[tri_i];
const float2 &uv_0 = uv_map_[tri[0]];
const float2 &uv_1 = uv_map_[tri[1]];
const float2 &uv_2 = uv_map_[tri[2]];
float2 tri_min_uv = math::min(math::min(uv_0, uv_1), uv_2);
float2 tri_max_uv = math::max(math::max(uv_0, uv_1), uv_2);
if ((!support_set) || (tri_max_uv.x >= supp_uv_min_.x && tri_min_uv.x <= supp_uv_max_.x &&
tri_max_uv.y >= supp_uv_min_.y && tri_min_uv.y <= supp_uv_max_.y)) {
live_count++;
mask[tri_i] = 1;
min_uv = math::min(min_uv, tri_min_uv);
max_uv = math::max(max_uv, tri_max_uv);
} else {
mask[tri_i] = 0;
}
}
threading::EnumerableThreadSpecific<LocalData> data_per_thread;
sort_tris_into_rows(uv_map_, corner_tris_, resolution_, data_per_thread);
/* Balance between construction time and evaluation time. Smaller values
* lead to faster construction (fewer cells) but slower lookups. At 1.0,
* we expect ~2-4 triangles per cell. */
float magic_number = 0.5f;
float mult = magic_number * sqrt(float(live_count));
live_count_ = live_count;
VectorSet<int> all_ys;
Vector<const LocalData *> local_data_vec;
for (const LocalData &local_data : data_per_thread) {
local_data_vec.append(&local_data);
for (const int y : local_data.rows.keys()) {
all_ys.add(y);
}
}
resolution_.x = math::max<int>(1, mult / (max_uv[0] - min_uv[0] + 1e-6));
resolution_.y = math::max<int>(1, mult / (max_uv[1] - min_uv[1] + 1e-6));
const Bounds<int> y_bounds = *bounds::min_max(all_ys.as_span());
lookup_grid_->y_min = y_bounds.min;
uv_base_ = min_uv;
uv_max_ = max_uv;
span_ = int2{int((max_uv.x - min_uv.x) * resolution_.x),
int((max_uv.y - min_uv.y) * resolution_.y)};
span_.x += 1;
span_.y += 1;
const int rows_num = y_bounds.max - y_bounds.min + 1;
lookup_grid_->rows.reinitialize(rows_num);
cell_populations_.resize(span_.x * span_.y + 1, 0);
finish_rows(all_ys, local_data_vec, y_bounds, *lookup_grid_);
}
for (const int tri_i : corner_tris_.index_range()) {
if (!mask[tri_i])
continue;
static Span<int> lookup_tris_in_cell(const int2 cell,
const ReverseUVSampler::LookupGrid &lookup_grid)
{
if (cell.y < lookup_grid.y_min) {
return {};
}
if (cell.y >= lookup_grid.y_min + lookup_grid.rows.size()) {
return {};
}
const Row &row = lookup_grid.rows[cell.y - lookup_grid.y_min];
if (cell.x < row.x_min) {
return {};
}
if (cell.x > row.x_max) {
return {};
}
const int offset = row.offsets[cell.x - row.x_min];
const int tris_num = row.offsets[cell.x - row.x_min + 1] - offset;
return row.tri_indices.as_span().slice(offset, tris_num);
}
ReverseUVSampler::Result ReverseUVSampler::sample(const float2 &query_uv) const
{
const int2 cell = uv_to_cell(query_uv, resolution_);
const Span<int> tri_indices = lookup_tris_in_cell(cell, *lookup_grid_);
float best_dist = FLT_MAX;
float3 best_bary_weights;
int best_tri_index;
/* The distance to an edge that is allowed to be inside or outside the triangle. Without this,
* the lookup can fail for floating point accuracy reasons when the uv is almost exact on an
* edge. */
const float edge_epsilon = 0.00001f;
for (const int tri_i : tri_indices) {
const int3 &tri = corner_tris_[tri_i];
const float2 &uv_0 = uv_map_[tri[0]];
const float2 &uv_1 = uv_map_[tri[1]];
const float2 &uv_2 = uv_map_[tri[2]];
float3 bary_weights;
if (!barycentric_coords_v2(uv_0, uv_1, uv_2, query_uv, bary_weights)) {
continue;
float2 tri_min_uv = math::min(math::min(uv_0, uv_1), uv_2);
float2 tri_max_uv = math::max(math::max(uv_0, uv_1), uv_2);
const int2 min_key = uv_to_cell_key(tri_min_uv, uv_base_, resolution_, span_);
const int2 max_key = uv_to_cell_key(tri_max_uv, uv_base_, resolution_, span_);
for (int key_y = min_key.y; key_y <= max_key.y; key_y++) {
for (int key_x = min_key.x; key_x <= max_key.x; key_x++) {
cell_populations_[key_x + key_y * span_.x + 1]++;
}
}
}
/* If #query_uv is in the triangle, the distance is <= 0. Otherwise, the larger the distance,
* the further away the uv is from the triangle. */
const float x_dist = std::max(-bary_weights.x, bary_weights.x - 1.0f);
const float y_dist = std::max(-bary_weights.y, bary_weights.y - 1.0f);
const float z_dist = std::max(-bary_weights.z, bary_weights.z - 1.0f);
const float dist = std::max({x_dist, y_dist, z_dist});
int total_entries = 0;
for (int i = 0; i < span_.x * span_.y; i++) {
total_entries += cell_populations_[i + 1];
cell_populations_[i + 1] = total_entries;
}
corner_tris_by_cell_.resize(total_entries);
if (dist <= 0.0f && best_dist <= 0.0f) {
const float worse_dist = std::max(dist, best_dist);
/* Allow ignoring multiple triangle intersections if the uv is almost exactly on an edge. */
if (worse_dist < -edge_epsilon) {
/* The uv sample is in multiple triangles. */
return Result{ResultType::Multiple};
for (const int tri_i : corner_tris_.index_range()) {
if (!mask[tri_i])
continue;
const int3 &tri = corner_tris_[tri_i];
const float2 &uv_0 = uv_map_[tri[0]];
const float2 &uv_1 = uv_map_[tri[1]];
const float2 &uv_2 = uv_map_[tri[2]];
float2 tri_min_uv = math::min(math::min(uv_0, uv_1), uv_2);
float2 tri_max_uv = math::max(math::max(uv_0, uv_1), uv_2);
const int2 min_key = uv_to_cell_key(tri_min_uv, uv_base_, resolution_, span_);
const int2 max_key = uv_to_cell_key(tri_max_uv, uv_base_, resolution_, span_);
for (int key_y = min_key.y; key_y <= max_key.y; key_y++) {
for (int key_x = min_key.x; key_x <= max_key.x; key_x++) {
int &addr = cell_populations_[key_x + key_y * span_.x];
corner_tris_by_cell_[addr] = tri_i;
addr++;
}
}
}
}
void ReverseUVSampler::sample(
const float2 &query_uv, ReverseUVSampler::Result &result, bool hint) const
{
if (hint && result.type == ResultType::Ok) {
const int tri_i = result.tri_index;
if (tri_i >= 0 && tri_i < corner_tris_.size()) {
const int3 &tri = corner_tris_[tri_i];
const float2 &uv_0 = uv_map_[tri[0]];
const float2 &uv_1 = uv_map_[tri[1]];
const float2 &uv_2 = uv_map_[tri[2]];
float3 bary_weights;
if (barycentric_coords_v2(uv_0, uv_1, uv_2, query_uv, bary_weights)) {
/* If #query_uv is in the triangle, the distance is <= 0. Otherwise, the larger the
* distance, the further away the uv is from the triangle. */
const float x_dist = std::max(-bary_weights.x, bary_weights.x - 1.0f);
const float y_dist = std::max(-bary_weights.y, bary_weights.y - 1.0f);
const float z_dist = std::max(-bary_weights.z, bary_weights.z - 1.0f);
const float dist = std::max(std::max(x_dist, y_dist), z_dist);
if (dist <= 0.0f) {
result = Result{ResultType::Ok, tri_i, math::clamp(bary_weights, 0.0f, 1.0f)};
return;
}
}
}
}
if (dist < best_dist) {
best_dist = dist;
best_bary_weights = bary_weights;
best_tri_index = tri_i;
const int2 cell_key = uv_to_cell_key(query_uv, uv_base_, resolution_, span_);
int cell = cell_key.x + cell_key.y * span_.x;
int start_pos = (cell == 0) ? 0 : cell_populations_[cell - 1];
int end_pos = cell_populations_[cell];
auto tri_indices = Span<int>(&corner_tris_by_cell_[start_pos], end_pos - start_pos);
float best_dist = FLT_MAX;
float3 best_bary_weights;
int best_tri_index;
/* The distance to an edge that is allowed to be inside or outside the triangle. Without this,
* the lookup can fail for floating point accuracy reasons when the uv is almost exact on an
* edge. */
const float edge_epsilon = 0.00001f;
for (const int tri_i : tri_indices) {
const int3 &tri = corner_tris_[tri_i];
const float2 &uv_0 = uv_map_[tri[0]];
const float2 &uv_1 = uv_map_[tri[1]];
const float2 &uv_2 = uv_map_[tri[2]];
float3 bary_weights;
if (!barycentric_coords_v2(uv_0, uv_1, uv_2, query_uv, bary_weights)) {
continue;
}
/* If #query_uv is in the triangle, the distance is <= 0. Otherwise, the larger the distance,
* the further away the uv is from the triangle. */
const float x_dist = std::max(-bary_weights.x, bary_weights.x - 1.0f);
const float y_dist = std::max(-bary_weights.y, bary_weights.y - 1.0f);
const float z_dist = std::max(-bary_weights.z, bary_weights.z - 1.0f);
const float dist = std::max({x_dist, y_dist, z_dist});
if (dist <= 0.0f && best_dist <= 0.0f) {
const float worse_dist = std::max(dist, best_dist);
/* Allow ignoring multiple triangle intersections if the uv is almost exactly on an edge.
*/
if (worse_dist < -edge_epsilon) {
/* The uv sample is in multiple triangles. */
result = Result{ResultType::Multiple};
return;
}
}
if (dist < best_dist) {
best_dist = dist;
best_bary_weights = bary_weights;
best_tri_index = tri_i;
}
}
/* Allow using the closest (but not intersecting) triangle if the uv is almost exactly on an
* edge. */
if (best_dist < edge_epsilon) {
result = Result{ResultType::Ok, best_tri_index, math::clamp(best_bary_weights, 0.0f, 1.0f)};
return;
}
result = Result{ResultType::None, best_tri_index, best_bary_weights};
}
/* Allow using the closest (but not intersecting) triangle if the uv is almost exactly on an
* edge. */
if (best_dist < edge_epsilon) {
return Result{ResultType::Ok, best_tri_index, math::clamp(best_bary_weights, 0.0f, 1.0f)};
void ReverseUVSampler::sample_many(
const Span<float2> query_uvs, MutableSpan<Result> r_results, bool hints) const
{
BLI_assert(query_uvs.size() == r_results.size());
threading::parallel_for(query_uvs.index_range(), 256, [&](const IndexRange range) {
for (const int i : range) {
this->sample(query_uvs[i], r_results[i], hints);
}
});
}
return Result{};
}
ReverseUVSampler::~ReverseUVSampler() = default;
void ReverseUVSampler::sample_many(const Span<float2> query_uvs,
MutableSpan<Result> r_results) const
{
BLI_assert(query_uvs.size() == r_results.size());
threading::parallel_for(query_uvs.index_range(), 256, [&](const IndexRange range) {
for (const int i : range) {
r_results[i] = this->sample(query_uvs[i]);
}
});
}
} // namespace blender::geometry

View File

@ -1742,6 +1742,12 @@ typedef struct NodeGeometryCurveTrim {
uint8_t mode;
} NodeGeometryCurveTrim;
// struct NodeGeometryDeformCurvesOnSurfaceCache;
typedef struct NodeGeometryDeformCurvesOnSurface {
uint64_t cache;
} NodeGeometryDeformCurvesOnSurface;
typedef struct NodeGeometryCurveToPoints {
/** #GeometryNodeCurveResampleMode. */
uint8_t mode;

View File

@ -60,7 +60,7 @@ static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
static void node_init(bNodeTree * /*tree*/, bNode *node)
{
NodeGeometryCurveTrim *data = MEM_cnew<NodeGeometryCurveTrim>(__func__);
NodeGeometryCurveTrim *data = MEM_new<NodeGeometryCurveTrim>(__func__);
data->mode = GEO_NODE_CURVE_SAMPLE_FACTOR;
node->storage = data;

View File

@ -21,13 +21,34 @@
#include <fmt/format.h>
#include <map>
using blender::geometry::ReverseUVSampler;
struct NodeGeometryDeformCurvesOnSurfaceCacheEntry {
blender::Array<ReverseUVSampler::Result> old_samples;
blender::Array<ReverseUVSampler::Result> new_samples;
blender::uint2 tri_counts = {0, 0};
blender::uint2 uv_map_counts = {0, 0};
std::mutex mutex;
};
struct NodeGeometryDeformCurvesOnSurfaceCache
: public blender::Map<blender::uint2, NodeGeometryDeformCurvesOnSurfaceCacheEntry *> {
~NodeGeometryDeformCurvesOnSurfaceCache()
{
foreach_item([&]([[maybe_unused]] auto k, auto v) {
MEM_delete<NodeGeometryDeformCurvesOnSurfaceCacheEntry>(v);
});
}
};
namespace blender::nodes::node_geo_deform_curves_on_surface_cc {
using bke::CurvesGeometry;
using bke::attribute_math::mix3;
using geometry::ReverseUVSampler;
NODE_STORAGE_FUNCS(NodeGeometryCurveTrim)
NODE_STORAGE_FUNCS(NodeGeometryDeformCurvesOnSurface)
static void node_declare(NodeDeclarationBuilder &b)
{
@ -35,28 +56,96 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Curves").propagate_all();
}
static void node_init(bNodeTree *, bNode *node)
{
NodeGeometryDeformCurvesOnSurface *data = MEM_new<NodeGeometryDeformCurvesOnSurface>(__func__);
data->cache = 0;
node->storage = data;
}
/***
* Surface_mesh_orig usually stays unchanged from call to call, but it may change.
* We use tri_counts and uv_map_counts to detect changes, and use cached sampling results
* as hints as long as counts stay the same. (We still need to run sampling, because we
* don't keep full info about the previous mesh, so we can't detect all changes.)
*
* With regard to surface_mesh_eval, there are a few possible scenarios.
* 1. surface_mesh_eval is topologically the same as surface_mesh_orig, except possibly
* deformed by armature / shape keys (identical corner lists and UV maps); there's no
* need to rerun ReverseUVSampler, because all curves stay attached to the same places.
*
* 2. The mesh is substantially different; commonly happens if there's a subdivide modifier
* on the mesh (because 'surface_mesh_orig' will be the version without subdivision).
*
* -> 2.1. It is different from surface_mesh_orig, but it is (topologically) the same as
* in the previous call. Use cached result as hints.
*
* -> 2.2. It is different from both surface_mesh_orig and from the previous call. Run
* ReverseUVSampler without hints.
*
* */
static void deform_curves(const CurvesGeometry &curves,
const Mesh &surface_mesh_old,
const Mesh &surface_mesh_new,
const Span<float2> curve_attachment_uvs,
const ReverseUVSampler &reverse_uv_sampler_old,
const ReverseUVSampler &reverse_uv_sampler_new,
const ReverseUVSampler *reverse_uv_sampler_old,
const ReverseUVSampler *reverse_uv_sampler_new,
const Span<float3> corner_normals_old,
const Span<float3> corner_normals_new,
const Span<float3> rest_positions,
const float4x4 &surface_to_curves,
const Span<float3> r_orig_positions,
MutableSpan<float3> r_positions,
MutableSpan<float3x3> r_rotations,
std::atomic<int> &r_invalid_uv_count)
std::atomic<int> &r_invalid_uv_count,
uint2 uv_map_counts,
NodeGeometryDeformCurvesOnSurfaceCacheEntry *cache = nullptr)
{
/* Find attachment points on old and new mesh. */
const int curves_num = curves.curves_num();
Array<ReverseUVSampler::Result> surface_samples_old(curves_num);
Array<ReverseUVSampler::Result> surface_samples_new(curves_num);
threading::parallel_invoke(
1024 < curves_num,
[&]() { reverse_uv_sampler_old.sample_many(curve_attachment_uvs, surface_samples_old); },
[&]() { reverse_uv_sampler_new.sample_many(curve_attachment_uvs, surface_samples_new); });
const uint old_mesh_size = surface_mesh_old.corner_tris().size();
const uint new_mesh_size = surface_mesh_new.corner_tris().size();
/* this function is not expected to be called from multiple threads for the same object,
but we'll lock up just in case */
if (cache)
cache->mutex.lock();
bool hints = false;
if (cache && cache->old_samples.size() == curves_num && cache->tri_counts[0] == old_mesh_size &&
cache->uv_map_counts[0] == uv_map_counts[0])
{
surface_samples_old = cache->old_samples;
hints = true;
}
reverse_uv_sampler_old->sample_many(curve_attachment_uvs, surface_samples_old, hints);
if (reverse_uv_sampler_new != nullptr) {
/*
* If we get here, it means that at least one entry in either corner_tris or uv_map is
* different between 'orig' and 'eval'.
*/
hints = false;
if (cache && cache->new_samples.size() == curves_num &&
cache->tri_counts[1] == new_mesh_size && cache->uv_map_counts[1] == uv_map_counts[1])
{
hints = true;
surface_samples_new = cache->new_samples;
}
else if (surface_mesh_old.corner_tris().size() == new_mesh_size &&
uv_map_counts[0] == uv_map_counts[1])
{
hints = true;
surface_samples_new = surface_samples_old;
}
reverse_uv_sampler_new->sample_many(curve_attachment_uvs, surface_samples_new, hints);
}
else {
surface_samples_new = surface_samples_old;
}
const float4x4 curves_to_surface = math::invert(surface_to_curves);
@ -71,17 +160,49 @@ static void deform_curves(const CurvesGeometry &curves,
const OffsetIndices points_by_curve = curves.points_by_curve();
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) {
int last_known_good_point = -1;
for (const int curve_i : range) {
bool invalid = false;
const ReverseUVSampler::Result &surface_sample_old = surface_samples_old[curve_i];
if (surface_sample_old.type != ReverseUVSampler::ResultType::Ok) {
r_invalid_uv_count++;
continue;
}
if (surface_sample_old.type != ReverseUVSampler::ResultType::Ok)
invalid = true;
const ReverseUVSampler::Result &surface_sample_new = surface_samples_new[curve_i];
if (surface_sample_new.type != ReverseUVSampler::ResultType::Ok) {
if (surface_sample_new.type == ReverseUVSampler::ResultType::Multiple) {
invalid = true;
} else if(surface_sample_new.type == ReverseUVSampler::ResultType::None) {
/* UVs may end up outside the triangle during mapping from unsubdivided to
* subdivided mesh. */
auto bary_weights = surface_sample_new.bary_weights;
const float x_dist = std::max(-bary_weights.x, bary_weights.x - 1.0f);
const float y_dist = std::max(-bary_weights.y, bary_weights.y - 1.0f);
const float z_dist = std::max(-bary_weights.z, bary_weights.z - 1.0f);
const float dist = std::max({x_dist, y_dist, z_dist});
if(dist > 0.01f)
invalid = true;
}
if (invalid) {
r_invalid_uv_count++;
const IndexRange points = points_by_curve[curve_i];
/* There are no particularly good options here. Deleting invalid curves is
* potentially an very expensive operation. Leaving them at original locations
* creates clumps of unattached hairs hanging in midair.
*
* We set all points of an invalid curve to the same value, reducing it to
* a point (making it invisible in 'strand' mode and very small in 'strip' mode.)
* To obscure it even further, we also move it to the location of the last known
* good root.
* */
float3 pos = last_known_good_point>=0
? r_positions[last_known_good_point]
: r_orig_positions[points[0]];
for (const int point_i : points)
r_positions[point_i] = pos;
continue;
}
if(points_by_curve[curve_i].size() > 0)
last_known_good_point = points_by_curve[curve_i][0];
const int3 &tri_old = surface_corner_tris_old[surface_sample_old.tri_index];
const int3 &tri_new = surface_corner_tris_new[surface_sample_new.tri_index];
@ -185,7 +306,7 @@ static void deform_curves(const CurvesGeometry &curves,
/* Actually transform all points. */
const IndexRange points = points_by_curve[curve_i];
for (const int point_i : points) {
const float3 old_point_pos = r_positions[point_i];
const float3 old_point_pos = r_orig_positions[point_i];
const float3 new_point_pos = math::transform_point(curve_transform, old_point_pos);
r_positions[point_i] = new_point_pos;
}
@ -197,6 +318,28 @@ static void deform_curves(const CurvesGeometry &curves,
}
}
});
if (cache) {
cache->old_samples = std::move(surface_samples_old);
cache->new_samples = std::move(surface_samples_new);
cache->tri_counts = uint2{old_mesh_size, new_mesh_size};
cache->uv_map_counts = uv_map_counts;
cache->mutex.unlock();
}
}
template<typename T> bool threaded_compare(const T &va, const T &vb)
{
std::atomic<bool> difference(false);
threading::parallel_for(IndexRange(va.size()), 1024, [&](const IndexRange range) {
for (const int i : range) {
if (va[i] != vb[i]) {
difference = true;
break;
}
}
});
return difference;
}
static void node_geo_exec(GeoNodeExecParams params)
@ -301,8 +444,32 @@ static void node_geo_exec(GeoNodeExecParams params)
const Span<int3> corner_tris_orig = surface_mesh_orig->corner_tris();
const Span<int3> corner_tris_eval = surface_mesh_eval->corner_tris();
const ReverseUVSampler reverse_uv_sampler_orig{uv_map_orig, corner_tris_orig};
const ReverseUVSampler reverse_uv_sampler_eval{uv_map_eval, corner_tris_eval};
uint2 uv_map_counts{(uint)uv_map_orig.size(), (uint)uv_map_eval.size()};
bool same_mesh = false;
float2 uv_min = {0, 0}, uv_max = {0, 0};
if (surface_uv_coords.size() > 0) {
uv_min = uv_max = surface_uv_coords[0];
for (auto x : surface_uv_coords) {
uv_min = math::min(uv_min, x);
uv_max = math::max(uv_max, x);
}
}
same_mesh = (corner_tris_orig.size() == corner_tris_eval.size()) &&
(uv_map_orig.size() == uv_map_eval.size());
if (same_mesh)
same_mesh = !threaded_compare(corner_tris_eval, corner_tris_orig);
if (same_mesh)
same_mesh = !threaded_compare(uv_map_eval, uv_map_orig);
std::unique_ptr<ReverseUVSampler> reverse_uv_sampler_orig(
new ReverseUVSampler(uv_map_orig, corner_tris_orig, uv_min, uv_max));
std::unique_ptr<ReverseUVSampler> reverse_uv_sampler_eval;
if (!same_mesh)
reverse_uv_sampler_eval.reset(
new ReverseUVSampler(uv_map_eval, corner_tris_eval, uv_min, uv_max));
/* Retrieve face corner normals from each mesh. It's necessary to use face corner normals
* because face normals or vertex normals may lose information (custom normals, auto smooth) in
@ -329,20 +496,45 @@ static void node_geo_exec(GeoNodeExecParams params)
edit_hint_rotations = *edit_hints->deform_mats;
}
bNode &node = const_cast<bNode &>(params.node());
NodeGeometryDeformCurvesOnSurface &storage = node_storage(node);
NodeGeometryDeformCurvesOnSurfaceCache *cache = nullptr;
NodeGeometryDeformCurvesOnSurfaceCacheEntry *cache_entry = nullptr;
cache = reinterpret_cast<NodeGeometryDeformCurvesOnSurfaceCache *>(storage.cache);
if (cache == 0)
cache = MEM_new<NodeGeometryDeformCurvesOnSurfaceCache>(__func__);
storage.cache = reinterpret_cast<uint64_t &>(cache);
/* In case of multiple curves objects on the same mesh, a unique ID of the curves
* object would work better, but I can't find one */
uint2 key{(uint)surface_mesh_orig->id.session_uid, (uint)curves.curves_num()};
if (!cache->contains(key))
cache->add(key, MEM_new<NodeGeometryDeformCurvesOnSurfaceCacheEntry>(__func__));
cache_entry = cache->lookup(key);
auto orig_positions = curves.positions();
/* This is a somewhat expensive call: it allocates memory for the output array.
We pass 'preserve'='false' to indicate that we don't need the contents of the
buffer we receive; otherwise, it would be filled twice. */
MutableSpan<float3> new_positions = curves.positions_for_write(false);
if (edit_hint_positions.is_empty()) {
deform_curves(curves,
*surface_mesh_orig,
*surface_mesh_eval,
surface_uv_coords,
reverse_uv_sampler_orig,
reverse_uv_sampler_eval,
reverse_uv_sampler_orig.get(),
reverse_uv_sampler_eval.get(),
corner_normals_orig,
corner_normals_eval,
rest_positions,
transforms.surface_to_curves,
curves.positions_for_write(),
orig_positions,
new_positions,
edit_hint_rotations,
invalid_uv_count);
invalid_uv_count,
uv_map_counts,
cache_entry);
}
else {
/* First deform the actual curves in the input geometry. */
@ -350,15 +542,19 @@ static void node_geo_exec(GeoNodeExecParams params)
*surface_mesh_orig,
*surface_mesh_eval,
surface_uv_coords,
reverse_uv_sampler_orig,
reverse_uv_sampler_eval,
reverse_uv_sampler_orig.get(),
reverse_uv_sampler_eval.get(),
corner_normals_orig,
corner_normals_eval,
rest_positions,
transforms.surface_to_curves,
curves.positions_for_write(),
orig_positions,
new_positions,
{},
invalid_uv_count);
invalid_uv_count,
uv_map_counts,
cache_entry);
/* Then also deform edit curve information for use in sculpt mode. */
const CurvesGeometry &curves_orig = edit_hints->curves_id_orig.geometry.wrap();
const VArraySpan<float2> surface_uv_coords_orig = *curves_orig.attributes().lookup_or_default(
@ -368,18 +564,19 @@ static void node_geo_exec(GeoNodeExecParams params)
*surface_mesh_orig,
*surface_mesh_eval,
surface_uv_coords_orig,
reverse_uv_sampler_orig,
reverse_uv_sampler_eval,
reverse_uv_sampler_orig.get(),
reverse_uv_sampler_eval.get(),
corner_normals_orig,
corner_normals_eval,
rest_positions,
transforms.surface_to_curves,
edit_hint_positions,
edit_hint_positions,
edit_hint_rotations,
invalid_uv_count);
invalid_uv_count,
uv_map_counts);
}
}
curves.tag_positions_changed();
if (invalid_uv_count) {
@ -391,6 +588,31 @@ static void node_geo_exec(GeoNodeExecParams params)
params.set_output("Curves", curves_geometry);
}
void node_free_CurvesOnSurface_storage(bNode *node);
void node_copy_CurvesOnSurface_storage(bNodeTree *, bNode *, const bNode *);
void node_free_CurvesOnSurface_storage(bNode *node)
{
if (node->storage) {
auto storage = (NodeGeometryDeformCurvesOnSurface *)node->storage;
if (storage->cache) {
NodeGeometryDeformCurvesOnSurfaceCache *cache =
reinterpret_cast<NodeGeometryDeformCurvesOnSurfaceCache *>(storage->cache);
MEM_delete<NodeGeometryDeformCurvesOnSurfaceCache>(cache);
}
MEM_delete<NodeGeometryDeformCurvesOnSurface>(storage);
}
}
void node_copy_CurvesOnSurface_storage(bNodeTree * /*dest_ntree*/,
bNode *dest_node,
[[maybe_unused]] const bNode *src_node)
{
auto storage = MEM_new<NodeGeometryDeformCurvesOnSurface>(__func__);
storage->cache = 0;
dest_node->storage = storage;
}
static void node_register()
{
static bNodeType ntype;
@ -398,6 +620,11 @@ static void node_register()
&ntype, GEO_NODE_DEFORM_CURVES_ON_SURFACE, "Deform Curves on Surface", NODE_CLASS_GEOMETRY);
ntype.geometry_node_execute = node_geo_exec;
ntype.declare = node_declare;
node_type_storage(&ntype,
"NodeGeometryDeformCurvesOnSurface",
node_free_CurvesOnSurface_storage,
node_copy_CurvesOnSurface_storage);
ntype.initfunc = node_init;
blender::bke::node_type_size(&ntype, 170, 120, 700);
nodeRegisterType(&ntype);
}