diff options
4 files changed, 344 insertions, 0 deletions
diff --git a/release/scripts/presets/keyconfig/keymap_data/blender_default.py b/release/scripts/presets/keyconfig/keymap_data/blender_default.py index 57b31175728..4b037f209bb 100644 --- a/release/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/release/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -6302,6 +6302,18 @@ def km_3d_view_tool_sculpt_color_filter(params): ]}, ) +def km_3d_view_tool_sculpt_mask_by_color(params): + return ( + "3D View Tool: Sculpt, Mask By Color", + {"space_type": 'VIEW_3D', "region_type": 'WINDOW'}, + {"items": [ + ("sculpt.mask_by_color", {"type": params.tool_mouse, "value": 'ANY'}, + None), + ("sculpt.mask_by_color", {"type": params.tool_tweak, "value": 'ANY'}, + None), + ]}, + ) + def km_3d_view_tool_paint_weight_sample_weight(params): return ( "3D View Tool: Paint Weight, Sample Weight", @@ -6843,6 +6855,7 @@ def generate_keymaps(params=None): km_3d_view_tool_sculpt_mesh_filter(params), km_3d_view_tool_sculpt_cloth_filter(params), km_3d_view_tool_sculpt_color_filter(params), + km_3d_view_tool_sculpt_mask_by_color(params), km_3d_view_tool_paint_weight_sample_weight(params), km_3d_view_tool_paint_weight_sample_vertex_group(params), km_3d_view_tool_paint_weight_gradient(params), diff --git a/release/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/release/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 2391678b99b..87464451632 100644 --- a/release/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/release/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -1276,6 +1276,24 @@ class _defs_sculpt: draw_settings=draw_settings, ) + @ToolDef.from_fn + def mask_by_color(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("sculpt.mask_by_color") + layout.prop(props, "threshold") + layout.prop(props, "contiguous") + layout.prop(props, "invert") + layout.prop(props, "preserve_previous_mask") + + return dict( + idname="builtin.mask_by_color", + label="Mask By Color", + icon="ops.sculpt.mask_by_color", + widget=None, + keymap=(), + draw_settings=draw_settings, + ) + class _defs_vertex_paint: @@ -2453,6 +2471,8 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_sculpt.cloth_filter, _defs_sculpt.color_filter, None, + _defs_sculpt.mask_by_color, + None, _defs_transform.translate, _defs_transform.rotate, _defs_transform.scale, diff --git a/source/blender/editors/sculpt_paint/sculpt.c b/source/blender/editors/sculpt_paint/sculpt.c index 929f466f808..2873f2003cf 100644 --- a/source/blender/editors/sculpt_paint/sculpt.c +++ b/source/blender/editors/sculpt_paint/sculpt.c @@ -8466,6 +8466,306 @@ void SCULPT_fake_neighbors_free(Object *ob) sculpt_pose_fake_neighbors_free(ss); } +/* sculpt_mask_by_color_delta_get returns values in the (0,1) range that are used to generate the + * mask based on the diference between two colors (the active color and the color of any other + * vertex). Ideally, a threshold of 0 should mask only the colors that are equal to the active + * color and threshold of 1 should mask all colors. In order to avoid artifacts and produce softer + * falloffs in the mask, the MASK_BY_COLOR_SLOPE defines the size of the transition values between + * masked and unmasked vertices. The smaller this value is, the sharper the generated mask is going + * to be. */ +#define MASK_BY_COLOR_SLOPE 0.25f + +static float sculpt_mask_by_color_delta_get(const float *color_a, + const float *color_b, + const float threshold, + const bool invert) +{ + float len = len_v3v3(color_a, color_b); + /* Normalize len to the (0, 1) range. */ + len = len / M_SQRT3; + + if (len < threshold - MASK_BY_COLOR_SLOPE) { + len = 1.0f; + } + else if (len >= threshold) { + len = 0.0f; + } + else { + len = (-len + threshold) / MASK_BY_COLOR_SLOPE; + } + + if (invert) { + return 1.0f - len; + } + return len; +} + +static float sculpt_mask_by_color_final_mask_get(const float current_mask, + const float new_mask, + const bool invert, + const bool preserve_mask) +{ + if (preserve_mask) { + if (invert) { + return min_ff(current_mask, new_mask); + } + return max_ff(current_mask, new_mask); + } + return new_mask; +} + +typedef struct MaskByColorContiguousFloodFillData { + float threshold; + bool invert; + float *new_mask; + float initial_color[3]; +} MaskByColorContiguousFloodFillData; + +static void do_mask_by_color_contiguous_update_nodes_cb( + void *__restrict userdata, const int n, const TaskParallelTLS *__restrict UNUSED(tls)) +{ + SculptThreadedTaskData *data = userdata; + SculptSession *ss = data->ob->sculpt; + + SCULPT_undo_push_node(data->ob, data->nodes[n], SCULPT_UNDO_MASK); + bool update_node = false; + + const bool invert = data->mask_by_color_invert; + const bool preserve_mask = data->mask_by_color_preserve_mask; + + PBVHVertexIter vd; + BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE) + { + const float current_mask = *vd.mask; + const float new_mask = data->mask_by_color_floodfill[vd.index]; + *vd.mask = sculpt_mask_by_color_final_mask_get(current_mask, new_mask, invert, preserve_mask); + if (current_mask != *vd.mask) { + update_node = true; + if (vd.mvert) { + vd.mvert->flag |= ME_VERT_PBVH_UPDATE; + } + } + } + BKE_pbvh_vertex_iter_end; + if (update_node) { + BKE_pbvh_node_mark_redraw(data->nodes[n]); + } +} + +static bool sculpt_mask_by_color_contiguous_floodfill_cb( + SculptSession *ss, int from_v, int to_v, bool is_duplicate, void *userdata) +{ + MaskByColorContiguousFloodFillData *data = userdata; + const float *current_color = SCULPT_vertex_color_get(ss, to_v); + float new_vertex_mask = sculpt_mask_by_color_delta_get( + current_color, data->initial_color, data->threshold, data->invert); + data->new_mask[to_v] = new_vertex_mask; + + if (is_duplicate) { + data->new_mask[to_v] = data->new_mask[from_v]; + } + + float len = len_v3v3(current_color, data->initial_color); + len = len / M_SQRT3; + return len <= data->threshold; +} + +static void sculpt_mask_by_color_contiguous(Object *object, + const int vertex, + const float threshold, + const bool invert, + const bool preserve_mask) +{ + SculptSession *ss = object->sculpt; + const int totvert = SCULPT_vertex_count_get(ss); + + float *new_mask = MEM_calloc_arrayN(totvert, sizeof(float), "new mask"); + + if (invert) { + for (int i = 0; i < totvert; i++) { + new_mask[i] = 1.0f; + } + } + + SculptFloodFill flood; + SCULPT_floodfill_init(ss, &flood); + SCULPT_floodfill_add_initial(&flood, vertex); + + MaskByColorContiguousFloodFillData ffd; + ffd.threshold = threshold; + ffd.invert = invert; + ffd.new_mask = new_mask; + copy_v3_v3(ffd.initial_color, SCULPT_vertex_color_get(ss, vertex)); + + SCULPT_floodfill_execute(ss, &flood, sculpt_mask_by_color_contiguous_floodfill_cb, &ffd); + SCULPT_floodfill_free(&flood); + + int totnode; + PBVHNode **nodes; + BKE_pbvh_search_gather(ss->pbvh, NULL, NULL, &nodes, &totnode); + + SculptThreadedTaskData data = { + .ob = object, + .nodes = nodes, + .mask_by_color_floodfill = new_mask, + .mask_by_color_vertex = vertex, + .mask_by_color_threshold = threshold, + .mask_by_color_invert = invert, + .mask_by_color_preserve_mask = preserve_mask, + }; + + TaskParallelSettings settings; + BKE_pbvh_parallel_range_settings(&settings, true, totnode); + BLI_task_parallel_range( + 0, totnode, &data, do_mask_by_color_contiguous_update_nodes_cb, &settings); + + MEM_SAFE_FREE(nodes); + + MEM_freeN(new_mask); +} + +static void do_mask_by_color_task_cb(void *__restrict userdata, + const int n, + const TaskParallelTLS *__restrict UNUSED(tls)) +{ + SculptThreadedTaskData *data = userdata; + SculptSession *ss = data->ob->sculpt; + + SCULPT_undo_push_node(data->ob, data->nodes[n], SCULPT_UNDO_MASK); + bool update_node = false; + + const float threshold = data->mask_by_color_threshold; + const bool invert = data->mask_by_color_invert; + const bool preserve_mask = data->mask_by_color_preserve_mask; + const float *active_color = SCULPT_vertex_color_get(ss, data->mask_by_color_vertex); + + PBVHVertexIter vd; + BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE) + { + const float current_mask = *vd.mask; + const float new_mask = sculpt_mask_by_color_delta_get(active_color, vd.col, threshold, invert); + *vd.mask = sculpt_mask_by_color_final_mask_get(current_mask, new_mask, invert, preserve_mask); + + if (current_mask != *vd.mask) { + update_node = true; + if (vd.mvert) { + vd.mvert->flag |= ME_VERT_PBVH_UPDATE; + } + } + } + BKE_pbvh_vertex_iter_end; + if (update_node) { + BKE_pbvh_node_mark_redraw(data->nodes[n]); + } +} + +static void sculpt_mask_by_color_full_mesh(Object *object, + const int vertex, + const float threshold, + const bool invert, + const bool preserve_mask) +{ + SculptSession *ss = object->sculpt; + + int totnode; + PBVHNode **nodes; + BKE_pbvh_search_gather(ss->pbvh, NULL, NULL, &nodes, &totnode); + + SculptThreadedTaskData data = { + .ob = object, + .nodes = nodes, + .mask_by_color_vertex = vertex, + .mask_by_color_threshold = threshold, + .mask_by_color_invert = invert, + .mask_by_color_preserve_mask = preserve_mask, + }; + + TaskParallelSettings settings; + BKE_pbvh_parallel_range_settings(&settings, true, totnode); + BLI_task_parallel_range(0, totnode, &data, do_mask_by_color_task_cb, &settings); + + MEM_SAFE_FREE(nodes); +} + +static int sculpt_mask_by_color_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C); + Object *ob = CTX_data_active_object(C); + SculptSession *ss = ob->sculpt; + + BKE_sculpt_update_object_for_edit(depsgraph, ob, true, true, false); + + if (!ss->vcol) { + return OPERATOR_CANCELLED; + } + + SCULPT_vertex_random_access_init(ss); + + /* Tools that are not brushes do not have the brush gizmo to update the vertex as the mouse move, + * so it needs to be updated here. */ + SculptCursorGeometryInfo sgi; + float mouse[2]; + mouse[0] = event->mval[0]; + mouse[1] = event->mval[1]; + SCULPT_cursor_geometry_info_update(C, &sgi, mouse, false); + + SCULPT_undo_push_begin("Mask by color"); + + const int active_vertex = SCULPT_active_vertex_get(ss); + const float threshold = RNA_float_get(op->ptr, "threshold"); + const bool invert = RNA_boolean_get(op->ptr, "invert"); + const bool preserve_mask = RNA_boolean_get(op->ptr, "preserve_previous_mask"); + + if (RNA_boolean_get(op->ptr, "contiguous")) { + sculpt_mask_by_color_contiguous(ob, active_vertex, threshold, invert, preserve_mask); + } + else { + sculpt_mask_by_color_full_mesh(ob, active_vertex, threshold, invert, preserve_mask); + } + + BKE_pbvh_update_vertex_data(ss->pbvh, PBVH_UpdateMask); + SCULPT_undo_push_end(); + + SCULPT_flush_update_done(C, ob, SCULPT_UPDATE_MASK); + + return OPERATOR_FINISHED; +} + +static void SCULPT_OT_mask_by_color(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Mask By Color"; + ot->idname = "SCULPT_OT_mask_by_color"; + ot->description = "Creates a mask based on the sculpt vertex colors"; + + /* api callbacks */ + ot->invoke = sculpt_mask_by_color_invoke; + ot->poll = SCULPT_mode_poll; + + ot->flag = OPTYPE_REGISTER; + + ot->prop = RNA_def_boolean( + ot->srna, "contiguous", false, "Contiguous", "Mask only contiguos color areas"); + + ot->prop = RNA_def_boolean(ot->srna, "invert", false, "Invert", "Invert the generated mask"); + ot->prop = RNA_def_boolean( + ot->srna, + "preserve_previous_mask", + false, + "Preserve Previous Mask", + "Preserve the previous mask and add or substract the new one generated by the colors"); + + RNA_def_float(ot->srna, + "threshold", + 0.35f, + 0.0f, + 1.0f, + "Threshold", + "How much changes in color affect the mask generation", + 0.0f, + 1.0f); +} + void ED_operatortypes_sculpt(void) { WM_operatortype_append(SCULPT_OT_brush_stroke); @@ -8492,4 +8792,5 @@ void ED_operatortypes_sculpt(void) WM_operatortype_append(SCULPT_OT_loop_to_vertex_colors); WM_operatortype_append(SCULPT_OT_vertex_to_loop_colors); WM_operatortype_append(SCULPT_OT_color_filter); + WM_operatortype_append(SCULPT_OT_mask_by_color); } diff --git a/source/blender/editors/sculpt_paint/sculpt_intern.h b/source/blender/editors/sculpt_paint/sculpt_intern.h index 317e156760e..c7e07721eb7 100644 --- a/source/blender/editors/sculpt_paint/sculpt_intern.h +++ b/source/blender/editors/sculpt_paint/sculpt_intern.h @@ -633,6 +633,16 @@ typedef struct SculptThreadedTaskData { float dirty_mask_max; bool dirty_mask_dirty_only; + /* Mask By Color Tool */ + + float mask_by_color_threshold; + bool mask_by_color_invert; + bool mask_by_color_preserve_mask; + + /* Index of the vertex that is going to be used as a reference for the colors. */ + int mask_by_color_vertex; + float *mask_by_color_floodfill; + int face_set; int filter_undo_type; |