Curves: operator to snap curves to surface

This operator snaps the first point of every curve to the corresponding
surface object. The shape of individual curves or their orientation is
not changed.

There are two different attachment modes:
* `Nearest`: Move each curve so that the first point is on the closest
  point on the surface. This should be used when the topology of the
  surface mesh changed, but the shape generally stayed the same.
* `Deform`: Use the existing attachment information that is stored
  for curves to move curves to their new location when the surface
  mesh was deformed. This generally does not work when the
  topology changed.

The purpose of the operator is to help setup the "ground truth"
for how curves are attached to the surface. When the ground
truth surface changed, the original curves have to be updated
as well. Deforming curves based on an animated surface will be
done with geometry nodes independent of the operator.

In the UI, the operator is currently exposed in curves sculpt mode
in the `Curves > Snap Curves to Surface` menu.

Differential Revision: https://developer.blender.org/D14515
This commit is contained in:
Jacques Lucke 2022-04-07 12:49:13 +02:00
parent e5c7f37223
commit 50869b408b
Notes: blender-bot 2023-02-14 07:39:44 +01:00
Referenced by issue #96730, Operator to remap curves to updated surface mesh.
3 changed files with 225 additions and 0 deletions

View File

@ -935,6 +935,8 @@ class VIEW3D_MT_editor_menus(Menu):
if mode_string == 'SCULPT':
layout.menu("VIEW3D_MT_mask")
layout.menu("VIEW3D_MT_face_sets")
if mode_string == 'SCULPT_CURVES':
layout.menu("VIEW3D_MT_sculpt_curves")
else:
layout.menu("VIEW3D_MT_object")
@ -3124,6 +3126,15 @@ class VIEW3D_MT_sculpt(Menu):
layout.operator("object.transfer_mode", text="Transfer Sculpt Mode")
class VIEW3D_MT_sculpt_curves(Menu):
bl_label = "Curves"
def draw(self, _context):
layout = self.layout
layout.operator("curves.snap_curves_to_surface")
class VIEW3D_MT_mask(Menu):
bl_label = "Mask"
@ -7759,6 +7770,7 @@ classes = (
VIEW3D_MT_sculpt_automasking_pie,
VIEW3D_MT_wpaint_vgroup_lock_pie,
VIEW3D_MT_sculpt_face_sets_edit_pie,
VIEW3D_MT_sculpt_curves,
VIEW3D_PT_active_tool,
VIEW3D_PT_active_tool_duplicate,
VIEW3D_PT_view3d_properties,

View File

@ -368,6 +368,17 @@ inline int curve_segment_size(const int points_num, const bool cyclic)
return cyclic ? points_num : points_num - 1;
}
inline float2 encode_surface_bary_coord(const float3 &v)
{
BLI_assert(std::abs(v.x + v.y + v.z - 1.0f) < 0.00001f);
return {v.x, v.y};
}
inline float3 decode_surface_bary_coord(const float2 &v)
{
return {v.x, v.y, 1.0f - v.x - v.y};
}
namespace bezier {
/**

View File

@ -4,13 +4,17 @@
* \ingroup edcurves
*/
#include <atomic>
#include "BLI_utildefines.h"
#include "ED_curves.h"
#include "ED_object.h"
#include "ED_screen.h"
#include "WM_api.h"
#include "BKE_bvhutils.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "BKE_layer.h"
@ -18,6 +22,7 @@
#include "BKE_mesh_runtime.h"
#include "BKE_paint.h"
#include "BKE_particle.h"
#include "BKE_report.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
@ -28,6 +33,9 @@
#include "DEG_depsgraph.h"
#include "RNA_access.h"
#include "RNA_define.h"
/**
* The code below uses a suffix naming convention to indicate the coordinate space:
* `cu`: Local space of the curves object that is being edited.
@ -283,10 +291,204 @@ static void CURVES_OT_convert_to_particle_system(wmOperatorType *ot)
ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
}
namespace snap_curves_to_surface {
enum class AttachMode {
Nearest,
Deform,
};
static bool snap_curves_to_surface_poll(bContext *C)
{
Object *ob = CTX_data_active_object(C);
if (ob == nullptr || ob->type != OB_CURVES) {
return false;
}
if (!ED_operator_object_active_editable_ex(C, ob)) {
return false;
}
Curves &curves = *static_cast<Curves *>(ob->data);
if (curves.surface == nullptr) {
return false;
}
return true;
}
static int snap_curves_to_surface_exec(bContext *C, wmOperator *op)
{
const AttachMode attach_mode = static_cast<AttachMode>(RNA_enum_get(op->ptr, "attach_mode"));
std::atomic<bool> found_invalid_looptri_index = false;
CTX_DATA_BEGIN (C, Object *, curves_ob, selected_objects) {
if (curves_ob->type != OB_CURVES) {
continue;
}
Curves &curves_id = *static_cast<Curves *>(curves_ob->data);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
if (curves_id.surface == nullptr) {
continue;
}
Object &surface_ob = *curves_id.surface;
if (surface_ob.type != OB_MESH) {
continue;
}
Mesh &surface_mesh = *static_cast<Mesh *>(surface_ob.data);
MutableSpan<float3> positions_cu = curves.positions_for_write();
MutableSpan<int> surface_triangle_indices = curves.surface_triangle_indices_for_write();
MutableSpan<float2> surface_triangle_coords = curves.surface_triangle_coords_for_write();
const Span<MLoopTri> surface_looptris = {BKE_mesh_runtime_looptri_ensure(&surface_mesh),
BKE_mesh_runtime_looptri_len(&surface_mesh)};
const float4x4 curves_to_world_mat = curves_ob->obmat;
const float4x4 world_to_curves_mat = curves_to_world_mat.inverted();
const float4x4 surface_to_world_mat = surface_ob.obmat;
const float4x4 world_to_surface_mat = surface_to_world_mat.inverted();
const float4x4 curves_to_surface_mat = world_to_surface_mat * curves_to_world_mat;
const float4x4 surface_to_curves_mat = world_to_curves_mat * surface_to_world_mat;
switch (attach_mode) {
case AttachMode::Nearest: {
BVHTreeFromMesh surface_bvh;
BKE_bvhtree_from_mesh_get(&surface_bvh, &surface_mesh, BVHTREE_FROM_LOOPTRI, 2);
BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); });
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange curves_range) {
for (const int curve_i : curves_range) {
const IndexRange points = curves.points_for_curve(curve_i);
const int first_point_i = points.first();
const float3 old_first_point_pos_cu = positions_cu[first_point_i];
const float3 old_first_point_pos_su = curves_to_surface_mat * old_first_point_pos_cu;
BVHTreeNearest nearest;
nearest.index = -1;
nearest.dist_sq = FLT_MAX;
BLI_bvhtree_find_nearest(surface_bvh.tree,
old_first_point_pos_su,
&nearest,
surface_bvh.nearest_callback,
&surface_bvh);
const int looptri_index = nearest.index;
if (looptri_index == -1) {
continue;
}
const float3 new_first_point_pos_su = nearest.co;
const float3 new_first_point_pos_cu = surface_to_curves_mat * new_first_point_pos_su;
const float3 pos_diff_cu = new_first_point_pos_cu - old_first_point_pos_cu;
for (float3 &pos_cu : positions_cu.slice(points)) {
pos_cu += pos_diff_cu;
}
surface_triangle_indices[curve_i] = looptri_index;
const MLoopTri &looptri = surface_looptris[looptri_index];
const float3 &p0_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[0]].v].co;
const float3 &p1_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[1]].v].co;
const float3 &p2_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[2]].v].co;
float3 bary_coords;
interp_weights_tri_v3(bary_coords, p0_su, p1_su, p2_su, new_first_point_pos_su);
surface_triangle_coords[curve_i] = bke::curves::encode_surface_bary_coord(bary_coords);
}
});
break;
}
case AttachMode::Deform: {
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange curves_range) {
for (const int curve_i : curves_range) {
const IndexRange points = curves.points_for_curve(curve_i);
const int first_point_i = points.first();
const float3 old_first_point_pos_cu = positions_cu[first_point_i];
const int looptri_index = surface_triangle_indices[curve_i];
if (!surface_looptris.index_range().contains(looptri_index)) {
found_invalid_looptri_index = true;
continue;
}
const MLoopTri &looptri = surface_looptris[looptri_index];
const float3 bary_coords = bke::curves::decode_surface_bary_coord(
surface_triangle_coords[curve_i]);
const float3 &p0_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[0]].v].co;
const float3 &p1_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[1]].v].co;
const float3 &p2_su = surface_mesh.mvert[surface_mesh.mloop[looptri.tri[2]].v].co;
float3 new_first_point_pos_su;
interp_v3_v3v3v3(new_first_point_pos_su, p0_su, p1_su, p2_su, bary_coords);
const float3 new_first_point_pos_cu = surface_to_curves_mat * new_first_point_pos_su;
const float3 pos_diff_cu = new_first_point_pos_cu - old_first_point_pos_cu;
for (float3 &pos_cu : positions_cu.slice(points)) {
pos_cu += pos_diff_cu;
}
}
});
break;
}
}
DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY);
}
CTX_DATA_END;
if (found_invalid_looptri_index) {
BKE_report(op->reports, RPT_INFO, "Could not snap some curves to the surface");
}
WM_main_add_notifier(NC_OBJECT | ND_DRAW, nullptr);
return OPERATOR_FINISHED;
}
} // namespace snap_curves_to_surface
static void CURVES_OT_snap_curves_to_surface(wmOperatorType *ot)
{
using namespace snap_curves_to_surface;
ot->name = "Snap Curves to Surface";
ot->idname = "CURVES_OT_snap_curves_to_surface";
ot->description = "Move curves so that the first point is exactly on the surface mesh";
ot->poll = snap_curves_to_surface_poll;
ot->exec = snap_curves_to_surface_exec;
ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
static const EnumPropertyItem attach_mode_items[] = {
{static_cast<int>(AttachMode::Nearest),
"NEAREST",
0,
"Nearest",
"Find the closest point on the surface for the root point of every curve and move the root "
"there"},
{static_cast<int>(AttachMode::Deform),
"DEFORM",
0,
"Deform",
"Re-attach curves to a deformed surface using the existing attachment information. This "
"only works when the topology of the surface mesh has not changed"},
{0, NULL, 0, NULL, NULL},
};
RNA_def_enum(ot->srna,
"attach_mode",
attach_mode_items,
static_cast<int>(AttachMode::Nearest),
"Attach Mode",
"How to find the point on the surface to attach to");
}
} // namespace blender::ed::curves
void ED_operatortypes_curves()
{
using namespace blender::ed::curves;
WM_operatortype_append(CURVES_OT_convert_to_particle_system);
WM_operatortype_append(CURVES_OT_snap_curves_to_surface);
}