1016 lines
33 KiB
C++
1016 lines
33 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 "paint_intern.hh"
|
|
#include "uvedit_intern.hh"
|
|
|
|
#include "UI_view2d.hh"
|
|
|
|
/* 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 of brush 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 brush. */
|
|
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 brush. */
|
|
wmTimer *timer;
|
|
|
|
/** To determine quickly adjacent UVs. */
|
|
UvElementMap *elementMap;
|
|
|
|
/** UV-smooth Paint for fast reference. */
|
|
Paint *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);
|
|
}
|
|
|
|
/*********** 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],
|
|
float alpha,
|
|
float radius,
|
|
float aspectRatio)
|
|
{
|
|
Temp_UVData *tmp_uvdata;
|
|
float diff[2];
|
|
int i;
|
|
float radius_root = sqrtf(radius);
|
|
Brush *brush = BKE_paint_brush(sculptdata->uvsculpt);
|
|
|
|
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) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * BKE_brush_curve_strength_clamped(brush, sqrtf(dist), radius_root);
|
|
|
|
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],
|
|
float alpha,
|
|
float radius,
|
|
float aspectRatio)
|
|
{
|
|
Temp_UVData *tmp_uvdata;
|
|
float diff[2];
|
|
int i;
|
|
float radius_root = sqrtf(radius);
|
|
Brush *brush = BKE_paint_brush(sculptdata->uvsculpt);
|
|
|
|
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) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * BKE_brush_curve_strength_clamped(brush, sqrtf(dist), radius_root);
|
|
|
|
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_TOOL_RELAX_LAPLACIAN:
|
|
case UV_SCULPT_TOOL_RELAX_HC:
|
|
return 1.0f;
|
|
case UV_SCULPT_TOOL_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_squared,
|
|
const float aspect_ratio,
|
|
const int method)
|
|
{
|
|
if (method == UV_SCULPT_TOOL_RELAX_HC) {
|
|
HC_relaxation_iteration_uv(
|
|
sculptdata, cd_loop_uv_offset, mouse_coord, alpha, radius_squared, aspect_ratio);
|
|
return;
|
|
}
|
|
if (method == UV_SCULPT_TOOL_RELAX_LAPLACIAN) {
|
|
laplacian_relaxation_iteration_uv(
|
|
sculptdata, cd_loop_uv_offset, mouse_coord, alpha, radius_squared, 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_TOOL_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);
|
|
}
|
|
|
|
Brush *brush = BKE_paint_brush(sculptdata->uvsculpt);
|
|
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 brush's influence? */
|
|
float diff[2];
|
|
sub_v2_v2v2(diff, adj_el->uv, mouse_coord);
|
|
diff[1] /= aspect_ratio;
|
|
const float dist_squared = len_squared_v2(diff);
|
|
if (dist_squared > radius_squared) {
|
|
continue;
|
|
}
|
|
const float strength = alpha * BKE_brush_curve_strength_clamped(
|
|
brush, sqrtf(dist_squared), sqrtf(radius_squared));
|
|
|
|
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)
|
|
{
|
|
float co[2], radius, radius_root;
|
|
Scene *scene = CTX_data_scene(C);
|
|
ARegion *region = CTX_wm_region(C);
|
|
BMEditMesh *em = BKE_editmesh_from_object(obedit);
|
|
uint tool;
|
|
UvSculptData *sculptdata = (UvSculptData *)op->customdata;
|
|
SpaceImage *sima;
|
|
int invert;
|
|
int width, height;
|
|
float aspectRatio;
|
|
float alpha, zoomx, zoomy;
|
|
Brush *brush = BKE_paint_brush(sculptdata->uvsculpt);
|
|
ToolSettings *toolsettings = CTX_data_tool_settings(C);
|
|
tool = sculptdata->tool;
|
|
invert = sculptdata->invert ? -1 : 1;
|
|
alpha = BKE_brush_alpha_get(scene, brush);
|
|
UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &co[0], &co[1]);
|
|
|
|
sima = CTX_wm_space_image(C);
|
|
ED_space_image_get_size(sima, &width, &height);
|
|
ED_space_image_get_zoom(sima, region, &zoomx, &zoomy);
|
|
|
|
radius = BKE_brush_size_get(scene, brush) / (width * zoomx);
|
|
aspectRatio = width / float(height);
|
|
|
|
/* We will compare squares to save some computation */
|
|
radius = radius * radius;
|
|
radius_root = sqrtf(radius);
|
|
|
|
const int cd_loop_uv_offset = CustomData_get_offset(&em->bm->ldata, CD_PROP_FLOAT2);
|
|
|
|
/*
|
|
* Pinch Tool
|
|
*/
|
|
if (tool == UV_SCULPT_TOOL_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) {
|
|
UvElement *element;
|
|
float strength;
|
|
strength = alpha * BKE_brush_curve_strength_clamped(brush, sqrtf(dist), radius_root);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Relax Tool
|
|
*/
|
|
else if (tool == UV_SCULPT_TOOL_RELAX) {
|
|
relaxation_iteration_uv(sculptdata,
|
|
cd_loop_uv_offset,
|
|
co,
|
|
alpha,
|
|
radius,
|
|
aspectRatio,
|
|
toolsettings->uv_relax_method);
|
|
}
|
|
|
|
/*
|
|
* Grab Tool
|
|
*/
|
|
else if (tool == UV_SCULPT_TOOL_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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
Brush *brush = BKE_paint_brush(&ts->uvsculpt->paint);
|
|
|
|
op->customdata = data;
|
|
|
|
BKE_curvemapping_init(brush->curve);
|
|
|
|
if (data) {
|
|
ARegion *region = CTX_wm_region(C);
|
|
float co[2];
|
|
BMFace *efa;
|
|
float(*luv)[2];
|
|
BMLoop *l;
|
|
BMIter iter, liter;
|
|
|
|
UvEdge *edges;
|
|
GHash *edgeHash;
|
|
GHashIterator gh_iter;
|
|
|
|
bool do_island_optimization = !(ts->uv_sculpt_settings & UV_SCULPT_ALL_ISLANDS);
|
|
int island_index = 0;
|
|
data->tool = (RNA_enum_get(op->ptr, "mode") == BRUSH_STROKE_SMOOTH) ?
|
|
UV_SCULPT_TOOL_RELAX :
|
|
eBrushUVSculptTool(brush->uv_sculpt_tool);
|
|
data->invert = (RNA_enum_get(op->ptr, "mode") == BRUSH_STROKE_INVERT) ? 1 : 0;
|
|
|
|
data->uvsculpt = &ts->uvsculpt->paint;
|
|
|
|
/* 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__));
|
|
edgeHash = BLI_ghash_new(uv_edge_hash, uv_edge_compare, "uv_brush_edge_hash");
|
|
/* we have at most totalUVs edges */
|
|
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_TOOL_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_TOOL_GRAB) {
|
|
float radius, radius_root;
|
|
UvSculptData *sculptdata = (UvSculptData *)op->customdata;
|
|
int width, height;
|
|
float aspectRatio;
|
|
float alpha, zoomx, zoomy;
|
|
Brush *brush = BKE_paint_brush(sculptdata->uvsculpt);
|
|
|
|
alpha = BKE_brush_alpha_get(scene, brush);
|
|
|
|
radius = BKE_brush_size_get(scene, brush);
|
|
ED_space_image_get_size(sima, &width, &height);
|
|
ED_space_image_get_zoom(sima, region, &zoomx, &zoomy);
|
|
|
|
aspectRatio = width / float(height);
|
|
radius /= (width * zoomx);
|
|
radius = radius * radius;
|
|
radius_root = sqrtf(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) {
|
|
float strength;
|
|
strength = alpha * BKE_brush_curve_strength_clamped(brush, sqrtf(dist), radius_root);
|
|
|
|
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) {
|
|
ED_uvedit_live_unwrap_begin(scene, obedit);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 bool uv_sculpt_stroke_poll(bContext *C)
|
|
{
|
|
if (ED_operator_uvedit_space_image(C)) {
|
|
/* While these values could be initialized on demand,
|
|
* the only case this would be useful is running from the operator search popup.
|
|
* This is such a corner case that it's simpler to check a brush has already been created
|
|
* (something the tool system ensures). */
|
|
Scene *scene = CTX_data_scene(C);
|
|
ToolSettings *ts = scene->toolsettings;
|
|
Brush *brush = BKE_paint_brush(&ts->uvsculpt->paint);
|
|
if (brush != nullptr) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SCULPT_OT_uv_sculpt_stroke(wmOperatorType *ot)
|
|
{
|
|
static const EnumPropertyItem stroke_mode_items[] = {
|
|
{BRUSH_STROKE_NORMAL, "NORMAL", 0, "Regular", "Apply brush normally"},
|
|
{BRUSH_STROKE_INVERT,
|
|
"INVERT",
|
|
0,
|
|
"Invert",
|
|
"Invert action of brush for duration of stroke"},
|
|
{BRUSH_STROKE_SMOOTH,
|
|
"RELAX",
|
|
0,
|
|
"Relax",
|
|
"Switch brush to relax mode for duration of stroke"},
|
|
{0},
|
|
};
|
|
|
|
/* identifiers */
|
|
ot->name = "Sculpt UVs";
|
|
ot->description = "Sculpt UVs using a brush";
|
|
ot->idname = "SCULPT_OT_uv_sculpt_stroke";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = uv_sculpt_stroke_invoke;
|
|
ot->modal = uv_sculpt_stroke_modal;
|
|
ot->poll = uv_sculpt_stroke_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
|
|
/* props */
|
|
PropertyRNA *prop;
|
|
|
|
prop = RNA_def_enum(
|
|
ot->srna, "mode", stroke_mode_items, BRUSH_STROKE_NORMAL, "Mode", "Stroke Mode");
|
|
RNA_def_property_flag(prop, PropertyFlag(PROP_SKIP_SAVE));
|
|
}
|