Integrate an existing implementation of the SLIM unwrapping algorithm into Blender. More info about SLIM here: https://igl.ethz.ch/projects/slim/ This commit is based on the integration code written by Aurel Gruber for Blender 2.7x (unfinished and never merged with the main branch). This commit is based on Aurel's code, rebased and further improved. Details: - Unwrap has been moved into a sub-menu, slim unwrapping is exposed as: "Minimum Stretch". - Live unwrap with SLIM refines the solutions using a timer. - When using SLIM there are options to: - Set the number of iterations. - Weight the influence using vertex weights. - SLIM can be disabled using the `WITH_UV_SLIM` build option. Co-authored-by: Aurel Gruber <aurel.gruber@infix.ch> Ref !114545
1044 lines
34 KiB
C++
1044 lines
34 KiB
C++
/* SPDX-FileCopyrightText: 2002-2009 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup edsculpt
|
|
* UV Sculpt tools.
|
|
*/
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "BLI_ghash.h"
|
|
#include "BLI_math_base_safe.h"
|
|
#include "BLI_math_geom.h"
|
|
#include "BLI_math_vector.h"
|
|
#include "BLI_utildefines.h"
|
|
|
|
#include "DNA_brush_types.h"
|
|
#include "DNA_object_types.h"
|
|
#include "DNA_scene_types.h"
|
|
|
|
#include "BKE_brush.hh"
|
|
#include "BKE_colortools.hh"
|
|
#include "BKE_context.hh"
|
|
#include "BKE_customdata.hh"
|
|
#include "BKE_editmesh.hh"
|
|
#include "BKE_image.h"
|
|
#include "BKE_mesh_mapping.hh"
|
|
#include "BKE_paint.hh"
|
|
|
|
#include "DEG_depsgraph.hh"
|
|
|
|
#include "ED_image.hh"
|
|
#include "ED_mesh.hh"
|
|
#include "ED_screen.hh"
|
|
#include "ED_uvedit.hh"
|
|
|
|
#include "WM_api.hh"
|
|
#include "WM_types.hh"
|
|
|
|
#include "RNA_access.hh"
|
|
#include "RNA_define.hh"
|
|
#include "RNA_enum_types.hh"
|
|
|
|
#include "paint_intern.hh"
|
|
#include "uvedit_intern.hh"
|
|
|
|
#include "UI_view2d.hh"
|
|
|
|
typedef enum eBrushUVSculptTool {
|
|
UV_SCULPT_BRUSH_TYPE_GRAB = 0,
|
|
UV_SCULPT_BRUSH_TYPE_RELAX = 1,
|
|
UV_SCULPT_BRUSH_TYPE_PINCH = 2,
|
|
} eBrushUVSculptTool;
|
|
|
|
enum {
|
|
UV_SCULPT_BRUSH_TYPE_RELAX_LAPLACIAN = 0,
|
|
UV_SCULPT_BRUSH_TYPE_RELAX_HC = 1,
|
|
UV_SCULPT_BRUSH_TYPE_RELAX_COTAN = 2,
|
|
};
|
|
|
|
/* When set, the UV element is on the boundary of the graph.
|
|
* i.e. Instead of a 2-dimensional laplace operator, use a 1-dimensional version.
|
|
* Visually, UV elements on the graph boundary appear as borders of the UV Island. */
|
|
#define MARK_BOUNDARY 1
|
|
|
|
struct UvAdjacencyElement {
|
|
/** pointer to original UV-element. */
|
|
UvElement *element;
|
|
/** UV pointer for convenience. Caution, this points to the original UVs! */
|
|
float *uv;
|
|
/** Are we on locked in place? */
|
|
bool is_locked;
|
|
/** Are we on the boundary? */
|
|
bool is_boundary;
|
|
};
|
|
|
|
struct UvEdge {
|
|
uint uv1;
|
|
uint uv2;
|
|
/** Are we in the interior? */
|
|
bool is_interior;
|
|
};
|
|
|
|
struct UVInitialStrokeElement {
|
|
/** index to unique UV. */
|
|
int uv;
|
|
|
|
/** Strength on initial position. */
|
|
float strength;
|
|
|
|
/** initial UV position. */
|
|
float initial_uv[2];
|
|
};
|
|
|
|
struct UVInitialStroke {
|
|
/** Initial Selection,for grab brushes for instance. */
|
|
UVInitialStrokeElement *initialSelection;
|
|
|
|
/** Total initially selected UVs. */
|
|
int totalInitialSelected;
|
|
|
|
/** Initial mouse coordinates. */
|
|
float init_coord[2];
|
|
};
|
|
|
|
/** Custom data for UV smoothing. */
|
|
struct UvSculptData {
|
|
/**
|
|
* Contains the first of each set of coincident UVs.
|
|
* These will be used to perform smoothing on and propagate the changes to their coincident UVs.
|
|
*/
|
|
UvAdjacencyElement *uv;
|
|
|
|
/** Total number of unique UVs. */
|
|
int totalUniqueUvs;
|
|
|
|
/** Edges used for adjacency info, used with laplacian smoothing */
|
|
UvEdge *uvedges;
|
|
|
|
/** Total number of #UvEdge. */
|
|
int totalUvEdges;
|
|
|
|
/** data for initial stroke, used by tools like grab */
|
|
UVInitialStroke *initial_stroke;
|
|
|
|
/** Timer to be used for airbrush-type. */
|
|
wmTimer *timer;
|
|
|
|
/** To determine quickly adjacent UVs. */
|
|
UvElementMap *elementMap;
|
|
|
|
/** UV-smooth for fast reference. */
|
|
UvSculpt *uvsculpt;
|
|
|
|
/** Tool to use. duplicating here to change if modifier keys are pressed. */
|
|
char tool;
|
|
|
|
/** Store invert flag here. */
|
|
char invert;
|
|
|
|
/** Is constrain to image bounds active? */
|
|
bool constrain_to_bounds;
|
|
|
|
/** Base for constrain_to_bounds. */
|
|
float uv_base_offset[2];
|
|
};
|
|
|
|
static void apply_sculpt_data_constraints(UvSculptData *sculptdata, float uv[2])
|
|
{
|
|
if (!sculptdata->constrain_to_bounds) {
|
|
return;
|
|
}
|
|
float u = sculptdata->uv_base_offset[0];
|
|
float v = sculptdata->uv_base_offset[1];
|
|
uv[0] = clamp_f(uv[0], u, u + 1.0f);
|
|
uv[1] = clamp_f(uv[1], v, v + 1.0f);
|
|
}
|
|
|
|
static float calc_strength(const UvSculptData *sculptdata, float p, const float len)
|
|
{
|
|
float strength = BKE_brush_curve_strength(eBrushCurvePreset(sculptdata->uvsculpt->curve_preset),
|
|
sculptdata->uvsculpt->strength_curve,
|
|
p,
|
|
len);
|
|
|
|
CLAMP(strength, 0.0f, 1.0f);
|
|
|
|
return strength;
|
|
}
|
|
|
|
/*********** Improved Laplacian Relaxation Operator ************************/
|
|
/* original code by Raul Fernandez Hernandez "farsthary" *
|
|
* adapted to uv smoothing by Antony Riakiatakis *
|
|
***************************************************************************/
|
|
|
|
struct Temp_UVData {
|
|
float sum_co[2], p[2], b[2], sum_b[2];
|
|
int ncounter;
|
|
};
|
|
|
|
static void HC_relaxation_iteration_uv(UvSculptData *sculptdata,
|
|
const int cd_loop_uv_offset,
|
|
const float mouse_coord[2],
|
|
const float alpha,
|
|
const float radius_sq,
|
|
const float aspectRatio)
|
|
{
|
|
float diff[2];
|
|
int i;
|
|
const float radius = sqrtf(radius_sq);
|
|
|
|
Temp_UVData *tmp_uvdata = (Temp_UVData *)MEM_callocN(
|
|
sculptdata->totalUniqueUvs * sizeof(Temp_UVData), "Temporal data");
|
|
|
|
/* counting neighbors */
|
|
for (i = 0; i < sculptdata->totalUvEdges; i++) {
|
|
UvEdge *tmpedge = sculptdata->uvedges + i;
|
|
tmp_uvdata[tmpedge->uv1].ncounter++;
|
|
tmp_uvdata[tmpedge->uv2].ncounter++;
|
|
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv2].sum_co, sculptdata->uv[tmpedge->uv1].uv);
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv1].sum_co, sculptdata->uv[tmpedge->uv2].uv);
|
|
}
|
|
|
|
for (i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
copy_v2_v2(diff, tmp_uvdata[i].sum_co);
|
|
mul_v2_fl(diff, 1.0f / tmp_uvdata[i].ncounter);
|
|
copy_v2_v2(tmp_uvdata[i].p, diff);
|
|
|
|
tmp_uvdata[i].b[0] = diff[0] - sculptdata->uv[i].uv[0];
|
|
tmp_uvdata[i].b[1] = diff[1] - sculptdata->uv[i].uv[1];
|
|
}
|
|
|
|
for (i = 0; i < sculptdata->totalUvEdges; i++) {
|
|
UvEdge *tmpedge = sculptdata->uvedges + i;
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv1].sum_b, tmp_uvdata[tmpedge->uv2].b);
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv2].sum_b, tmp_uvdata[tmpedge->uv1].b);
|
|
}
|
|
|
|
for (i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
if (sculptdata->uv[i].is_locked) {
|
|
continue;
|
|
}
|
|
|
|
sub_v2_v2v2(diff, sculptdata->uv[i].uv, mouse_coord);
|
|
diff[1] /= aspectRatio;
|
|
float dist = dot_v2v2(diff, diff);
|
|
if (dist <= radius_sq) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * calc_strength(sculptdata, sqrtf(dist), radius);
|
|
|
|
sculptdata->uv[i].uv[0] = (1.0f - strength) * sculptdata->uv[i].uv[0] +
|
|
strength *
|
|
(tmp_uvdata[i].p[0] -
|
|
0.5f * (tmp_uvdata[i].b[0] +
|
|
tmp_uvdata[i].sum_b[0] / tmp_uvdata[i].ncounter));
|
|
sculptdata->uv[i].uv[1] = (1.0f - strength) * sculptdata->uv[i].uv[1] +
|
|
strength *
|
|
(tmp_uvdata[i].p[1] -
|
|
0.5f * (tmp_uvdata[i].b[1] +
|
|
tmp_uvdata[i].sum_b[1] / tmp_uvdata[i].ncounter));
|
|
|
|
apply_sculpt_data_constraints(sculptdata, sculptdata->uv[i].uv);
|
|
|
|
for (element = sculptdata->uv[i].element; element; element = element->next) {
|
|
if (element->separate && element != sculptdata->uv[i].element) {
|
|
break;
|
|
}
|
|
float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(element->l, cd_loop_uv_offset);
|
|
copy_v2_v2(*luv, sculptdata->uv[i].uv);
|
|
}
|
|
}
|
|
}
|
|
|
|
MEM_SAFE_FREE(tmp_uvdata);
|
|
}
|
|
|
|
/* Legacy version which only does laplacian relaxation.
|
|
* Probably a little faster as it caches UvEdges.
|
|
* Mostly preserved for comparison with `HC_relaxation_iteration_uv`.
|
|
* Once the HC method has been merged into `relaxation_iteration_uv`,
|
|
* all the `HC_*` and `laplacian_*` specific functions can probably be removed.
|
|
*/
|
|
|
|
static void laplacian_relaxation_iteration_uv(UvSculptData *sculptdata,
|
|
const int cd_loop_uv_offset,
|
|
const float mouse_coord[2],
|
|
const float alpha,
|
|
const float radius_sq,
|
|
const float aspectRatio)
|
|
{
|
|
float diff[2];
|
|
int i;
|
|
const float radius = sqrtf(radius_sq);
|
|
|
|
Temp_UVData *tmp_uvdata = (Temp_UVData *)MEM_callocN(
|
|
sculptdata->totalUniqueUvs * sizeof(Temp_UVData), "Temporal data");
|
|
|
|
/* counting neighbors */
|
|
for (i = 0; i < sculptdata->totalUvEdges; i++) {
|
|
UvEdge *tmpedge = sculptdata->uvedges + i;
|
|
bool code1 = sculptdata->uv[sculptdata->uvedges[i].uv1].is_boundary;
|
|
bool code2 = sculptdata->uv[sculptdata->uvedges[i].uv2].is_boundary;
|
|
if (code1 || (code1 == code2)) {
|
|
tmp_uvdata[tmpedge->uv2].ncounter++;
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv2].sum_co, sculptdata->uv[tmpedge->uv1].uv);
|
|
}
|
|
if (code2 || (code1 == code2)) {
|
|
tmp_uvdata[tmpedge->uv1].ncounter++;
|
|
add_v2_v2(tmp_uvdata[tmpedge->uv1].sum_co, sculptdata->uv[tmpedge->uv2].uv);
|
|
}
|
|
}
|
|
|
|
/* Original Laplacian algorithm included removal of normal component of translation.
|
|
* here it is not needed since we translate along the UV plane always. */
|
|
for (i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
copy_v2_v2(tmp_uvdata[i].p, tmp_uvdata[i].sum_co);
|
|
mul_v2_fl(tmp_uvdata[i].p, 1.0f / tmp_uvdata[i].ncounter);
|
|
}
|
|
|
|
for (i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
if (sculptdata->uv[i].is_locked) {
|
|
continue;
|
|
}
|
|
|
|
sub_v2_v2v2(diff, sculptdata->uv[i].uv, mouse_coord);
|
|
diff[1] /= aspectRatio;
|
|
float dist = dot_v2v2(diff, diff);
|
|
if (dist <= radius_sq) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * calc_strength(sculptdata, sqrtf(dist), radius);
|
|
|
|
sculptdata->uv[i].uv[0] = (1.0f - strength) * sculptdata->uv[i].uv[0] +
|
|
strength * tmp_uvdata[i].p[0];
|
|
sculptdata->uv[i].uv[1] = (1.0f - strength) * sculptdata->uv[i].uv[1] +
|
|
strength * tmp_uvdata[i].p[1];
|
|
|
|
apply_sculpt_data_constraints(sculptdata, sculptdata->uv[i].uv);
|
|
|
|
for (element = sculptdata->uv[i].element; element; element = element->next) {
|
|
if (element->separate && element != sculptdata->uv[i].element) {
|
|
break;
|
|
}
|
|
|
|
float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(element->l, cd_loop_uv_offset);
|
|
copy_v2_v2(*luv, sculptdata->uv[i].uv);
|
|
}
|
|
}
|
|
}
|
|
|
|
MEM_SAFE_FREE(tmp_uvdata);
|
|
}
|
|
|
|
static void add_weighted_edge(float (*delta_buf)[3],
|
|
const UvElement *storage,
|
|
const UvElement *ele_next,
|
|
const UvElement *ele_prev,
|
|
const float luv_next[2],
|
|
const float luv_prev[2],
|
|
const float weight)
|
|
{
|
|
float delta[2];
|
|
sub_v2_v2v2(delta, luv_next, luv_prev);
|
|
|
|
bool code1 = (ele_prev->flag & MARK_BOUNDARY);
|
|
bool code2 = (ele_next->flag & MARK_BOUNDARY);
|
|
if (code1 || (code1 == code2)) {
|
|
int index_next = ele_next - storage;
|
|
delta_buf[index_next][0] -= delta[0] * weight;
|
|
delta_buf[index_next][1] -= delta[1] * weight;
|
|
delta_buf[index_next][2] += fabsf(weight);
|
|
}
|
|
if (code2 || (code1 == code2)) {
|
|
int index_prev = ele_prev - storage;
|
|
delta_buf[index_prev][0] += delta[0] * weight;
|
|
delta_buf[index_prev][1] += delta[1] * weight;
|
|
delta_buf[index_prev][2] += fabsf(weight);
|
|
}
|
|
}
|
|
|
|
static float tri_weight_v3(int method, const float *v1, const float *v2, const float *v3)
|
|
{
|
|
switch (method) {
|
|
case UV_SCULPT_BRUSH_TYPE_RELAX_LAPLACIAN:
|
|
case UV_SCULPT_BRUSH_TYPE_RELAX_HC:
|
|
return 1.0f;
|
|
case UV_SCULPT_BRUSH_TYPE_RELAX_COTAN:
|
|
return cotangent_tri_weight_v3(v1, v2, v3);
|
|
default:
|
|
BLI_assert_unreachable();
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
static void relaxation_iteration_uv(UvSculptData *sculptdata,
|
|
const int cd_loop_uv_offset,
|
|
const float mouse_coord[2],
|
|
const float alpha,
|
|
const float radius_sq,
|
|
const float aspect_ratio,
|
|
const int method)
|
|
{
|
|
if (method == UV_SCULPT_BRUSH_TYPE_RELAX_HC) {
|
|
HC_relaxation_iteration_uv(
|
|
sculptdata, cd_loop_uv_offset, mouse_coord, alpha, radius_sq, aspect_ratio);
|
|
return;
|
|
}
|
|
if (method == UV_SCULPT_BRUSH_TYPE_RELAX_LAPLACIAN) {
|
|
laplacian_relaxation_iteration_uv(
|
|
sculptdata, cd_loop_uv_offset, mouse_coord, alpha, radius_sq, aspect_ratio);
|
|
return;
|
|
}
|
|
|
|
UvElement **head_table = BM_uv_element_map_ensure_head_table(sculptdata->elementMap);
|
|
|
|
const int total_uvs = sculptdata->elementMap->total_uvs;
|
|
float(*delta_buf)[3] = (float(*)[3])MEM_callocN(total_uvs * sizeof(float[3]), __func__);
|
|
|
|
const UvElement *storage = sculptdata->elementMap->storage;
|
|
for (int j = 0; j < total_uvs; j++) {
|
|
const UvElement *ele_curr = storage + j;
|
|
const UvElement *ele_next = BM_uv_element_get(sculptdata->elementMap, ele_curr->l->next);
|
|
const UvElement *ele_prev = BM_uv_element_get(sculptdata->elementMap, ele_curr->l->prev);
|
|
|
|
const float *v_curr_co = ele_curr->l->v->co;
|
|
const float *v_prev_co = ele_prev->l->v->co;
|
|
const float *v_next_co = ele_next->l->v->co;
|
|
|
|
const float(*luv_curr)[2] = BM_ELEM_CD_GET_FLOAT2_P(ele_curr->l, cd_loop_uv_offset);
|
|
const float(*luv_next)[2] = BM_ELEM_CD_GET_FLOAT2_P(ele_next->l, cd_loop_uv_offset);
|
|
const float(*luv_prev)[2] = BM_ELEM_CD_GET_FLOAT2_P(ele_prev->l, cd_loop_uv_offset);
|
|
|
|
const UvElement *head_curr = head_table[ele_curr - sculptdata->elementMap->storage];
|
|
const UvElement *head_next = head_table[ele_next - sculptdata->elementMap->storage];
|
|
const UvElement *head_prev = head_table[ele_prev - sculptdata->elementMap->storage];
|
|
|
|
/* If the mesh is triangulated with no boundaries, only one edge is required. */
|
|
const float weight_curr = tri_weight_v3(method, v_curr_co, v_prev_co, v_next_co);
|
|
add_weighted_edge(delta_buf, storage, head_next, head_prev, *luv_next, *luv_prev, weight_curr);
|
|
|
|
/* Triangulated with a boundary? We need the incoming edges to solve the boundary. */
|
|
const float weight_prev = tri_weight_v3(method, v_prev_co, v_curr_co, v_next_co);
|
|
add_weighted_edge(delta_buf, storage, head_next, head_curr, *luv_next, *luv_curr, weight_prev);
|
|
|
|
if (method == UV_SCULPT_BRUSH_TYPE_RELAX_LAPLACIAN) {
|
|
/* Laplacian method has zero weights on virtual edges. */
|
|
continue;
|
|
}
|
|
|
|
/* Meshes with quads (or other n-gons) need "virtual" edges too. */
|
|
const float weight_next = tri_weight_v3(method, v_next_co, v_curr_co, v_prev_co);
|
|
add_weighted_edge(delta_buf, storage, head_prev, head_curr, *luv_prev, *luv_curr, weight_next);
|
|
}
|
|
|
|
for (int i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
UvAdjacencyElement *adj_el = &sculptdata->uv[i];
|
|
if (adj_el->is_locked) {
|
|
continue; /* Locked UVs can't move. */
|
|
}
|
|
|
|
/* Is UV within influence? */
|
|
float diff[2];
|
|
sub_v2_v2v2(diff, adj_el->uv, mouse_coord);
|
|
diff[1] /= aspect_ratio;
|
|
const float dist_sq = len_squared_v2(diff);
|
|
if (dist_sq > radius_sq) {
|
|
continue;
|
|
}
|
|
const float strength = alpha * calc_strength(sculptdata, sqrtf(dist_sq), sqrtf(radius_sq));
|
|
|
|
const float *delta_sum = delta_buf[adj_el->element - storage];
|
|
|
|
{
|
|
const float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(adj_el->element->l, cd_loop_uv_offset);
|
|
BLI_assert(adj_el->uv == (float *)luv); /* Only true for head. */
|
|
adj_el->uv[0] = (*luv)[0] + strength * safe_divide(delta_sum[0], delta_sum[2]);
|
|
adj_el->uv[1] = (*luv)[1] + strength * safe_divide(delta_sum[1], delta_sum[2]);
|
|
apply_sculpt_data_constraints(sculptdata, adj_el->uv);
|
|
}
|
|
|
|
/* Copy UV co-ordinates to all UvElements. */
|
|
UvElement *tail = adj_el->element;
|
|
while (tail) {
|
|
float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(tail->l, cd_loop_uv_offset);
|
|
copy_v2_v2(*luv, adj_el->uv);
|
|
tail = tail->next;
|
|
if (tail && tail->separate) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
MEM_SAFE_FREE(delta_buf);
|
|
}
|
|
|
|
static void uv_sculpt_stroke_apply(bContext *C,
|
|
wmOperator *op,
|
|
const wmEvent *event,
|
|
Object *obedit)
|
|
{
|
|
ARegion *region = CTX_wm_region(C);
|
|
BMEditMesh *em = BKE_editmesh_from_object(obedit);
|
|
UvSculptData *sculptdata = (UvSculptData *)op->customdata;
|
|
eBrushUVSculptTool tool = eBrushUVSculptTool(sculptdata->tool);
|
|
int invert = sculptdata->invert ? -1 : 1;
|
|
float alpha = sculptdata->uvsculpt->strength;
|
|
|
|
float co[2];
|
|
UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &co[0], &co[1]);
|
|
|
|
SpaceImage *sima = CTX_wm_space_image(C);
|
|
|
|
int width, height;
|
|
ED_space_image_get_size(sima, &width, &height);
|
|
|
|
float zoomx, zoomy;
|
|
ED_space_image_get_zoom(sima, region, &zoomx, &zoomy);
|
|
|
|
const float radius = sculptdata->uvsculpt->size / (width * zoomx);
|
|
float aspectRatio = width / float(height);
|
|
|
|
/* We will compare squares to save some computation */
|
|
const float radius_sq = radius * radius;
|
|
|
|
const int cd_loop_uv_offset = CustomData_get_offset(&em->bm->ldata, CD_PROP_FLOAT2);
|
|
|
|
switch (tool) {
|
|
case UV_SCULPT_BRUSH_TYPE_PINCH: {
|
|
int i;
|
|
alpha *= invert;
|
|
for (i = 0; i < sculptdata->totalUniqueUvs; i++) {
|
|
if (sculptdata->uv[i].is_locked) {
|
|
continue;
|
|
}
|
|
|
|
float diff[2];
|
|
sub_v2_v2v2(diff, sculptdata->uv[i].uv, co);
|
|
diff[1] /= aspectRatio;
|
|
float dist = dot_v2v2(diff, diff);
|
|
if (dist <= radius_sq) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * calc_strength(sculptdata, sqrtf(dist), radius);
|
|
normalize_v2(diff);
|
|
|
|
sculptdata->uv[i].uv[0] -= strength * diff[0] * 0.001f;
|
|
sculptdata->uv[i].uv[1] -= strength * diff[1] * 0.001f;
|
|
|
|
apply_sculpt_data_constraints(sculptdata, sculptdata->uv[i].uv);
|
|
|
|
for (element = sculptdata->uv[i].element; element; element = element->next) {
|
|
if (element->separate && element != sculptdata->uv[i].element) {
|
|
break;
|
|
}
|
|
float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(element->l, cd_loop_uv_offset);
|
|
copy_v2_v2(*luv, sculptdata->uv[i].uv);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case UV_SCULPT_BRUSH_TYPE_RELAX: {
|
|
relaxation_iteration_uv(sculptdata,
|
|
cd_loop_uv_offset,
|
|
co,
|
|
alpha,
|
|
radius_sq,
|
|
aspectRatio,
|
|
RNA_enum_get(op->ptr, "relax_method"));
|
|
break;
|
|
}
|
|
case UV_SCULPT_BRUSH_TYPE_GRAB: {
|
|
int i;
|
|
float diff[2];
|
|
sub_v2_v2v2(diff, co, sculptdata->initial_stroke->init_coord);
|
|
|
|
for (i = 0; i < sculptdata->initial_stroke->totalInitialSelected; i++) {
|
|
UvElement *element;
|
|
int uvindex = sculptdata->initial_stroke->initialSelection[i].uv;
|
|
float strength = sculptdata->initial_stroke->initialSelection[i].strength;
|
|
sculptdata->uv[uvindex].uv[0] =
|
|
sculptdata->initial_stroke->initialSelection[i].initial_uv[0] + strength * diff[0];
|
|
sculptdata->uv[uvindex].uv[1] =
|
|
sculptdata->initial_stroke->initialSelection[i].initial_uv[1] + strength * diff[1];
|
|
|
|
apply_sculpt_data_constraints(sculptdata, sculptdata->uv[uvindex].uv);
|
|
|
|
for (element = sculptdata->uv[uvindex].element; element; element = element->next) {
|
|
if (element->separate && element != sculptdata->uv[uvindex].element) {
|
|
break;
|
|
}
|
|
float(*luv)[2] = BM_ELEM_CD_GET_FLOAT2_P(element->l, cd_loop_uv_offset);
|
|
copy_v2_v2(*luv, sculptdata->uv[uvindex].uv);
|
|
}
|
|
}
|
|
if (sima->flag & SI_LIVE_UNWRAP) {
|
|
ED_uvedit_live_unwrap_re_solve();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void uv_sculpt_stroke_exit(bContext *C, wmOperator *op)
|
|
{
|
|
SpaceImage *sima = CTX_wm_space_image(C);
|
|
if (sima->flag & SI_LIVE_UNWRAP) {
|
|
ED_uvedit_live_unwrap_end(false);
|
|
}
|
|
UvSculptData *data = static_cast<UvSculptData *>(op->customdata);
|
|
if (data->timer) {
|
|
WM_event_timer_remove(CTX_wm_manager(C), CTX_wm_window(C), data->timer);
|
|
}
|
|
BM_uv_element_map_free(data->elementMap);
|
|
data->elementMap = nullptr;
|
|
MEM_SAFE_FREE(data->uv);
|
|
MEM_SAFE_FREE(data->uvedges);
|
|
if (data->initial_stroke) {
|
|
MEM_SAFE_FREE(data->initial_stroke->initialSelection);
|
|
MEM_SAFE_FREE(data->initial_stroke);
|
|
}
|
|
|
|
MEM_SAFE_FREE(data);
|
|
op->customdata = nullptr;
|
|
}
|
|
|
|
static int uv_element_offset_from_face_get(UvElementMap *map,
|
|
BMLoop *l,
|
|
int island_index,
|
|
const bool do_islands)
|
|
{
|
|
UvElement *element = BM_uv_element_get(map, l);
|
|
if (!element || (do_islands && element->island != island_index)) {
|
|
return -1;
|
|
}
|
|
return element - map->storage;
|
|
}
|
|
|
|
static uint uv_edge_hash(const void *key)
|
|
{
|
|
const UvEdge *edge = static_cast<const UvEdge *>(key);
|
|
return (BLI_ghashutil_uinthash(edge->uv2) + BLI_ghashutil_uinthash(edge->uv1));
|
|
}
|
|
|
|
static bool uv_edge_compare(const void *a, const void *b)
|
|
{
|
|
const UvEdge *edge1 = static_cast<const UvEdge *>(a);
|
|
const UvEdge *edge2 = static_cast<const UvEdge *>(b);
|
|
|
|
if ((edge1->uv1 == edge2->uv1) && (edge1->uv2 == edge2->uv2)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void set_element_flag(UvElement *element, const int flag)
|
|
{
|
|
while (element) {
|
|
element->flag |= flag;
|
|
element = element->next;
|
|
if (!element || element->separate) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static UvSculptData *uv_sculpt_stroke_init(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
Scene *scene = CTX_data_scene(C);
|
|
Object *obedit = CTX_data_edit_object(C);
|
|
ToolSettings *ts = scene->toolsettings;
|
|
UvSculptData *data = MEM_cnew<UvSculptData>(__func__);
|
|
BMEditMesh *em = BKE_editmesh_from_object(obedit);
|
|
BMesh *bm = em->bm;
|
|
|
|
op->customdata = data;
|
|
|
|
BKE_curvemapping_init(ts->uvsculpt.strength_curve);
|
|
|
|
if (!data) {
|
|
return nullptr;
|
|
}
|
|
|
|
ARegion *region = CTX_wm_region(C);
|
|
float co[2];
|
|
BMFace *efa;
|
|
float(*luv)[2];
|
|
BMLoop *l;
|
|
BMIter iter, liter;
|
|
|
|
GHashIterator gh_iter;
|
|
|
|
bool do_island_optimization = !(ts->uv_sculpt_settings & UV_SCULPT_ALL_ISLANDS);
|
|
int island_index = 0;
|
|
if (STREQ(op->type->idname, "SCULPT_OT_uv_sculpt_relax")) {
|
|
data->tool = UV_SCULPT_BRUSH_TYPE_RELAX;
|
|
}
|
|
else if (STREQ(op->type->idname, "SCULPT_OT_uv_sculpt_grab")) {
|
|
data->tool = UV_SCULPT_BRUSH_TYPE_GRAB;
|
|
}
|
|
else {
|
|
data->tool = UV_SCULPT_BRUSH_TYPE_PINCH;
|
|
}
|
|
data->invert = RNA_boolean_get(op->ptr, "use_invert");
|
|
|
|
data->uvsculpt = &ts->uvsculpt;
|
|
|
|
/* Winding was added to island detection in 5197aa04c6bd
|
|
* However the sculpt tools can flip faces, potentially creating orphaned islands.
|
|
* See #100132 */
|
|
const bool use_winding = false;
|
|
const bool use_seams = true;
|
|
data->elementMap = BM_uv_element_map_create(
|
|
bm, scene, false, use_winding, use_seams, do_island_optimization);
|
|
|
|
if (!data->elementMap) {
|
|
uv_sculpt_stroke_exit(C, op);
|
|
return nullptr;
|
|
}
|
|
|
|
/* Mouse coordinates, useful for some functions like grab and sculpt all islands */
|
|
UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &co[0], &co[1]);
|
|
|
|
/* We need to find the active island here. */
|
|
if (do_island_optimization) {
|
|
UvNearestHit hit = uv_nearest_hit_init_max(®ion->v2d);
|
|
uv_find_nearest_vert(scene, obedit, co, 0.0f, &hit);
|
|
|
|
UvElement *element = BM_uv_element_get(data->elementMap, hit.l);
|
|
if (element) {
|
|
island_index = element->island;
|
|
}
|
|
}
|
|
|
|
/* Count 'unique' UVs */
|
|
int unique_uvs = data->elementMap->total_unique_uvs;
|
|
if (do_island_optimization) {
|
|
unique_uvs = data->elementMap->island_total_unique_uvs[island_index];
|
|
}
|
|
|
|
/* Allocate the unique uv buffers */
|
|
data->uv = MEM_cnew_array<UvAdjacencyElement>(unique_uvs, __func__);
|
|
/* Holds, for each UvElement in elementMap, an index of its unique UV. */
|
|
int *uniqueUv = static_cast<int *>(
|
|
MEM_mallocN(sizeof(*uniqueUv) * data->elementMap->total_uvs, __func__));
|
|
GHash *edgeHash = BLI_ghash_new(uv_edge_hash, uv_edge_compare, "uv_brush_edge_hash");
|
|
/* we have at most totalUVs edges */
|
|
UvEdge *edges = MEM_cnew_array<UvEdge>(data->elementMap->total_uvs, __func__);
|
|
if (!data->uv || !uniqueUv || !edgeHash || !edges) {
|
|
MEM_SAFE_FREE(edges);
|
|
MEM_SAFE_FREE(uniqueUv);
|
|
if (edgeHash) {
|
|
BLI_ghash_free(edgeHash, nullptr, nullptr);
|
|
}
|
|
uv_sculpt_stroke_exit(C, op);
|
|
return nullptr;
|
|
}
|
|
|
|
data->totalUniqueUvs = unique_uvs;
|
|
/* Index for the UvElements. */
|
|
int counter = -1;
|
|
|
|
const BMUVOffsets offsets = BM_uv_map_get_offsets(em->bm);
|
|
/* initialize the unique UVs */
|
|
for (int i = 0; i < bm->totvert; i++) {
|
|
UvElement *element = data->elementMap->vertex[i];
|
|
for (; element; element = element->next) {
|
|
if (element->separate) {
|
|
if (do_island_optimization && (element->island != island_index)) {
|
|
/* skip this uv if not on the active island */
|
|
for (; element->next && !(element->next->separate); element = element->next) {
|
|
/* pass */
|
|
}
|
|
continue;
|
|
}
|
|
|
|
luv = BM_ELEM_CD_GET_FLOAT2_P(element->l, offsets.uv);
|
|
|
|
counter++;
|
|
data->uv[counter].element = element;
|
|
data->uv[counter].uv = *luv;
|
|
if (data->tool != UV_SCULPT_BRUSH_TYPE_GRAB) {
|
|
if (BM_ELEM_CD_GET_BOOL(element->l, offsets.pin)) {
|
|
data->uv[counter].is_locked = true;
|
|
}
|
|
}
|
|
}
|
|
/* Pointer arithmetic to the rescue, as always :). */
|
|
uniqueUv[element - data->elementMap->storage] = counter;
|
|
}
|
|
}
|
|
BLI_assert(counter + 1 == unique_uvs);
|
|
|
|
/* Now, on to generate our uv connectivity data */
|
|
counter = 0;
|
|
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
|
|
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
|
|
int itmp1 = uv_element_offset_from_face_get(
|
|
data->elementMap, l, island_index, do_island_optimization);
|
|
int itmp2 = uv_element_offset_from_face_get(
|
|
data->elementMap, l->next, island_index, do_island_optimization);
|
|
|
|
/* Skip edge if not found(unlikely) or not on valid island */
|
|
if (itmp1 == -1 || itmp2 == -1) {
|
|
continue;
|
|
}
|
|
|
|
int offset1 = uniqueUv[itmp1];
|
|
int offset2 = uniqueUv[itmp2];
|
|
|
|
/* Using an order policy, sort UVs according to address space.
|
|
* This avoids having two different UvEdges with the same UVs on different positions. */
|
|
if (offset1 < offset2) {
|
|
edges[counter].uv1 = offset1;
|
|
edges[counter].uv2 = offset2;
|
|
}
|
|
else {
|
|
edges[counter].uv1 = offset2;
|
|
edges[counter].uv2 = offset1;
|
|
}
|
|
UvEdge *prev_edge = static_cast<UvEdge *>(BLI_ghash_lookup(edgeHash, &edges[counter]));
|
|
if (prev_edge) {
|
|
prev_edge->is_interior = true;
|
|
edges[counter].is_interior = true;
|
|
}
|
|
else {
|
|
BLI_ghash_insert(edgeHash, &edges[counter], &edges[counter]);
|
|
}
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
MEM_SAFE_FREE(uniqueUv);
|
|
|
|
/* Allocate connectivity data, we allocate edges once */
|
|
data->uvedges = MEM_cnew_array<UvEdge>(BLI_ghash_len(edgeHash), __func__);
|
|
if (!data->uvedges) {
|
|
BLI_ghash_free(edgeHash, nullptr, nullptr);
|
|
MEM_SAFE_FREE(edges);
|
|
uv_sculpt_stroke_exit(C, op);
|
|
return nullptr;
|
|
}
|
|
|
|
/* fill the edges with data */
|
|
{
|
|
int i = 0;
|
|
GHASH_ITER (gh_iter, edgeHash) {
|
|
data->uvedges[i++] = *((UvEdge *)BLI_ghashIterator_getKey(&gh_iter));
|
|
}
|
|
data->totalUvEdges = BLI_ghash_len(edgeHash);
|
|
}
|
|
|
|
/* cleanup temporary stuff */
|
|
BLI_ghash_free(edgeHash, nullptr, nullptr);
|
|
MEM_SAFE_FREE(edges);
|
|
|
|
/* transfer boundary edge property to UVs */
|
|
for (int i = 0; i < data->totalUvEdges; i++) {
|
|
if (!data->uvedges[i].is_interior) {
|
|
data->uv[data->uvedges[i].uv1].is_boundary = true;
|
|
data->uv[data->uvedges[i].uv2].is_boundary = true;
|
|
if (ts->uv_sculpt_settings & UV_SCULPT_LOCK_BORDERS) {
|
|
data->uv[data->uvedges[i].uv1].is_locked = true;
|
|
data->uv[data->uvedges[i].uv2].is_locked = true;
|
|
}
|
|
set_element_flag(data->uv[data->uvedges[i].uv1].element, MARK_BOUNDARY);
|
|
set_element_flag(data->uv[data->uvedges[i].uv2].element, MARK_BOUNDARY);
|
|
}
|
|
}
|
|
|
|
SpaceImage *sima = CTX_wm_space_image(C);
|
|
data->constrain_to_bounds = (sima->flag & SI_CLIP_UV);
|
|
BKE_image_find_nearest_tile_with_offset(sima->image, co, data->uv_base_offset);
|
|
|
|
/* Allocate initial selection for grab tool */
|
|
if (data->tool == UV_SCULPT_BRUSH_TYPE_GRAB) {
|
|
float alpha = data->uvsculpt->strength;
|
|
float radius = data->uvsculpt->size;
|
|
int width, height;
|
|
ED_space_image_get_size(sima, &width, &height);
|
|
float zoomx, zoomy;
|
|
ED_space_image_get_zoom(sima, region, &zoomx, &zoomy);
|
|
|
|
float aspectRatio = width / float(height);
|
|
radius /= (width * zoomx);
|
|
const float radius_sq = radius * radius;
|
|
|
|
/* Allocate selection stack */
|
|
data->initial_stroke = static_cast<UVInitialStroke *>(
|
|
MEM_mallocN(sizeof(*data->initial_stroke), __func__));
|
|
if (!data->initial_stroke) {
|
|
uv_sculpt_stroke_exit(C, op);
|
|
}
|
|
data->initial_stroke->initialSelection = static_cast<UVInitialStrokeElement *>(MEM_mallocN(
|
|
sizeof(*data->initial_stroke->initialSelection) * data->totalUniqueUvs, __func__));
|
|
if (!data->initial_stroke->initialSelection) {
|
|
uv_sculpt_stroke_exit(C, op);
|
|
}
|
|
|
|
copy_v2_v2(data->initial_stroke->init_coord, co);
|
|
|
|
counter = 0;
|
|
for (int i = 0; i < data->totalUniqueUvs; i++) {
|
|
if (data->uv[i].is_locked) {
|
|
continue;
|
|
}
|
|
|
|
float diff[2];
|
|
sub_v2_v2v2(diff, data->uv[i].uv, co);
|
|
diff[1] /= aspectRatio;
|
|
float dist = dot_v2v2(diff, diff);
|
|
if (dist <= radius_sq) {
|
|
float strength;
|
|
strength = alpha * calc_strength(data, sqrtf(dist), radius);
|
|
|
|
data->initial_stroke->initialSelection[counter].uv = i;
|
|
data->initial_stroke->initialSelection[counter].strength = strength;
|
|
copy_v2_v2(data->initial_stroke->initialSelection[counter].initial_uv, data->uv[i].uv);
|
|
counter++;
|
|
}
|
|
}
|
|
|
|
data->initial_stroke->totalInitialSelected = counter;
|
|
if (sima->flag & SI_LIVE_UNWRAP) {
|
|
wmWindow *win_modal = CTX_wm_window(C);
|
|
ED_uvedit_live_unwrap_begin(scene, obedit, win_modal);
|
|
}
|
|
}
|
|
|
|
return static_cast<UvSculptData *>(op->customdata);
|
|
}
|
|
|
|
static int uv_sculpt_stroke_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
UvSculptData *data;
|
|
Object *obedit = CTX_data_edit_object(C);
|
|
|
|
if (!(data = uv_sculpt_stroke_init(C, op, event))) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
uv_sculpt_stroke_apply(C, op, event, obedit);
|
|
|
|
data->timer = WM_event_timer_add(CTX_wm_manager(C), CTX_wm_window(C), TIMER, 0.001f);
|
|
|
|
if (!data->timer) {
|
|
uv_sculpt_stroke_exit(C, op);
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
WM_event_add_modal_handler(C, op);
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
static int uv_sculpt_stroke_modal(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
UvSculptData *data = (UvSculptData *)op->customdata;
|
|
Object *obedit = CTX_data_edit_object(C);
|
|
|
|
switch (event->type) {
|
|
case LEFTMOUSE:
|
|
case MIDDLEMOUSE:
|
|
case RIGHTMOUSE:
|
|
uv_sculpt_stroke_exit(C, op);
|
|
return OPERATOR_FINISHED;
|
|
|
|
case MOUSEMOVE:
|
|
case INBETWEEN_MOUSEMOVE:
|
|
uv_sculpt_stroke_apply(C, op, event, obedit);
|
|
break;
|
|
case TIMER:
|
|
if (event->customdata == data->timer) {
|
|
uv_sculpt_stroke_apply(C, op, event, obedit);
|
|
}
|
|
break;
|
|
default:
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
ED_region_tag_redraw(CTX_wm_region(C));
|
|
WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data);
|
|
DEG_id_tag_update(static_cast<ID *>(obedit->data), 0);
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
static void register_common_props(wmOperatorType *ot)
|
|
{
|
|
PropertyRNA *prop;
|
|
|
|
prop = RNA_def_boolean(
|
|
ot->srna, "use_invert", false, "Invert", "Invert action for the duration of the stroke");
|
|
RNA_def_property_flag(prop, PropertyFlag(PROP_SKIP_SAVE));
|
|
}
|
|
|
|
void SCULPT_OT_uv_sculpt_grab(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Grab UVs";
|
|
ot->description = "Grab UVs";
|
|
ot->idname = "SCULPT_OT_uv_sculpt_grab";
|
|
|
|
ot->invoke = uv_sculpt_stroke_invoke;
|
|
ot->modal = uv_sculpt_stroke_modal;
|
|
ot->poll = ED_operator_uvedit_space_image;
|
|
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
register_common_props(ot);
|
|
}
|
|
|
|
void SCULPT_OT_uv_sculpt_relax(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Relax UVs";
|
|
ot->description = "Relax UVs";
|
|
ot->idname = "SCULPT_OT_uv_sculpt_relax";
|
|
|
|
ot->invoke = uv_sculpt_stroke_invoke;
|
|
ot->modal = uv_sculpt_stroke_modal;
|
|
ot->poll = ED_operator_uvedit_space_image;
|
|
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
register_common_props(ot);
|
|
|
|
static const EnumPropertyItem relax_method_items[] = {
|
|
{UV_SCULPT_BRUSH_TYPE_RELAX_LAPLACIAN,
|
|
"LAPLACIAN",
|
|
0,
|
|
"Laplacian",
|
|
"Use Laplacian method for relaxation"},
|
|
{UV_SCULPT_BRUSH_TYPE_RELAX_HC, "HC", 0, "HC", "Use HC method for relaxation"},
|
|
{UV_SCULPT_BRUSH_TYPE_RELAX_COTAN,
|
|
"COTAN",
|
|
0,
|
|
"Geometry",
|
|
"Use Geometry (cotangent) relaxation, making UVs follow the underlying 3D geometry"},
|
|
{0, nullptr, 0, nullptr, nullptr},
|
|
};
|
|
|
|
RNA_def_enum(ot->srna,
|
|
"relax_method",
|
|
relax_method_items,
|
|
CURVE_PRESET_SMOOTH,
|
|
"Relax Method",
|
|
"Algorithm used for UV relaxation");
|
|
}
|
|
|
|
void SCULPT_OT_uv_sculpt_pinch(wmOperatorType *ot)
|
|
{
|
|
ot->name = "Pinch UVs";
|
|
ot->description = "Pinch UVs";
|
|
ot->idname = "SCULPT_OT_uv_sculpt_pinch";
|
|
|
|
ot->invoke = uv_sculpt_stroke_invoke;
|
|
ot->modal = uv_sculpt_stroke_modal;
|
|
ot->poll = ED_operator_uvedit_space_image;
|
|
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
register_common_props(ot);
|
|
}
|