GPv3: Weight Paint tools (Draw, Blur, Average, Smear, Sample weight)

This PR implements the Weight Paint tools for GPv3.

Tools:
- Draw, for assigning weight to stroke points
- Blur, smooths out weight using adjacent stroke point weights
- Average, smooths weight using the average weight under the brush
- Smear, like finger painting, drags weights in the direction of the brush
- Sample weight, sets the brush weight to the weight under the cursor

The weights are assigned to the active vertex group. When there is no
active vertex group, a group is automatically created.

When the Auto Normalize option is enabled, it is ensured that all
bone-deforming vertex groups add up to the weight of 1.0.
When a vertex group is locked, it's weights will not be altered by
Auto Normalize.

The PR already supports multi frame editing, including the use of a
falloff (defined by a curve).

The implementation is in accordance with the Weight Paint tools in GPv2.

Pull Request: https://projects.blender.org/blender/blender/pulls/118347
This commit is contained in:
Sietse Brouwer 2024-04-25 15:21:14 +02:00 committed by Falk David
parent 07f2a0ee9f
commit 5220caeabb
25 changed files with 1760 additions and 41 deletions

@ -4699,6 +4699,58 @@ def km_grease_pencil_sculpt_mode(params):
return keymap
def km_grease_pencil_weight_paint(params):
# NOTE: This keymap falls through to "Pose" when an armature modifying the GP object
# is selected in weight paint mode. When editing the key-map take care that pose operations
# (such as transforming bones) is not impacted.
items = []
keymap = (
"Grease Pencil Weight Paint",
{"space_type": 'EMPTY', "region_type": 'WINDOW'},
{"items": items},
)
items.extend([
# Paint weight
("grease_pencil.weight_brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("grease_pencil.weight_brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
# Increase/Decrease brush size
("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True},
{"properties": [("scalar", 0.9)]}),
("brush.scale_size", {"type": 'RIGHT_BRACKET', "value": 'PRESS', "repeat": True},
{"properties": [("scalar", 1.0 / 0.9)]}),
# Radial controls
*_template_paint_radial_control("gpencil_weight_paint"),
("wm.radial_control", {"type": 'F', "value": 'PRESS', "ctrl": True},
radial_control_properties("gpencil_weight_paint", 'weight', 'use_unified_weight')),
# Toggle Add/Subtract for weight draw tool
("grease_pencil.weight_toggle_direction", {"type": 'D', "value": 'PRESS'}, None),
# Sample weight
("grease_pencil.weight_sample", {"type": 'X', "value": 'PRESS', "shift": True}, None),
# Context menu
*_template_items_context_panel("VIEW3D_PT_gpencil_weight_context_menu", params.context_menu_event),
# Show/hide layer
*_template_items_hide_reveal_actions("grease_pencil.layer_hide", "grease_pencil.layer_reveal"),
])
if params.select_mouse == 'LEFTMOUSE':
# Bone selection for combined weight paint + pose mode (Alt).
items.extend([
("view3d.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True}, None),
("view3d.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "alt": True},
{"properties": [("toggle", True)]}),
# Ctrl-Shift-LMB is needed for MMB emulation (which conflicts with Alt).
# NOTE: this works reasonably well for pose-mode where typically selecting a single bone is sufficient.
# For selecting faces/vertices, this is less useful. Selection tools are needed in this case.
("view3d.select", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True, "shift": True}, None),
])
return keymap
# ------------------------------------------------------------------------------
# Object/Pose Modes
@ -8091,6 +8143,7 @@ def km_3d_view_tool_paint_weight_sample_weight(params):
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
{"items": [
("paint.weight_sample", {"type": params.tool_mouse, "value": 'PRESS'}, None),
("grease_pencil.weight_sample", {"type": params.tool_mouse, "value": 'PRESS'}, None),
]},
)
@ -8785,6 +8838,7 @@ def generate_keymaps(params=None):
km_grease_pencil_paint_mode(params),
km_grease_pencil_edit_mode(params),
km_grease_pencil_sculpt_mode(params),
km_grease_pencil_weight_paint(params),
# Object mode.
km_object_mode(params),
km_object_non_modal(params),

@ -84,7 +84,7 @@ class GreasePencilDisplayPanel:
settings = tool_settings.gpencil_paint
elif context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint
@ -102,7 +102,7 @@ class GreasePencilDisplayPanel:
settings = tool_settings.gpencil_paint
elif context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint
@ -156,7 +156,7 @@ class GreasePencilBrushFalloff:
settings = tool_settings.gpencil_paint
if context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint
@ -931,7 +931,7 @@ class GreasePencilFlipTintColors(Operator):
settings = tool_settings.gpencil_paint
if context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint
@ -945,7 +945,7 @@ class GreasePencilFlipTintColors(Operator):
settings = tool_settings.gpencil_paint
if context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint

@ -86,6 +86,8 @@ class UnifiedPaintPanel:
return tool_settings.gpencil_paint
elif mode == 'SCULPT_GREASE_PENCIL':
return tool_settings.gpencil_sculpt_paint
elif mode == 'WEIGHT_GREASE_PENCIL':
return tool_settings.gpencil_weight_paint
return None
@staticmethod
@ -1580,6 +1582,46 @@ def brush_basic_gpencil_vertex_settings(layout, _context, brush, *, compact=Fals
row.prop(gp_settings, "vertex_mode", text="Mode")
def brush_basic_grease_pencil_weight_settings(layout, context, brush, *, compact=False):
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"size",
pressure_name="use_pressure_size",
unified_name="use_unified_size",
text="Radius",
slider=True,
header=compact,
)
capabilities = brush.sculpt_capabilities
pressure_name = "use_pressure_strength" if capabilities.has_strength_pressure else None
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"strength",
pressure_name=pressure_name,
unified_name="use_unified_strength",
text="Strength",
header=compact,
)
if brush.gpencil_weight_tool in {'WEIGHT'}:
UnifiedPaintPanel.prop_unified(
layout,
context,
brush,
"weight",
unified_name="use_unified_weight",
text="Weight",
slider=True,
header=compact,
)
layout.prop(brush, "direction", expand=True, text="" if compact else "Direction")
classes = (
VIEW3D_MT_tools_projectpaint_clone,
)

@ -2678,9 +2678,9 @@ class _defs_grease_pencil_weight:
type=bpy.types.Brush,
# Uses GPv2 tool settings
attr="gpencil_weight_tool",
# tooldef_keywords=dict(
# operator="grease_pencil.weight_paint",
# ),
tooldef_keywords=dict(
operator="grease_pencil.weight_brush_stroke",
),
)

@ -12,6 +12,7 @@ from bl_ui.properties_paint_common import (
UnifiedPaintPanel,
brush_basic_texpaint_settings,
brush_basic_gpencil_weight_settings,
brush_basic_grease_pencil_weight_settings,
)
from bl_ui.properties_grease_pencil_common import (
AnnotationDataPanel,
@ -128,7 +129,7 @@ class VIEW3D_HT_tool_header(Header):
if tool in {'SMOOTH', 'RANDOMIZE'}:
layout.popover("VIEW3D_PT_tools_grease_pencil_sculpt_brush_popover")
layout.popover("VIEW3D_PT_tools_grease_pencil_sculpt_appearance")
elif tool_mode == 'WEIGHT_GPENCIL':
elif tool_mode == 'WEIGHT_GPENCIL' or tool_mode == 'WEIGHT_GREASE_PENCIL':
if is_valid_context:
layout.popover("VIEW3D_PT_tools_grease_pencil_weight_appearance")
elif tool_mode == 'VERTEX_GPENCIL':
@ -496,6 +497,25 @@ class _draw_tool_settings_context_mode:
return True
@staticmethod
def WEIGHT_GREASE_PENCIL(context, layout, tool):
if (tool is None) or (not tool.has_datablock):
return False
paint = context.tool_settings.gpencil_weight_paint
layout.template_ID_preview(paint, "brush", rows=3, cols=8, hide_buttons=True)
brush = paint.brush
if brush is None:
return False
brush_basic_grease_pencil_weight_settings(layout, context, brush, compact=True)
layout.popover("VIEW3D_PT_tools_grease_pencil_weight_options", text="Options")
layout.popover("VIEW3D_PT_tools_grease_pencil_brush_weight_falloff", text="Falloff")
return True
@staticmethod
def VERTEX_GPENCIL(context, layout, tool):
if (tool is None) or (not tool.has_datablock):
@ -874,11 +894,11 @@ class VIEW3D_HT_header(Header):
row = layout.row(align=True)
row.prop(tool_settings, "use_gpencil_draw_additive", text="", icon='FREEZE')
if object_mode in {'PAINT_GPENCIL', 'EDIT', 'WEIGHT_PAINT'}:
if object_mode in {'PAINT_GPENCIL', 'EDIT', 'WEIGHT_GPENCIL'}:
row = layout.row(align=True)
row.prop(tool_settings, "use_grease_pencil_multi_frame_editing", text="")
if object_mode in {'EDIT', 'WEIGHT_PAINT'}:
if object_mode in {'EDIT', 'WEIGHT_GPENCIL'}:
sub = row.row(align=True)
sub.enabled = tool_settings.use_grease_pencil_multi_frame_editing
sub.popover(
@ -7945,6 +7965,7 @@ class VIEW3D_PT_overlay_grease_pencil_options(Panel):
layout.label(text={
'PAINT_GREASE_PENCIL': iface_("Draw Grease Pencil"),
'EDIT_GREASE_PENCIL': iface_("Edit Grease Pencil"),
'WEIGHT_GREASE_PENCIL': iface_("Weight Grease Pencil"),
'OBJECT': iface_("Grease Pencil"),
}[context.mode], translate=False)
@ -8495,7 +8516,11 @@ class VIEW3D_PT_gpencil_weight_context_menu(Panel):
layout = self.layout
# Weight settings
brush_basic_gpencil_weight_settings(layout, context, brush)
if context.mode == 'WEIGHT_GPENCIL':
brush_basic_gpencil_weight_settings(layout, context, brush)
else:
# Grease Pencil v3
brush_basic_grease_pencil_weight_settings(layout, context, brush)
# Layers
draw_gpencil_layer_active(context, layout)

@ -76,7 +76,7 @@ class VIEW3D_MT_brush_gpencil_context_menu(Menu):
settings = tool_settings.gpencil_paint
if context.mode == 'SCULPT_GPENCIL':
settings = tool_settings.gpencil_sculpt_paint
elif context.mode == 'WEIGHT_GPENCIL':
elif context.mode == 'WEIGHT_GPENCIL' or context.mode == 'WEIGHT_GREASE_PENCIL':
settings = tool_settings.gpencil_weight_paint
elif context.mode == 'VERTEX_GPENCIL':
settings = tool_settings.gpencil_vertex_paint
@ -2110,6 +2110,9 @@ class GreasePencilWeightPanel:
@classmethod
def poll(cls, context):
if context.space_data.type in {'VIEW_3D', 'PROPERTIES'}:
if context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'WEIGHT_GREASE_PENCIL':
return True
if context.gpencil_data is None:
return False
@ -2136,7 +2139,7 @@ class VIEW3D_PT_tools_grease_pencil_weight_paint_select(View3DPanel, Panel, Grea
col = row.column()
col.menu("VIEW3D_MT_brush_gpencil_context_menu", icon='DOWNARROW_HLT', text="")
if context.mode == 'WEIGHT_GPENCIL':
if context.mode in {'WEIGHT_GPENCIL', 'WEIGHT_GREASE_PENCIL'}:
brush = tool_settings.gpencil_weight_paint.brush
if brush is not None:
col.prop(brush, "use_custom_icon", toggle=True, icon='FILE_IMAGE', text="")
@ -2160,10 +2163,17 @@ class VIEW3D_PT_tools_grease_pencil_weight_paint_settings(Panel, View3DPanel, Gr
settings = tool_settings.gpencil_weight_paint
brush = settings.brush
from bl_ui.properties_paint_common import (
brush_basic_gpencil_weight_settings,
)
brush_basic_gpencil_weight_settings(layout, context, brush)
if context.mode == 'WEIGHT_GPENCIL':
from bl_ui.properties_paint_common import (
brush_basic_gpencil_weight_settings,
)
brush_basic_gpencil_weight_settings(layout, context, brush)
else:
# Grease Pencil v3
from bl_ui.properties_paint_common import (
brush_basic_grease_pencil_weight_settings,
)
brush_basic_grease_pencil_weight_settings(layout, context, brush)
class VIEW3D_PT_tools_grease_pencil_brush_weight_falloff(GreasePencilBrushFalloff, Panel, View3DPaintPanel):

@ -16,6 +16,9 @@ namespace blender::bke::greasepencil {
/** Make sure drawings only contain vertex groups of the #GreasePencil. */
void validate_drawing_vertex_groups(GreasePencil &grease_pencil);
/** Find or create a vertex group in a drawing. */
int ensure_vertex_group(const StringRef name, ListBase &vertex_group_names);
/** Assign selected vertices to the vertex group. */
void assign_to_vertex_group(GreasePencil &grease_pencil, StringRef name, float weight);

@ -52,6 +52,19 @@ void validate_drawing_vertex_groups(GreasePencil &grease_pencil)
}
}
int ensure_vertex_group(const StringRef name, ListBase &vertex_group_names)
{
int def_nr = BLI_findstringindex(&vertex_group_names, name.data(), offsetof(bDeformGroup, name));
if (def_nr < 0) {
bDeformGroup *defgroup = MEM_cnew<bDeformGroup>(__func__);
STRNCPY(defgroup->name, name.data());
BLI_addtail(&vertex_group_names, defgroup);
def_nr = BLI_listbase_count(&vertex_group_names) - 1;
BLI_assert(def_nr >= 0);
}
return def_nr;
}
void assign_to_vertex_group(GreasePencil &grease_pencil, const StringRef name, const float weight)
{
for (GreasePencilDrawingBase *base : grease_pencil.drawings()) {

@ -715,9 +715,21 @@ bool BKE_object_defgroup_check_lock_relative_multi(int defbase_tot,
bool BKE_object_defgroup_active_is_locked(const Object *ob)
{
Mesh *mesh = static_cast<Mesh *>(ob->data);
bDeformGroup *dg = static_cast<bDeformGroup *>(
BLI_findlink(&mesh->vertex_group_names, mesh->vertex_group_active_index - 1));
bDeformGroup *dg;
switch (ob->type) {
case OB_GREASE_PENCIL: {
GreasePencil *grease_pencil = static_cast<GreasePencil *>(ob->data);
dg = static_cast<bDeformGroup *>(BLI_findlink(&grease_pencil->vertex_group_names,
grease_pencil->vertex_group_active_index - 1));
break;
}
default: {
Mesh *mesh = static_cast<Mesh *>(ob->data);
dg = static_cast<bDeformGroup *>(
BLI_findlink(&mesh->vertex_group_names, mesh->vertex_group_active_index - 1));
break;
}
}
return dg->flag & DG_LOCK_WEIGHT;
}

@ -635,6 +635,13 @@ void GPENCIL_OT_sculptmode_toggle(wmOperatorType *ot)
/* Stroke Weight Paint Mode Management */
static bool grease_pencil_poll_weight_cursor(bContext *C)
{
Object *ob = CTX_data_active_object(C);
return ob && (ob->mode & OB_MODE_WEIGHT_GPENCIL_LEGACY) && (ob->type == OB_GREASE_PENCIL) &&
CTX_wm_region_view3d(C) && WM_toolsystem_active_tool_is_brush(C);
}
static bool gpencil_weightmode_toggle_poll(bContext *C)
{
/* if using gpencil object, use this gpd */
@ -648,6 +655,7 @@ static bool gpencil_weightmode_toggle_poll(bContext *C)
static int gpencil_weightmode_toggle_exec(bContext *C, wmOperator *op)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ToolSettings *ts = CTX_data_tool_settings(C);
const bool back = RNA_boolean_get(op->ptr, "back");
@ -702,12 +710,17 @@ static int gpencil_weightmode_toggle_exec(bContext *C, wmOperator *op)
if (mode == OB_MODE_WEIGHT_GPENCIL_LEGACY) {
/* Be sure we have brushes. */
BKE_paint_ensure(ts, (Paint **)&ts->gp_weightpaint);
Paint *weight_paint = BKE_paint_get_active_from_paintmode(scene, PaintMode::WeightGPencil);
BKE_paint_ensure(ts, &weight_paint);
const bool reset_mode = (BKE_paint_brush(&ts->gp_weightpaint->paint) == nullptr);
if (ob->type == OB_GREASE_PENCIL) {
ED_paint_cursor_start(weight_paint, grease_pencil_poll_weight_cursor);
}
const bool reset_mode = (BKE_paint_brush(weight_paint) == nullptr);
BKE_brush_gpencil_weight_presets(bmain, ts, reset_mode);
BKE_paint_toolslots_brush_validate(bmain, &ts->gp_weightpaint->paint);
BKE_paint_toolslots_brush_validate(bmain, weight_paint);
}
/* setup other modes */

@ -34,6 +34,7 @@ set(SRC
intern/grease_pencil_select.cc
intern/grease_pencil_undo.cc
intern/grease_pencil_utils.cc
intern/grease_pencil_weight_paint.cc
)
set(LIB

@ -98,6 +98,22 @@ bool grease_pencil_sculpting_poll(bContext *C)
return true;
}
bool grease_pencil_weight_painting_poll(bContext *C)
{
if (!active_grease_pencil_poll(C)) {
return false;
}
Object *object = CTX_data_active_object(C);
if ((object->mode & OB_MODE_WEIGHT_GPENCIL_LEGACY) == 0) {
return false;
}
ToolSettings *ts = CTX_data_tool_settings(C);
if (!ts || !ts->gp_weightpaint) {
return false;
}
return true;
}
static void keymap_grease_pencil_edit_mode(wmKeyConfig *keyconf)
{
wmKeyMap *keymap = WM_keymap_ensure(
@ -119,6 +135,13 @@ static void keymap_grease_pencil_sculpt_mode(wmKeyConfig *keyconf)
keymap->poll = grease_pencil_sculpting_poll;
}
static void keymap_grease_pencil_weight_paint_mode(wmKeyConfig *keyconf)
{
wmKeyMap *keymap = WM_keymap_ensure(
keyconf, "Grease Pencil Weight Paint", SPACE_EMPTY, RGN_TYPE_WINDOW);
keymap->poll = grease_pencil_weight_painting_poll;
}
} // namespace blender::ed::greasepencil
void ED_operatortypes_grease_pencil()
@ -130,6 +153,7 @@ void ED_operatortypes_grease_pencil()
ED_operatortypes_grease_pencil_edit();
ED_operatortypes_grease_pencil_material();
ED_operatortypes_grease_pencil_primitives();
ED_operatortypes_grease_pencil_weight_paint();
}
void ED_operatormacros_grease_pencil()
@ -163,5 +187,6 @@ void ED_keymap_grease_pencil(wmKeyConfig *keyconf)
keymap_grease_pencil_edit_mode(keyconf);
keymap_grease_pencil_paint_mode(keyconf);
keymap_grease_pencil_sculpt_mode(keyconf);
keymap_grease_pencil_weight_paint_mode(keyconf);
ED_primitivetool_modal_keymap(keyconf);
}

@ -454,6 +454,91 @@ Vector<MutableDrawingInfo> retrieve_editable_drawings_with_falloff(const Scene &
return editable_drawings;
}
Array<Vector<MutableDrawingInfo>> retrieve_editable_drawings_grouped_per_frame(
const Scene &scene, GreasePencil &grease_pencil)
{
using namespace blender::bke::greasepencil;
int current_frame = scene.r.cfra;
const ToolSettings *toolsettings = scene.toolsettings;
const bool use_multi_frame_editing = (toolsettings->gpencil_flags &
GP_USE_MULTI_FRAME_EDITING) != 0;
const bool use_multi_frame_falloff = use_multi_frame_editing &&
(toolsettings->gp_sculpt.flag &
GP_SCULPT_SETT_FLAG_FRAME_FALLOFF) != 0;
if (use_multi_frame_falloff) {
BKE_curvemapping_init(toolsettings->gp_sculpt.cur_falloff);
}
/* Get a set of unique frame numbers with editable drawings on them. */
VectorSet<int> selected_frames;
int frame_min = current_frame, frame_max = current_frame;
Span<const Layer *> layers = grease_pencil.layers();
if (use_multi_frame_editing) {
for (const int layer_i : layers.index_range()) {
const Layer &layer = *layers[layer_i];
if (!layer.is_editable()) {
continue;
}
for (const auto [frame_number, frame] : layer.frames().items()) {
if (frame_number != current_frame && frame.is_selected()) {
selected_frames.add(frame_number);
frame_min = math::min(frame_min, frame_number);
frame_max = math::max(frame_max, frame_number);
}
}
}
}
selected_frames.add(current_frame);
/* Get multi frame falloff factor per selected frame. */
Array<float> falloff_per_selected_frame(selected_frames.size(), 1.0f);
if (use_multi_frame_falloff) {
int frame_group = 0;
for (const int frame_number : selected_frames) {
falloff_per_selected_frame[frame_group] = get_multi_frame_falloff(
frame_number, current_frame, frame_min, frame_max, toolsettings->gp_sculpt.cur_falloff);
frame_group++;
}
}
/* Get drawings grouped per frame. */
Array<Vector<MutableDrawingInfo>> drawings_grouped_per_frame(selected_frames.size());
Set<int> added_drawings;
for (const int layer_i : layers.index_range()) {
const Layer &layer = *layers[layer_i];
if (!layer.is_editable()) {
continue;
}
/* In multi frame editing mode, add drawings at selected frames. */
if (use_multi_frame_editing) {
for (const auto [frame_number, frame] : layer.frames().items()) {
if (!frame.is_selected() || added_drawings.contains(frame.drawing_index)) {
continue;
}
if (Drawing *drawing = grease_pencil.get_editable_drawing_at(layer, frame_number)) {
const int frame_group = selected_frames.index_of(frame_number);
drawings_grouped_per_frame[frame_group].append(
{*drawing, layer_i, frame_number, falloff_per_selected_frame[frame_group]});
added_drawings.add(frame.drawing_index);
}
}
}
/* Add drawing at current frame. */
const int drawing_index_current_frame = layer.drawing_index_at(current_frame);
if (!added_drawings.contains(drawing_index_current_frame)) {
if (Drawing *drawing = grease_pencil.get_editable_drawing_at(layer, current_frame)) {
const int frame_group = selected_frames.index_of(current_frame);
drawings_grouped_per_frame[frame_group].append(
{*drawing, layer_i, current_frame, falloff_per_selected_frame[frame_group]});
added_drawings.add(drawing_index_current_frame);
}
}
}
return drawings_grouped_per_frame;
}
Vector<MutableDrawingInfo> retrieve_editable_drawings_from_layer(
const Scene &scene,
GreasePencil &grease_pencil,
@ -688,6 +773,39 @@ IndexMask retrieve_visible_strokes(Object &object,
});
}
IndexMask retrieve_visible_points(Object &object,
const bke::greasepencil::Drawing &drawing,
IndexMaskMemory &memory)
{
/* Get all the hidden material indices. */
VectorSet<int> hidden_material_indices = get_hidden_material_indices(object);
if (hidden_material_indices.is_empty()) {
return drawing.strokes().points_range();
}
const bke::CurvesGeometry &curves = drawing.strokes();
const IndexRange points_range = curves.points_range();
const bke::AttributeAccessor attributes = curves.attributes();
/* Propagate the material index to the points. */
const VArray<int> materials = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Point, 0);
if (const std::optional<int> single_material = materials.get_if_single()) {
if (!hidden_material_indices.contains(*single_material)) {
return points_range;
}
return {};
}
/* Get all the points that are part of a stroke with a visible material. */
return IndexMask::from_predicate(
points_range, GrainSize(4096), memory, [&](const int64_t point_i) {
const int material_index = materials[point_i];
return !hidden_material_indices.contains(material_index);
});
}
IndexMask retrieve_editable_and_selected_strokes(Object &object,
const bke::greasepencil::Drawing &drawing,
IndexMaskMemory &memory)

@ -0,0 +1,365 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edgreasepencil
*/
#include "BKE_brush.hh"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_deform.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_modifier.hh"
#include "BKE_paint.hh"
#include "DNA_meshdata_types.h"
#include "ED_curves.hh"
#include "ED_view3d.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_grease_pencil.hh"
namespace blender::ed::greasepencil {
Set<std::string> get_bone_deformed_vertex_group_names(const Object &object)
{
/* Get all vertex group names in the object. */
const ListBase *defbase = BKE_object_defgroup_list(&object);
Set<std::string> defgroups;
LISTBASE_FOREACH (bDeformGroup *, dg, defbase) {
defgroups.add(dg->name);
}
/* Inspect all armature modifiers in the object. */
Set<std::string> bone_deformed_vgroups;
VirtualModifierData virtual_modifier_data;
ModifierData *md = BKE_modifiers_get_virtual_modifierlist(&object, &virtual_modifier_data);
for (; md; md = md->next) {
if (!(md->mode & (eModifierMode_Realtime | eModifierMode_Virtual)) ||
md->type != eModifierType_Armature)
{
continue;
}
ArmatureModifierData *amd = reinterpret_cast<ArmatureModifierData *>(md);
if (!amd->object || !amd->object->pose) {
continue;
}
bPose *pose = amd->object->pose;
LISTBASE_FOREACH (bPoseChannel *, channel, &pose->chanbase) {
if (channel->bone->flag & BONE_NO_DEFORM) {
continue;
}
/* When a vertex group name matches the bone name, it is bone-deformed. */
if (defgroups.contains(channel->name)) {
bone_deformed_vgroups.add(channel->name);
}
}
}
return bone_deformed_vgroups;
}
/* Normalize the weights of vertex groups deformed by bones so that the sum is 1.0f.
* Returns false when the normalization failed due to too many locked vertex groups. In that case a
* second pass can be done with the active vertex group unlocked.
*/
static bool normalize_vertex_weights_try(MDeformVert &dvert,
const int vertex_groups_num,
const Span<bool> vertex_group_is_bone_deformed,
const FunctionRef<bool(int)> vertex_group_is_locked)
{
/* Nothing to normalize when there are less than two vertex group weights. */
if (dvert.totweight <= 1) {
return true;
}
/* Get the sum of weights of bone-deformed vertex groups. */
float sum_weights_total = 0.0f;
float sum_weights_locked = 0.0f;
float sum_weights_unlocked = 0.0f;
int locked_num = 0;
int unlocked_num = 0;
for (const int i : IndexRange(dvert.totweight)) {
MDeformWeight &dw = dvert.dw[i];
/* Auto-normalize is only applied on bone-deformed vertex groups that have weight already. */
if (dw.def_nr >= vertex_groups_num || !vertex_group_is_bone_deformed[dw.def_nr] ||
dw.weight <= FLT_EPSILON)
{
continue;
}
sum_weights_total += dw.weight;
if (vertex_group_is_locked(dw.def_nr)) {
locked_num++;
sum_weights_locked += dw.weight;
}
else {
unlocked_num++;
sum_weights_unlocked += dw.weight;
}
}
/* Already normalized? */
if (sum_weights_total == 1.0f) {
return true;
}
/* Any unlocked vertex group to normalize? */
if (unlocked_num == 0) {
/* We don't need a second pass when there is only one locked group (the active group). */
return (locked_num == 1);
}
/* Locked groups can make it impossible to fully normalize. */
if (sum_weights_locked >= 1.0f - VERTEX_WEIGHT_LOCK_EPSILON) {
/* Zero out the weights we are allowed to touch and return false, indicating a second pass is
* needed. */
for (const int i : IndexRange(dvert.totweight)) {
MDeformWeight &dw = dvert.dw[i];
if (dw.def_nr < vertex_groups_num && vertex_group_is_bone_deformed[dw.def_nr] &&
!vertex_group_is_locked(dw.def_nr))
{
dw.weight = 0.0f;
}
}
return (sum_weights_locked == 1.0f);
}
/* When the sum of the unlocked weights isn't zero, we can use a multiplier to normalize them
* to 1.0f. */
if (sum_weights_unlocked != 0.0f) {
const float normalize_factor = (1.0f - sum_weights_locked) / sum_weights_unlocked;
for (const int i : IndexRange(dvert.totweight)) {
MDeformWeight &dw = dvert.dw[i];
if (dw.def_nr < vertex_groups_num && vertex_group_is_bone_deformed[dw.def_nr] &&
dw.weight > FLT_EPSILON && !vertex_group_is_locked(dw.def_nr))
{
dw.weight = math::clamp(dw.weight * normalize_factor, 0.0f, 1.0f);
}
}
return true;
}
/* Spread out the remainder of the locked weights over the unlocked weights. */
const float weight_remainder = math::clamp(
(1.0f - sum_weights_locked) / unlocked_num, 0.0f, 1.0f);
for (const int i : IndexRange(dvert.totweight)) {
MDeformWeight &dw = dvert.dw[i];
if (dw.def_nr < vertex_groups_num && vertex_group_is_bone_deformed[dw.def_nr] &&
dw.weight > FLT_EPSILON && !vertex_group_is_locked(dw.def_nr))
{
dw.weight = weight_remainder;
}
}
return true;
}
void normalize_vertex_weights(MDeformVert &dvert,
const int active_vertex_group,
const Span<bool> vertex_group_is_locked,
const Span<bool> vertex_group_is_bone_deformed)
{
/* Try to normalize the weights with both active and explicitly locked vertex groups restricted
* from change. */
const auto active_vertex_group_is_locked = [&](const int vertex_group_index) {
return vertex_group_is_locked[vertex_group_index] || vertex_group_index == active_vertex_group;
};
const bool success = normalize_vertex_weights_try(dvert,
vertex_group_is_locked.size(),
vertex_group_is_bone_deformed,
active_vertex_group_is_locked);
if (success) {
return;
}
/* Do a second pass with the active vertex group unlocked. */
const auto active_vertex_group_is_unlocked = [&](const int vertex_group_index) {
return vertex_group_is_locked[vertex_group_index];
};
normalize_vertex_weights_try(dvert,
vertex_group_is_locked.size(),
vertex_group_is_bone_deformed,
active_vertex_group_is_unlocked);
}
struct ClosestGreasePencilDrawing {
const bke::greasepencil::Drawing *drawing = nullptr;
int active_defgroup_index;
ed::curves::FindClosestData elem = {};
};
static int weight_sample_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event)
{
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
ViewContext vc = ED_view3d_viewcontext_init(C, depsgraph);
/* Get the active vertex group. */
const int object_defgroup_nr = BKE_object_defgroup_active_index_get(vc.obact) - 1;
if (object_defgroup_nr == -1) {
return OPERATOR_CANCELLED;
}
const bDeformGroup *object_defgroup = static_cast<const bDeformGroup *>(
BLI_findlink(BKE_object_defgroup_list(vc.obact), object_defgroup_nr));
/* Collect visible drawings. */
const Object *ob_eval = DEG_get_evaluated_object(vc.depsgraph, const_cast<Object *>(vc.obact));
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(vc.obact->data);
const Vector<DrawingInfo> drawings = retrieve_visible_drawings(*vc.scene, grease_pencil, false);
/* Find stroke points closest to mouse cursor position. */
const ClosestGreasePencilDrawing closest = threading::parallel_reduce(
drawings.index_range(),
1L,
ClosestGreasePencilDrawing(),
[&](const IndexRange range, const ClosestGreasePencilDrawing &init) {
ClosestGreasePencilDrawing new_closest = init;
for (const int i : range) {
DrawingInfo info = drawings[i];
const bke::greasepencil::Layer &layer = *grease_pencil.layers()[info.layer_index];
/* Skip drawing when it doesn't use the active vertex group. */
const int drawing_defgroup_nr = BLI_findstringindex(
&info.drawing.strokes().vertex_group_names,
object_defgroup->name,
offsetof(bDeformGroup, name));
if (drawing_defgroup_nr == -1) {
continue;
}
/* Get deformation by modifiers. */
bke::crazyspace::GeometryDeformation deformation =
bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation(
ob_eval, *vc.obact, info.layer_index, info.frame_number);
IndexMaskMemory memory;
const IndexMask points = retrieve_visible_points(*vc.obact, info.drawing, memory);
if (points.is_empty()) {
continue;
}
const float4x4 layer_to_world = layer.to_world_space(*ob_eval);
const float4x4 projection = ED_view3d_ob_project_mat_get_from_obmat(vc.rv3d,
layer_to_world);
std::optional<ed::curves::FindClosestData> new_closest_elem =
ed::curves::closest_elem_find_screen_space(vc,
info.drawing.strokes().points_by_curve(),
deformation.positions,
projection,
points,
bke::AttrDomain::Point,
event->mval,
new_closest.elem);
if (new_closest_elem) {
new_closest.elem = *new_closest_elem;
new_closest.drawing = &info.drawing;
new_closest.active_defgroup_index = drawing_defgroup_nr;
}
}
return new_closest;
},
[](const ClosestGreasePencilDrawing &a, const ClosestGreasePencilDrawing &b) {
return (a.elem.distance < b.elem.distance) ? a : b;
});
if (!closest.drawing) {
return OPERATOR_CANCELLED;
}
/* From the closest point found, get the vertex weight in the active vertex group. */
const VArray<float> point_weights = bke::varray_for_deform_verts(
closest.drawing->strokes().deform_verts(), closest.active_defgroup_index);
const float new_weight = math::clamp(point_weights[closest.elem.index], 0.0f, 1.0f);
/* Set the new brush weight. */
const ToolSettings *ts = vc.scene->toolsettings;
Brush *brush = BKE_paint_brush(&ts->wpaint->paint);
BKE_brush_weight_set(vc.scene, brush, new_weight);
/* Update brush settings in UI. */
WM_main_add_notifier(NC_BRUSH | NA_EDITED, nullptr);
return OPERATOR_FINISHED;
}
static void GREASE_PENCIL_OT_weight_sample(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Weight Paint Sample Weight";
ot->idname = "GREASE_PENCIL_OT_weight_sample";
ot->description =
"Set the weight of the Draw tool to the weight of the vertex under the mouse cursor";
/* Callbacks. */
ot->poll = grease_pencil_weight_painting_poll;
ot->invoke = weight_sample_invoke;
/* Flags. */
ot->flag = OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
}
static int toggle_weight_tool_direction(bContext *C, wmOperator * /*op*/)
{
Paint *paint = BKE_paint_get_active_from_context(C);
Brush *brush = BKE_paint_brush(paint);
/* Toggle direction flag. */
brush->flag ^= BRUSH_DIR_IN;
/* Update brush settings in UI. */
WM_main_add_notifier(NC_BRUSH | NA_EDITED, nullptr);
return OPERATOR_FINISHED;
}
static bool toggle_weight_tool_direction_poll(bContext *C)
{
if (!grease_pencil_weight_painting_poll(C)) {
return false;
}
Paint *paint = BKE_paint_get_active_from_context(C);
if (paint == nullptr) {
return false;
}
Brush *brush = BKE_paint_brush(paint);
if (brush == nullptr) {
return false;
}
return brush->gpencil_weight_tool == GPWEIGHT_TOOL_DRAW;
}
static void GREASE_PENCIL_OT_weight_toggle_direction(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Weight Paint Toggle Direction";
ot->idname = "GREASE_PENCIL_OT_weight_toggle_direction";
ot->description = "Toggle Add/Subtract for the weight paint draw tool";
/* Callbacks. */
ot->poll = toggle_weight_tool_direction_poll;
ot->exec = toggle_weight_tool_direction;
/* Flags. */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
} // namespace blender::ed::greasepencil
void ED_operatortypes_grease_pencil_weight_paint()
{
using namespace blender::ed::greasepencil;
WM_operatortype_append(GREASE_PENCIL_OT_weight_toggle_direction);
WM_operatortype_append(GREASE_PENCIL_OT_weight_sample);
}

@ -13,6 +13,7 @@
#include "BLI_generic_span.hh"
#include "BLI_index_mask_fwd.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_set.hh"
#include "ED_keyframes_edit.hh"
@ -53,6 +54,7 @@ void ED_operatortypes_grease_pencil_select();
void ED_operatortypes_grease_pencil_edit();
void ED_operatortypes_grease_pencil_material();
void ED_operatortypes_grease_pencil_primitives();
void ED_operatortypes_grease_pencil_weight_paint();
void ED_operatormacros_grease_pencil();
void ED_keymap_grease_pencil(wmKeyConfig *keyconf);
void ED_primitivetool_modal_keymap(wmKeyConfig *keyconf);
@ -216,6 +218,7 @@ bool active_grease_pencil_layer_poll(bContext *C);
bool editable_grease_pencil_point_selection_poll(bContext *C);
bool grease_pencil_painting_poll(bContext *C);
bool grease_pencil_sculpting_poll(bContext *C);
bool grease_pencil_weight_painting_poll(bContext *C);
float opacity_from_input_sample(const float pressure,
const Brush *brush,
@ -249,6 +252,8 @@ Vector<MutableDrawingInfo> retrieve_editable_drawings(const Scene &scene,
GreasePencil &grease_pencil);
Vector<MutableDrawingInfo> retrieve_editable_drawings_with_falloff(const Scene &scene,
GreasePencil &grease_pencil);
Array<Vector<MutableDrawingInfo>> retrieve_editable_drawings_grouped_per_frame(
const Scene &scene, GreasePencil &grease_pencil);
Vector<MutableDrawingInfo> retrieve_editable_drawings_from_layer(
const Scene &scene, GreasePencil &grease_pencil, const bke::greasepencil::Layer &layer);
Vector<DrawingInfo> retrieve_visible_drawings(const Scene &scene,
@ -273,6 +278,9 @@ IndexMask retrieve_editable_elements(Object &object,
IndexMask retrieve_visible_strokes(Object &grease_pencil_object,
const bke::greasepencil::Drawing &drawing,
IndexMaskMemory &memory);
IndexMask retrieve_visible_points(Object &object,
const bke::greasepencil::Drawing &drawing,
IndexMaskMemory &memory);
IndexMask retrieve_editable_and_selected_strokes(Object &grease_pencil_object,
const bke::greasepencil::Drawing &drawing,
@ -363,4 +371,14 @@ Array<PointTransferData> compute_topology_change(
const Span<Vector<PointTransferData>> src_to_dst_points,
const bool keep_caps);
/** Returns a set of vertex group names that are deformed by a bone in an armature. */
Set<std::string> get_bone_deformed_vertex_group_names(const Object &object);
/** For a point in a stroke, normalize the weights of vertex groups deformed by bones so that the
* sum is 1.0f. */
void normalize_vertex_weights(MDeformVert &dvert,
int active_vertex_group,
Span<bool> vertex_group_is_locked,
Span<bool> vertex_group_is_bone_deformed);
} // namespace blender::ed::greasepencil

@ -44,6 +44,10 @@ set(SRC
grease_pencil_erase.cc
grease_pencil_paint.cc
grease_pencil_tint.cc
grease_pencil_weight_average.cc
grease_pencil_weight_blur.cc
grease_pencil_weight_draw.cc
grease_pencil_weight_smear.cc
paint_canvas.cc
paint_cursor.cc
paint_curve.cc
@ -93,6 +97,7 @@ set(SRC
curves_sculpt_intern.hh
grease_pencil_intern.hh
grease_pencil_weight_paint.hh
paint_intern.hh
sculpt_intern.hh
)

@ -3,7 +3,9 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BKE_deform.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_object_deform.h"
#include "BKE_report.hh"
#include "DEG_depsgraph_query.hh"
@ -325,6 +327,135 @@ static void GREASE_PENCIL_OT_sculpt_paint(wmOperatorType *ot)
/** \} */
/* -------------------------------------------------------------------- */
/** \name Weight Brush Stroke Operator
* \{ */
static bool weight_stroke_test_start(bContext *C, wmOperator *op, const float mouse[2])
{
InputSample start_sample;
start_sample.mouse_position = float2(mouse);
start_sample.pressure = 0.0f;
GreasePencilStrokeOperation *operation = nullptr;
Paint *paint = BKE_paint_get_active_from_context(C);
Brush *brush = BKE_paint_brush(paint);
const BrushStrokeMode brush_mode = BrushStrokeMode(RNA_enum_get(op->ptr, "mode"));
switch (eBrushGPWeightTool(brush->gpencil_weight_tool)) {
case GPWEIGHT_TOOL_DRAW:
operation = greasepencil::new_weight_paint_draw_operation(brush_mode).release();
break;
case GPWEIGHT_TOOL_BLUR:
operation = greasepencil::new_weight_paint_blur_operation().release();
break;
case GPWEIGHT_TOOL_AVERAGE:
operation = greasepencil::new_weight_paint_average_operation().release();
break;
case GPWEIGHT_TOOL_SMEAR:
operation = greasepencil::new_weight_paint_smear_operation().release();
break;
}
if (operation == nullptr) {
return false;
}
PaintStroke *paint_stroke = static_cast<PaintStroke *>(op->customdata);
paint_stroke_set_mode_data(paint_stroke, operation);
operation->on_stroke_begin(*C, start_sample);
return true;
}
static int grease_pencil_weight_brush_stroke_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
const Scene *scene = CTX_data_scene(C);
const Object *object = CTX_data_active_object(C);
if (!object || object->type != OB_GREASE_PENCIL) {
return OPERATOR_CANCELLED;
}
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object->data);
const Paint *paint = BKE_paint_get_active_from_context(C);
const Brush *brush = BKE_paint_brush_for_read(paint);
if (brush == nullptr) {
return OPERATOR_CANCELLED;
}
const Vector<ed::greasepencil::MutableDrawingInfo> drawings =
ed::greasepencil::retrieve_editable_drawings(*scene, grease_pencil);
if (drawings.is_empty()) {
BKE_report(op->reports, RPT_ERROR, "No Grease Pencil frame to draw weight on");
return OPERATOR_CANCELLED;
}
const int active_defgroup_nr = BKE_object_defgroup_active_index_get(object) - 1;
if (active_defgroup_nr >= 0 && BKE_object_defgroup_active_is_locked(object)) {
BKE_report(op->reports, RPT_WARNING, "Active group is locked, aborting");
return OPERATOR_CANCELLED;
}
op->customdata = paint_stroke_new(C,
op,
stroke_get_location,
weight_stroke_test_start,
stroke_update_step,
stroke_redraw,
stroke_done,
event->type);
const int return_value = op->type->modal(C, op, event);
if (return_value == OPERATOR_FINISHED) {
return OPERATOR_FINISHED;
}
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL;
}
static int grease_pencil_weight_brush_stroke_modal(bContext *C,
wmOperator *op,
const wmEvent *event)
{
return paint_stroke_modal(C, op, event, reinterpret_cast<PaintStroke **>(&op->customdata));
}
static void grease_pencil_weight_brush_stroke_cancel(bContext *C, wmOperator *op)
{
paint_stroke_cancel(C, op, static_cast<PaintStroke *>(op->customdata));
}
static bool grease_pencil_weight_brush_stroke_poll(bContext *C)
{
if (!ed::greasepencil::grease_pencil_weight_painting_poll(C)) {
return false;
}
if (!WM_toolsystem_active_tool_is_brush(C)) {
return false;
}
return true;
}
static void GREASE_PENCIL_OT_weight_brush_stroke(wmOperatorType *ot)
{
ot->name = "Grease Pencil Paint Weight";
ot->idname = "GREASE_PENCIL_OT_weight_brush_stroke";
ot->description = "Draw weight on stroke points in the active Grease Pencil object";
ot->poll = grease_pencil_weight_brush_stroke_poll;
ot->invoke = grease_pencil_weight_brush_stroke_invoke;
ot->modal = grease_pencil_weight_brush_stroke_modal;
ot->cancel = grease_pencil_weight_brush_stroke_cancel;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
paint_stroke_operator_properties(ot);
}
/** \} */
} // namespace blender::ed::sculpt_paint
/* -------------------------------------------------------------------- */
@ -336,6 +467,7 @@ void ED_operatortypes_grease_pencil_draw()
using namespace blender::ed::sculpt_paint;
WM_operatortype_append(GREASE_PENCIL_OT_brush_stroke);
WM_operatortype_append(GREASE_PENCIL_OT_sculpt_paint);
WM_operatortype_append(GREASE_PENCIL_OT_weight_brush_stroke);
}
/** \} */

@ -28,6 +28,11 @@ namespace greasepencil {
std::unique_ptr<GreasePencilStrokeOperation> new_paint_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_erase_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_tint_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_draw_operation(
const BrushStrokeMode &brush_mode);
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_blur_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_average_operation();
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_smear_operation();
} // namespace greasepencil

@ -0,0 +1,116 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "grease_pencil_weight_paint.hh"
namespace blender::ed::sculpt_paint::greasepencil {
class AverageWeightPaintOperation : public WeightPaintOperation {
/* Get the average weight of all points in the brush buffer. */
float get_average_weight_in_brush_buffer(const Span<DrawingWeightData> drawing_weights)
{
float average_sum = 0.0f;
float point_num = 0;
for (const DrawingWeightData &drawing_weight : drawing_weights) {
for (const BrushPoint &point : drawing_weight.points_in_brush) {
average_sum += drawing_weight.deform_weights[point.drawing_point_index];
point_num++;
}
}
if (point_num == 0) {
return 0.0f;
}
return math::clamp(average_sum / point_num, 0.0f, 1.0f);
}
public:
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override
{
using namespace blender::ed::greasepencil;
this->get_brush_settings(C, start_sample);
this->ensure_active_vertex_group_in_object();
this->get_locked_and_bone_deformed_vertex_groups();
/* Get editable drawings grouped per frame number. When multiframe editing is disabled, this
* is just one group for the current frame. When multiframe editing is enabled, the selected
* keyframes are grouped per frame number. This way we can use Average on multiple layers
* together instead of on every layer individually. */
const Scene *scene = CTX_data_scene(&C);
Array<Vector<MutableDrawingInfo>> drawings_per_frame =
retrieve_editable_drawings_grouped_per_frame(*scene, *this->grease_pencil);
this->drawing_weight_data = Array<Array<DrawingWeightData>>(drawings_per_frame.size());
/* Get weight data for all drawings in this frame group. */
for (const int frame_group : drawings_per_frame.index_range()) {
const Vector<MutableDrawingInfo> &drawings = drawings_per_frame[frame_group];
this->init_weight_data_for_drawings(C, drawings, frame_group);
}
}
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override
{
using namespace blender::ed::greasepencil;
this->get_mouse_input_sample(extension_sample);
/* Iterate over the drawings grouped per frame number. Collect all stroke points under the
* brush and average them. */
std::atomic<bool> changed = false;
threading::parallel_for_each(
this->drawing_weight_data.index_range(), [&](const int frame_group) {
Array<DrawingWeightData> &drawing_weights = this->drawing_weight_data[frame_group];
/* For all layers at this key frame, collect the stroke points under the brush in a
* buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const int point_index : drawing_weight.point_positions.index_range()) {
const float2 &co = drawing_weight.point_positions[point_index];
/* When the point is under the brush, add it to the brush point buffer. */
this->add_point_under_brush_to_brush_buffer(co, drawing_weight, point_index);
}
});
/* Get the average weight of the points in the brush buffer. */
const float average_weight = this->get_average_weight_in_brush_buffer(drawing_weights);
/* Apply the Average tool to all points in the brush buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const BrushPoint &point : drawing_weight.points_in_brush) {
this->apply_weight_to_point(point, average_weight, drawing_weight);
/* Normalize weights of bone-deformed vertex groups to 1.0f. */
if (this->auto_normalize) {
normalize_vertex_weights(drawing_weight.deform_verts[point.drawing_point_index],
drawing_weight.active_vertex_group,
drawing_weight.locked_vgroups,
drawing_weight.bone_deformed_vgroups);
}
}
if (!drawing_weight.points_in_brush.is_empty()) {
changed = true;
drawing_weight.points_in_brush.clear();
}
});
});
if (changed) {
DEG_id_tag_update(&this->grease_pencil->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
}
}
void on_stroke_done(const bContext & /*C*/) override {}
};
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_average_operation()
{
return std::make_unique<AverageWeightPaintOperation>();
}
} // namespace blender::ed::sculpt_paint::greasepencil

@ -0,0 +1,139 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "grease_pencil_weight_paint.hh"
namespace blender::ed::sculpt_paint::greasepencil {
class BlurWeightPaintOperation : public WeightPaintOperation {
/* Apply the Blur tool to a point under the brush. */
void apply_blur_tool(const BrushPoint &point,
DrawingWeightData &drawing_weight,
PointsTouchedByBrush &touched_points)
{
/* Find the nearest neighbours of the to-be-blurred point. The point itself is included. */
KDTreeNearest_2d nearest_points[BLUR_NEIGHBOUR_NUM];
const int point_num = BLI_kdtree_2d_find_nearest_n(
touched_points.kdtree,
drawing_weight.point_positions[point.drawing_point_index],
nearest_points,
BLUR_NEIGHBOUR_NUM);
if (point_num <= 1) {
return;
}
/* Calculate the blurred weight for the point (A). For this we use a weighted average of the
* point weights, based on the distance of the neighbour point to A. So points closer to A
* contribute more to the average than points farther away from A. */
float distance_sum = 0.0f;
for (const int i : IndexRange(point_num)) {
distance_sum += nearest_points[i].dist;
}
if (distance_sum == 0.0f) {
return;
}
float blur_weight_sum = 0.0f;
for (const int i : IndexRange(point_num)) {
blur_weight_sum += (1.0f - nearest_points[i].dist / distance_sum) *
touched_points.weights[nearest_points[i].index];
}
const float blur_weight = blur_weight_sum / (point_num - 1);
apply_weight_to_point(point, blur_weight, drawing_weight);
}
public:
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override
{
using namespace blender::ed::greasepencil;
this->get_brush_settings(C, start_sample);
this->ensure_active_vertex_group_in_object();
this->get_locked_and_bone_deformed_vertex_groups();
/* Get editable drawings grouped per frame number. When multiframe editing is disabled, this
* is just one group for the current frame. When multiframe editing is enabled, the selected
* keyframes are grouped per frame number. This way we can use Blur on multiple layers
* together instead of on every layer individually. */
const Scene *scene = CTX_data_scene(&C);
Array<Vector<MutableDrawingInfo>> drawings_per_frame =
retrieve_editable_drawings_grouped_per_frame(*scene, *this->grease_pencil);
this->drawing_weight_data = Array<Array<DrawingWeightData>>(drawings_per_frame.size());
/* Get weight data for all drawings in this frame group. */
for (const int frame_group : drawings_per_frame.index_range()) {
const Vector<MutableDrawingInfo> &drawings = drawings_per_frame[frame_group];
this->init_weight_data_for_drawings(C, drawings, frame_group);
}
}
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override
{
using namespace blender::ed::greasepencil;
this->get_mouse_input_sample(extension_sample, 1.3f);
/* Iterate over the drawings grouped per frame number. Collect all stroke points under the
* brush and blur them. */
std::atomic<bool> changed = false;
threading::parallel_for_each(
this->drawing_weight_data.index_range(), [&](const int frame_group) {
Array<DrawingWeightData> &drawing_weights = this->drawing_weight_data[frame_group];
/* For all layers at this key frame, collect the stroke points under the brush in a
* buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const int point_index : drawing_weight.point_positions.index_range()) {
const float2 &co = drawing_weight.point_positions[point_index];
/* When the point is under the brush, add it to the brush point buffer. */
this->add_point_under_brush_to_brush_buffer(co, drawing_weight, point_index);
}
});
/* Create a KDTree with all stroke points touched by the brush during the weight paint
* operation. */
PointsTouchedByBrush touched_points = this->create_affected_points_kdtree(
drawing_weights);
/* Apply the Blur tool to all points in the brush buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const BrushPoint &point : drawing_weight.points_in_brush) {
this->apply_blur_tool(point, drawing_weight, touched_points);
/* Normalize weights of bone-deformed vertex groups to 1.0f. */
if (this->auto_normalize) {
normalize_vertex_weights(drawing_weight.deform_verts[point.drawing_point_index],
drawing_weight.active_vertex_group,
drawing_weight.locked_vgroups,
drawing_weight.bone_deformed_vgroups);
}
}
if (!drawing_weight.points_in_brush.is_empty()) {
changed = true;
drawing_weight.points_in_brush.clear();
}
});
BLI_kdtree_2d_free(touched_points.kdtree);
});
if (changed) {
DEG_id_tag_update(&this->grease_pencil->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
}
}
void on_stroke_done(const bContext & /*C*/) override {}
};
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_blur_operation()
{
return std::make_unique<BlurWeightPaintOperation>();
}
} // namespace blender::ed::sculpt_paint::greasepencil

@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "grease_pencil_weight_paint.hh"
namespace blender::ed::sculpt_paint::greasepencil {
class DrawWeightPaintOperation : public WeightPaintOperation {
public:
DrawWeightPaintOperation(const BrushStrokeMode &brush_mode)
{
this->brush_mode = brush_mode;
}
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override
{
using namespace blender::ed::greasepencil;
this->get_brush_settings(C, start_sample);
this->ensure_active_vertex_group_in_object();
this->get_locked_and_bone_deformed_vertex_groups();
/* Get the add/subtract mode of the draw tool. */
this->invert_brush_weight = (this->brush->flag & BRUSH_DIR_IN) != 0;
if (this->brush_mode == BRUSH_STROKE_INVERT) {
this->invert_brush_weight = !this->invert_brush_weight;
}
/* Get editable drawings grouped per frame number. When multiframe editing is disabled, this
* is just one group for the current frame. When multiframe editing is enabled, the selected
* keyframes are grouped per frame number. */
const Scene *scene = CTX_data_scene(&C);
Array<Vector<MutableDrawingInfo>> drawings_per_frame =
retrieve_editable_drawings_grouped_per_frame(*scene, *this->grease_pencil);
this->drawing_weight_data = Array<Array<DrawingWeightData>>(drawings_per_frame.size());
/* Get weight data for all drawings in this frame group. */
for (const int frame_group : drawings_per_frame.index_range()) {
const Vector<MutableDrawingInfo> &drawings = drawings_per_frame[frame_group];
this->init_weight_data_for_drawings(C, drawings, frame_group);
}
}
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override
{
using namespace blender::ed::greasepencil;
this->get_mouse_input_sample(extension_sample);
/* Iterate over the drawings grouped per frame number. Collect all stroke points under the
* brush and draw weight on them. */
std::atomic<bool> changed = false;
threading::parallel_for_each(
this->drawing_weight_data.index_range(), [&](const int frame_group) {
Array<DrawingWeightData> &drawing_weights = this->drawing_weight_data[frame_group];
/* For all layers at this key frame, collect the stroke points under the brush in a
* buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const int point_index : drawing_weight.point_positions.index_range()) {
const float2 &co = drawing_weight.point_positions[point_index];
/* When the point is under the brush, add it to the brush point buffer. */
this->add_point_under_brush_to_brush_buffer(co, drawing_weight, point_index);
}
});
/* Apply the Draw tool to all points in the brush buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const BrushPoint &point : drawing_weight.points_in_brush) {
this->apply_weight_to_point(point, this->brush_weight, drawing_weight);
/* Normalize weights of bone-deformed vertex groups to 1.0f. */
if (this->auto_normalize) {
normalize_vertex_weights(drawing_weight.deform_verts[point.drawing_point_index],
drawing_weight.active_vertex_group,
drawing_weight.locked_vgroups,
drawing_weight.bone_deformed_vgroups);
}
}
if (!drawing_weight.points_in_brush.is_empty()) {
changed = true;
drawing_weight.points_in_brush.clear();
}
});
});
if (changed) {
DEG_id_tag_update(&this->grease_pencil->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
}
}
void on_stroke_done(const bContext & /*C*/) override {}
};
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_draw_operation(
const BrushStrokeMode &brush_mode)
{
return std::make_unique<DrawWeightPaintOperation>(brush_mode);
}
} // namespace blender::ed::sculpt_paint::greasepencil

@ -0,0 +1,315 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_brush.hh"
#include "BKE_colortools.hh"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_deform.hh"
#include "BKE_grease_pencil_vertex_groups.hh"
#include "BKE_modifier.hh"
#include "BKE_object_deform.h"
#include "BKE_scene.hh"
#include "DEG_depsgraph_query.hh"
#include "BLI_kdtree.h"
#include "DNA_meshdata_types.h"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "grease_pencil_intern.hh"
namespace blender::ed::sculpt_paint::greasepencil {
static constexpr float FIND_NEAREST_POINT_EPSILON = 1e-6f;
static constexpr int BLUR_NEIGHBOUR_NUM = 5;
static constexpr int SMEAR_NEIGHBOUR_NUM = 8;
class WeightPaintOperation : public GreasePencilStrokeOperation {
public:
struct BrushPoint {
float influence;
int drawing_point_index;
};
struct DrawingWeightData {
int active_vertex_group;
MutableSpan<MDeformVert> deform_verts;
VMutableArray<float> deform_weights;
float multi_frame_falloff;
Vector<bool> locked_vgroups;
Vector<bool> bone_deformed_vgroups;
Array<float2> point_positions;
/* Flag for all stroke points in a drawing: true when the point was touched by the brush during
* a #GreasePencilStrokeOperation. */
Array<bool> points_touched_by_brush;
int points_touched_by_brush_num;
/* Collected points under the brush in one #on_stroke_extended action. */
Vector<BrushPoint> points_in_brush;
};
struct PointsTouchedByBrush {
KDTree_2d *kdtree;
Array<float> weights;
};
Object *object;
GreasePencil *grease_pencil;
Brush *brush;
float initial_brush_radius;
float brush_radius;
float brush_radius_wide;
float initial_brush_strength;
float brush_strength;
float brush_weight;
float2 mouse_position;
float2 mouse_position_previous;
rctf brush_bbox;
/* Flag for Auto-normalize weights of bone deformed vertex groups. */
bool auto_normalize;
/* Brush mode: normal, invert or smooth. */
BrushStrokeMode brush_mode;
/* Add or subtract weight? */
bool invert_brush_weight;
/* Active vertex group in GP object. */
bDeformGroup *object_defgroup;
/* Weight paint data per editable drawing. Stored per frame group. */
Array<Array<DrawingWeightData>> drawing_weight_data;
/* Set of bone-deformed vertex groups (object level). */
Set<std::string> object_bone_deformed_defgroups;
/* Set of locked vertex groups (object level). */
Set<std::string> object_locked_defgroups;
~WeightPaintOperation() override {}
/* Apply a weight to a point under the brush. */
void apply_weight_to_point(const BrushPoint &point,
const float target_weight,
DrawingWeightData &drawing_weight)
{
/* Blend the current point weight with the target weight. */
const float old_weight = drawing_weight.deform_weights[point.drawing_point_index];
const float weight_delta = (this->invert_brush_weight ? (1.0f - target_weight) :
target_weight) -
old_weight;
drawing_weight.deform_weights.set(
point.drawing_point_index,
math::clamp(
old_weight + math::interpolate(0.0f, weight_delta, point.influence), 0.0f, 1.0f));
}
/* Get brush settings (radius, strength etc.) */
void get_brush_settings(const bContext &C, const InputSample &start_sample)
{
using namespace blender::ed::greasepencil;
const Scene *scene = CTX_data_scene(&C);
this->object = CTX_data_active_object(&C);
this->grease_pencil = static_cast<GreasePencil *>(this->object->data);
Paint *paint = BKE_paint_get_active_from_context(&C);
Brush *brush = BKE_paint_brush(paint);
this->brush = brush;
this->initial_brush_radius = BKE_brush_size_get(scene, brush);
this->initial_brush_strength = BKE_brush_alpha_get(scene, brush);
this->brush_weight = BKE_brush_weight_get(scene, brush);
this->mouse_position_previous = start_sample.mouse_position;
this->invert_brush_weight = false;
BKE_curvemapping_init(brush->curve);
/* Auto-normalize weights is only applied when the object is deformed by an armature. */
const ToolSettings *ts = CTX_data_tool_settings(&C);
this->auto_normalize = ts->auto_normalize &&
(BKE_modifiers_is_deformed_by_armature(this->object) != nullptr);
}
/* Get or create active vertex group in GP object. */
void ensure_active_vertex_group_in_object()
{
int object_defgroup_nr = BKE_object_defgroup_active_index_get(this->object) - 1;
if (object_defgroup_nr == -1) {
BKE_object_defgroup_add(this->object);
object_defgroup_nr = 0;
}
this->object_defgroup = static_cast<bDeformGroup *>(
BLI_findlink(BKE_object_defgroup_list(this->object), object_defgroup_nr));
}
/* Get locked and bone-deformed vertex groups in GP object. */
void get_locked_and_bone_deformed_vertex_groups()
{
const ListBase *defgroups = BKE_object_defgroup_list(this->object);
LISTBASE_FOREACH (bDeformGroup *, dg, defgroups) {
if ((dg->flag & DG_LOCK_WEIGHT) != 0) {
this->object_locked_defgroups.add(dg->name);
}
}
this->object_bone_deformed_defgroups = ed::greasepencil::get_bone_deformed_vertex_group_names(
*this->object);
}
/* For each drawing, retrieve pointers to the vertex weight data of the active vertex group,
* so that we can read and write to them later. And create buffers for points under the brush
* during one #on_stroke_extended action. */
void init_weight_data_for_drawings(const bContext &C,
const Span<ed::greasepencil::MutableDrawingInfo> &drawings,
const int frame_group)
{
const Depsgraph *depsgraph = CTX_data_depsgraph_pointer(&C);
const Object *ob_eval = DEG_get_evaluated_object(depsgraph, this->object);
const RegionView3D *rv3d = CTX_wm_region_view3d(&C);
const ARegion *region = CTX_wm_region(&C);
this->drawing_weight_data[frame_group].reinitialize(drawings.size());
threading::parallel_for(drawings.index_range(), 1, [&](const IndexRange range) {
for (const int drawing_index : range) {
const ed::greasepencil::MutableDrawingInfo &drawing_info = drawings[drawing_index];
bke::CurvesGeometry &curves = drawing_info.drawing.strokes_for_write();
/* Find or create the active vertex group in the drawing. */
DrawingWeightData &drawing_weight_data =
this->drawing_weight_data[frame_group][drawing_index];
drawing_weight_data.active_vertex_group = bke::greasepencil::ensure_vertex_group(
this->object_defgroup->name, curves.vertex_group_names);
drawing_weight_data.multi_frame_falloff = drawing_info.multi_frame_falloff;
drawing_weight_data.deform_verts = curves.deform_verts_for_write();
drawing_weight_data.deform_weights = bke::varray_for_mutable_deform_verts(
drawing_weight_data.deform_verts, drawing_weight_data.active_vertex_group);
/* Create boolean arrays indicating whether a vertex group is locked/bone deformed
* or not. */
if (this->auto_normalize) {
LISTBASE_FOREACH (bDeformGroup *, dg, &curves.vertex_group_names) {
drawing_weight_data.locked_vgroups.append(
this->object_locked_defgroups.contains(dg->name));
drawing_weight_data.bone_deformed_vgroups.append(
this->object_bone_deformed_defgroups.contains(dg->name));
}
}
/* Convert stroke points to screen space positions. */
const bke::greasepencil::Layer &layer =
*this->grease_pencil->layers()[drawing_info.layer_index];
const float4x4 layer_to_world = layer.to_world_space(*ob_eval);
const float4x4 projection = ED_view3d_ob_project_mat_get_from_obmat(rv3d, layer_to_world);
bke::crazyspace::GeometryDeformation deformation =
bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation(
ob_eval, *this->object, drawing_info.layer_index, drawing_info.frame_number);
drawing_weight_data.point_positions.reinitialize(deformation.positions.size());
threading::parallel_for(curves.points_range(), 1024, [&](const IndexRange point_range) {
for (const int point : point_range) {
drawing_weight_data.point_positions[point] = ED_view3d_project_float_v2_m4(
region, deformation.positions[point], projection);
}
});
/* Initialize the flag for stroke points being touched by the brush. */
drawing_weight_data.points_touched_by_brush_num = 0;
drawing_weight_data.points_touched_by_brush = Array<bool>(deformation.positions.size(),
false);
}
});
}
/* Get mouse position and pressure. */
void get_mouse_input_sample(const InputSample &input_sample,
const float brush_widen_factor = 1.0f)
{
this->mouse_position = input_sample.mouse_position;
this->brush_radius = this->initial_brush_radius;
if (BKE_brush_use_size_pressure(this->brush)) {
this->brush_radius *= input_sample.pressure;
}
this->brush_strength = this->initial_brush_strength;
if (BKE_brush_use_alpha_pressure(this->brush)) {
this->brush_strength *= input_sample.pressure;
}
this->brush_radius_wide = this->brush_radius * brush_widen_factor;
BLI_rctf_init(&this->brush_bbox,
this->mouse_position.x - this->brush_radius_wide,
this->mouse_position.x + this->brush_radius_wide,
this->mouse_position.y - this->brush_radius_wide,
this->mouse_position.y + this->brush_radius_wide);
}
/* Add a point to the brush buffer when it is within the brush radius. */
void add_point_under_brush_to_brush_buffer(const float2 point_position,
DrawingWeightData &drawing_weight,
const int point_index)
{
if (!BLI_rctf_isect_pt_v(&this->brush_bbox, point_position)) {
return;
}
const float dist_point_to_brush_center = math::distance(point_position, this->mouse_position);
if (dist_point_to_brush_center > this->brush_radius_wide) {
return;
}
/* Point is touched by the (wide) brush, set flag for that. */
if (!drawing_weight.points_touched_by_brush[point_index]) {
drawing_weight.points_touched_by_brush_num++;
}
drawing_weight.points_touched_by_brush[point_index] = true;
if (dist_point_to_brush_center > this->brush_radius) {
return;
}
/* When the point is under the brush, add it to the brush buffer. */
const float influence = drawing_weight.multi_frame_falloff * this->brush_strength *
BKE_brush_curve_strength(
this->brush, dist_point_to_brush_center, this->brush_radius);
if (influence != 0.0f) {
drawing_weight.points_in_brush.append({influence, point_index});
}
}
/* Create KDTree for all stroke points touched by the brush during a weight paint operation. */
PointsTouchedByBrush create_affected_points_kdtree(const Span<DrawingWeightData> drawing_weights)
{
/* Get number of stroke points touched by the brush. */
int point_num = 0;
for (const DrawingWeightData &drawing_weight : drawing_weights) {
point_num += drawing_weight.points_touched_by_brush_num;
}
/* Create KDTree of stroke points touched by the brush. */
KDTree_2d *touched_points = BLI_kdtree_2d_new(point_num);
Array<float> touched_points_weights(point_num);
int kdtree_index = 0;
for (const DrawingWeightData &drawing_weight : drawing_weights) {
for (const int point_index : drawing_weight.point_positions.index_range()) {
if (drawing_weight.points_touched_by_brush[point_index]) {
BLI_kdtree_2d_insert(
touched_points, kdtree_index, drawing_weight.point_positions[point_index]);
touched_points_weights[kdtree_index] = drawing_weight.deform_weights[point_index];
kdtree_index++;
}
}
}
BLI_kdtree_2d_balance(touched_points);
return {touched_points, touched_points_weights};
}
};
} // namespace blender::ed::sculpt_paint::greasepencil

@ -0,0 +1,197 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "grease_pencil_weight_paint.hh"
namespace blender::ed::sculpt_paint::greasepencil {
class SmearWeightPaintOperation : public WeightPaintOperation {
/* Brush direction (angle) during a stroke movement. */
float2 brush_direction;
bool brush_direction_is_set;
/** Get the direction of the brush while the mouse is moving. The direction is given as a
* normalized XY vector. */
bool get_brush_direction()
{
this->brush_direction = this->mouse_position - this->mouse_position_previous;
/* Skip tiny changes in direction, we want the bigger movements only. */
if (math::length_squared(this->brush_direction) < 9.0f) {
return this->brush_direction_is_set;
}
this->brush_direction = math::normalize(this->brush_direction);
this->brush_direction_is_set = true;
this->mouse_position_previous = this->mouse_position;
return true;
}
/** Apply the Smear tool to a point under the brush. */
void apply_smear_tool(const BrushPoint &point,
DrawingWeightData &drawing_weight,
PointsTouchedByBrush &touched_points)
{
/* Find the nearest neighbours of the to-be-smeared point. */
KDTreeNearest_2d nearest_points[SMEAR_NEIGHBOUR_NUM];
const int point_num = BLI_kdtree_2d_find_nearest_n(
touched_points.kdtree,
drawing_weight.point_positions[point.drawing_point_index],
nearest_points,
SMEAR_NEIGHBOUR_NUM);
/* For smearing a weight to point A, we look for a point B in the trail of the mouse
* movement, matching the last known brush angle best and with the shortest distance to A. */
float point_dot_product[SMEAR_NEIGHBOUR_NUM];
float min_distance = FLT_MAX, max_distance = -FLT_MAX;
int smear_point_num = 0;
for (const int i : IndexRange(point_num)) {
/* Skip the point we are about to smear. */
if (nearest_points[i].dist < FIND_NEAREST_POINT_EPSILON) {
continue;
}
const float2 direction_nearest_to_point = math::normalize(
drawing_weight.point_positions[point.drawing_point_index] -
float2(nearest_points[i].co));
/* Match point direction with brush direction. */
point_dot_product[i] = math::dot(direction_nearest_to_point, this->brush_direction);
if (point_dot_product[i] <= 0.0f) {
continue;
}
smear_point_num++;
min_distance = math::min(min_distance, nearest_points[i].dist);
max_distance = math::max(max_distance, nearest_points[i].dist);
}
if (smear_point_num == 0) {
return;
}
/* Find best match in angle and distance. */
int best_match = -1;
float max_score = 0.0f;
const float distance_normalizer = (min_distance == max_distance) ?
1.0f :
(0.95f / (max_distance - min_distance));
for (const int i : IndexRange(point_num)) {
if (point_dot_product[i] <= 0.0f) {
continue;
}
const float score = point_dot_product[i] *
(1.0f - (nearest_points[i].dist - min_distance) * distance_normalizer);
if (score > max_score) {
max_score = score;
best_match = i;
}
}
if (best_match == -1) {
return;
}
const float smear_weight = touched_points.weights[nearest_points[best_match].index];
apply_weight_to_point(point, smear_weight, drawing_weight);
}
public:
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override
{
using namespace blender::ed::greasepencil;
this->get_brush_settings(C, start_sample);
this->ensure_active_vertex_group_in_object();
this->get_locked_and_bone_deformed_vertex_groups();
/* Get editable drawings grouped per frame number. When multiframe editing is disabled, this
* is just one group for the current frame. When multiframe editing is enabled, the selected
* keyframes are grouped per frame number. This way we can use Smear on multiple layers
* together instead of on every layer individually. */
const Scene *scene = CTX_data_scene(&C);
Array<Vector<MutableDrawingInfo>> drawings_per_frame =
retrieve_editable_drawings_grouped_per_frame(*scene, *this->grease_pencil);
this->drawing_weight_data = Array<Array<DrawingWeightData>>(drawings_per_frame.size());
/* Get weight data for all drawings in this frame group. */
for (const int frame_group : drawings_per_frame.index_range()) {
const Vector<MutableDrawingInfo> &drawings = drawings_per_frame[frame_group];
this->init_weight_data_for_drawings(C, drawings, frame_group);
}
}
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override
{
using namespace blender::ed::greasepencil;
this->get_mouse_input_sample(extension_sample);
/* For the Smear tool, we use the direction of the brush during the stroke movement. The
* direction is derived from the current and previous mouse position. */
if (!this->get_brush_direction()) {
/* Abort when no direction is established yet. */
return;
}
/* Iterate over the drawings grouped per frame number. Collect all stroke points under the
* brush and smear them. */
std::atomic<bool> changed = false;
threading::parallel_for_each(
this->drawing_weight_data.index_range(), [&](const int frame_group) {
Array<DrawingWeightData> &drawing_weights = this->drawing_weight_data[frame_group];
std::atomic<bool> balance_kdtree = false;
/* For all layers at this key frame, collect the stroke points under the brush in a
* buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const int point_index : drawing_weight.point_positions.index_range()) {
const float2 &co = drawing_weight.point_positions[point_index];
/* When the point is under the brush, add it to the brush point buffer. */
this->add_point_under_brush_to_brush_buffer(co, drawing_weight, point_index);
}
});
/* Create a KDTree with all stroke points touched by the brush during the weight paint
* operation. */
PointsTouchedByBrush touched_points = this->create_affected_points_kdtree(
drawing_weights);
/* Apply the Smear tool to all points in the brush buffer. */
threading::parallel_for_each(drawing_weights, [&](DrawingWeightData &drawing_weight) {
for (const BrushPoint &point : drawing_weight.points_in_brush) {
this->apply_smear_tool(point, drawing_weight, touched_points);
/* Normalize weights of bone-deformed vertex groups to 1.0f. */
if (this->auto_normalize) {
normalize_vertex_weights(drawing_weight.deform_verts[point.drawing_point_index],
drawing_weight.active_vertex_group,
drawing_weight.locked_vgroups,
drawing_weight.bone_deformed_vgroups);
}
}
if (!drawing_weight.points_in_brush.is_empty()) {
changed = true;
drawing_weight.points_in_brush.clear();
}
});
BLI_kdtree_2d_free(touched_points.kdtree);
});
if (changed) {
DEG_id_tag_update(&this->grease_pencil->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(&C, NC_GEOM | ND_DATA, &grease_pencil);
}
}
void on_stroke_done(const bContext & /*C*/) override {}
};
std::unique_ptr<GreasePencilStrokeOperation> new_weight_paint_smear_operation()
{
return std::make_unique<SmearWeightPaintOperation>();
}
} // namespace blender::ed::sculpt_paint::greasepencil

@ -1516,35 +1516,31 @@ static void grease_pencil_brush_cursor_draw(PaintCursorContext *pcontext)
return;
}
Paint *paint = pcontext->paint;
Brush *brush = pcontext->brush;
if ((brush == nullptr) || (brush->gpencil_settings == nullptr)) {
return;
}
if ((paint->flags & PAINT_SHOW_BRUSH) == 0) {
return;
}
/* default radius and color */
float color[3] = {1.0f, 1.0f, 1.0f};
float darkcolor[3];
float radius = 2.0f;
float radius = BKE_brush_size_get(pcontext->scene, brush);
const int x = pcontext->x;
const int y = pcontext->y;
/* for paint use paint brush size and color */
if (pcontext->mode == PaintMode::GPencil) {
Paint *paint = pcontext->paint;
Brush *brush = pcontext->brush;
if ((brush == nullptr) || (brush->gpencil_settings == nullptr)) {
return;
}
if ((paint->flags & PAINT_SHOW_BRUSH) == 0) {
return;
}
/* Eraser has a special shape and use a different shader program. */
if (brush->gpencil_tool == GPAINT_TOOL_ERASE) {
grease_pencil_eraser_draw(pcontext);
return;
}
/* Note: For now, there is only as screen space sized cursor. */
radius = BKE_brush_size_get(pcontext->scene, brush);
/* Get current drawing material. */
Material *ma = BKE_grease_pencil_object_material_from_brush_get(object, brush);
if (ma) {
@ -1570,6 +1566,9 @@ static void grease_pencil_brush_cursor_draw(PaintCursorContext *pcontext)
}
}
}
else if (pcontext->mode == PaintMode::WeightGPencil) {
copy_v3_v3(color, brush->add_col);
}
GPU_line_width(1.0f);
/* Inner Ring: Color from UI panel */
@ -1577,6 +1576,7 @@ static void grease_pencil_brush_cursor_draw(PaintCursorContext *pcontext)
imm_draw_circle_wire_2d(pcontext->pos, x, y, radius, 32);
/* Outer Ring: Dark color for contrast on light backgrounds (e.g. gray on white) */
float darkcolor[3];
mul_v3_v3fl(darkcolor, color, 0.40f);
immUniformColor4f(darkcolor[0], darkcolor[1], darkcolor[2], 0.8f);
imm_draw_circle_wire_2d(pcontext->pos, x, y, radius + 1, 32);
@ -1604,7 +1604,8 @@ static void grease_pencil_brush_cursor_draw(PaintCursorContext *pcontext)
static void paint_draw_2D_view_brush_cursor(PaintCursorContext *pcontext)
{
switch (pcontext->mode) {
case PaintMode::GPencil: {
case PaintMode::GPencil:
case PaintMode::WeightGPencil: {
grease_pencil_brush_cursor_draw(pcontext);
break;
}

@ -414,6 +414,10 @@ static void view3d_main_region_init(wmWindowManager *wm, ARegion *region)
wm->defaultconf, "Grease Pencil Sculpt Mode", SPACE_EMPTY, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler(&region->handlers, keymap);
keymap = WM_keymap_ensure(
wm->defaultconf, "Grease Pencil Weight Paint", SPACE_EMPTY, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler(&region->handlers, keymap);
/* Edit-font key-map swallows almost all (because of text input). */
keymap = WM_keymap_ensure(wm->defaultconf, "Font", SPACE_EMPTY, RGN_TYPE_WINDOW);
WM_event_add_keymap_handler(&region->handlers, keymap);
@ -1716,7 +1720,7 @@ void ED_view3d_buttons_region_layout_ex(const bContext *C,
ARRAY_SET_ITEMS(contexts, ".paint_common", ".grease_pencil_sculpt");
break;
case CTX_MODE_WEIGHT_GREASE_PENCIL:
ARRAY_SET_ITEMS(contexts, ".grease_pencil_weight");
ARRAY_SET_ITEMS(contexts, ".greasepencil_weight");
break;
case CTX_MODE_EDIT_POINT_CLOUD:
ARRAY_SET_ITEMS(contexts, ".point_cloud_edit");