1683 lines
52 KiB
C++
1683 lines
52 KiB
C++
/* SPDX-FileCopyrightText: 2015 Blender Foundation
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup edgpencil
|
|
* Brush based operators for editing Grease Pencil strokes.
|
|
*/
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "BLI_blenlib.h"
|
|
#include "BLI_ghash.h"
|
|
#include "BLI_kdtree.h"
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_math.h"
|
|
|
|
#include "BLT_translation.h"
|
|
|
|
#include "DNA_armature_types.h"
|
|
#include "DNA_brush_types.h"
|
|
#include "DNA_gpencil_legacy_types.h"
|
|
#include "DNA_gpencil_modifier_types.h"
|
|
|
|
#include "BKE_action.h"
|
|
#include "BKE_brush.hh"
|
|
#include "BKE_colortools.h"
|
|
#include "BKE_context.h"
|
|
#include "BKE_deform.h"
|
|
#include "BKE_gpencil_legacy.h"
|
|
#include "BKE_main.h"
|
|
#include "BKE_modifier.h"
|
|
#include "BKE_object_deform.h"
|
|
#include "BKE_report.h"
|
|
#include "DNA_meshdata_types.h"
|
|
|
|
#include "WM_api.h"
|
|
#include "WM_types.h"
|
|
|
|
#include "RNA_access.h"
|
|
#include "RNA_define.h"
|
|
#include "RNA_prototypes.h"
|
|
|
|
#include "UI_view2d.h"
|
|
|
|
#include "ED_gpencil_legacy.h"
|
|
#include "ED_screen.h"
|
|
#include "ED_view3d.h"
|
|
|
|
#include "DEG_depsgraph.h"
|
|
#include "DEG_depsgraph_query.h"
|
|
|
|
#include "gpencil_intern.h"
|
|
|
|
/* ************************************************ */
|
|
/* General Brush Editing Context */
|
|
#define GP_SELECT_BUFFER_CHUNK 256
|
|
#define GP_FIND_NEAREST_BUFFER_CHUNK 1024
|
|
#define GP_FIND_NEAREST_EPSILON 1e-6f
|
|
#define GP_STROKE_HASH_BITSHIFT 16
|
|
|
|
/* Grid of Colors for Smear. */
|
|
struct tGP_Grid {
|
|
/** Lower right corner of rectangle of grid cell. */
|
|
float bottom[2];
|
|
/** Upper left corner of rectangle of grid cell. */
|
|
float top[2];
|
|
/** Average Color */
|
|
float color[4];
|
|
/** Total points included. */
|
|
int totcol;
|
|
};
|
|
|
|
/* List of points affected by brush. */
|
|
struct tGP_Selected {
|
|
/** Referenced stroke. */
|
|
bGPDstroke *gps;
|
|
/** Point index in points array. */
|
|
int pt_index;
|
|
/** Position. */
|
|
int pc[2];
|
|
/** Color. */
|
|
float color[4];
|
|
/** Weight. */
|
|
float weight;
|
|
};
|
|
|
|
/* Context for brush operators */
|
|
struct tGP_BrushWeightpaintData {
|
|
Main *bmain;
|
|
Scene *scene;
|
|
Object *object;
|
|
|
|
ARegion *region;
|
|
|
|
/* Current GPencil datablock */
|
|
bGPdata *gpd;
|
|
|
|
Brush *brush;
|
|
|
|
/* Space Conversion Data */
|
|
GP_SpaceConversion gsc;
|
|
|
|
/* Is the brush currently painting? */
|
|
bool is_painting;
|
|
|
|
/* Start of new paint */
|
|
bool first;
|
|
|
|
/* Is multi-frame editing enabled, and are we using falloff for that? */
|
|
bool is_multiframe;
|
|
bool use_multiframe_falloff;
|
|
|
|
/* Draw tool: add or subtract? */
|
|
bool subtract;
|
|
|
|
/* Auto-normalize weights of bone-deformed vertices? */
|
|
bool auto_normalize;
|
|
|
|
/* Active vertex group */
|
|
int vrgroup;
|
|
|
|
/* Brush Runtime Data: */
|
|
/* - position and pressure
|
|
* - the *_prev variants are the previous values
|
|
*/
|
|
float mouse[2], mouse_prev[2];
|
|
float pressure;
|
|
|
|
/* - Brush direction. */
|
|
float brush_dir[2];
|
|
bool brush_dir_is_set;
|
|
|
|
/* - Multi-frame falloff factor. */
|
|
float mf_falloff;
|
|
|
|
/* Brush geometry (bounding box). */
|
|
rcti brush_rect;
|
|
|
|
/* Temp data to save selected points */
|
|
/** Stroke buffer. */
|
|
tGP_Selected *pbuffer;
|
|
/** Number of elements currently used in cache. */
|
|
int pbuffer_used;
|
|
/** Number of total elements available in cache. */
|
|
int pbuffer_size;
|
|
/** Average weight of elements in cache (used for average tool). */
|
|
float pbuffer_avg_weight;
|
|
|
|
/* Temp data for find-nearest-points, used by blur and smear tool. */
|
|
bool use_find_nearest;
|
|
/** Buffer of stroke points during one mouse swipe. */
|
|
tGP_Selected *fn_pbuffer;
|
|
/** Hash table of added points (to avoid duplicate entries). */
|
|
GHash *fn_added;
|
|
/** KDtree for finding nearest points. */
|
|
KDTree_2d *fn_kdtree;
|
|
/** Number of points used in find-nearest set. */
|
|
uint fn_used;
|
|
/** Number of points available in find-nearest set. */
|
|
uint fn_size;
|
|
/** Flag for balancing kdtree. */
|
|
bool fn_do_balance;
|
|
|
|
/* Temp data for auto-normalize weights used by deforming bones. */
|
|
/** Boolean array of locked vertex groups. */
|
|
bool *vgroup_locked;
|
|
/** Boolean array of vertex groups deformed by bones. */
|
|
bool *vgroup_bone_deformed;
|
|
/** Number of vertex groups in object. */
|
|
int vgroup_tot;
|
|
};
|
|
|
|
/* Ensure the buffer to hold temp selected point size is enough to save all points selected. */
|
|
static void gpencil_select_buffer_ensure(tGP_BrushWeightpaintData *gso, const bool clear)
|
|
{
|
|
/* By default a buffer is created with one block with a predefined number of free slots,
|
|
* if the size is not enough, the cache is reallocated adding a new block of free slots.
|
|
* This is done in order to keep cache small and improve speed. */
|
|
if ((gso->pbuffer_used + 1) > gso->pbuffer_size) {
|
|
if ((gso->pbuffer_size == 0) || (gso->pbuffer == nullptr)) {
|
|
gso->pbuffer = static_cast<tGP_Selected *>(
|
|
MEM_callocN(sizeof(tGP_Selected) * GP_SELECT_BUFFER_CHUNK, __func__));
|
|
gso->pbuffer_size = GP_SELECT_BUFFER_CHUNK;
|
|
}
|
|
else {
|
|
gso->pbuffer_size += GP_SELECT_BUFFER_CHUNK;
|
|
gso->pbuffer = static_cast<tGP_Selected *>(
|
|
MEM_recallocN(gso->pbuffer, sizeof(tGP_Selected) * gso->pbuffer_size));
|
|
}
|
|
}
|
|
|
|
/* Clear old data. */
|
|
if (clear) {
|
|
gso->pbuffer_used = 0;
|
|
if (gso->pbuffer != nullptr) {
|
|
memset(gso->pbuffer, 0, sizeof(tGP_Selected) * gso->pbuffer_size);
|
|
}
|
|
}
|
|
|
|
/* Create or enlarge buffer for find-nearest-points. */
|
|
if (gso->use_find_nearest && ((gso->fn_used + 1) > gso->fn_size)) {
|
|
gso->fn_size += GP_FIND_NEAREST_BUFFER_CHUNK;
|
|
|
|
/* Stroke point buffer. */
|
|
if (gso->fn_pbuffer == nullptr) {
|
|
gso->fn_pbuffer = static_cast<tGP_Selected *>(
|
|
MEM_callocN(sizeof(tGP_Selected) * gso->fn_size, __func__));
|
|
}
|
|
else {
|
|
gso->fn_pbuffer = static_cast<tGP_Selected *>(
|
|
MEM_recallocN(gso->fn_pbuffer, sizeof(tGP_Selected) * gso->fn_size));
|
|
}
|
|
|
|
/* Stroke point hash table (for duplicate checking.) */
|
|
if (gso->fn_added == nullptr) {
|
|
gso->fn_added = BLI_ghash_int_new("GP weight paint find nearest");
|
|
}
|
|
|
|
/* KDtree of stroke points. */
|
|
bool do_tree_rebuild = false;
|
|
if (gso->fn_kdtree != nullptr) {
|
|
BLI_kdtree_2d_free(gso->fn_kdtree);
|
|
do_tree_rebuild = true;
|
|
}
|
|
gso->fn_kdtree = BLI_kdtree_2d_new(gso->fn_size);
|
|
|
|
if (do_tree_rebuild) {
|
|
for (int i = 0; i < gso->fn_used; i++) {
|
|
float pc_f[2];
|
|
copy_v2fl_v2i(pc_f, gso->fn_pbuffer[i].pc);
|
|
BLI_kdtree_2d_insert(gso->fn_kdtree, i, pc_f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Ensure a vertex group for the weight paint brush. */
|
|
static void gpencil_vertex_group_ensure(tGP_BrushWeightpaintData *gso)
|
|
{
|
|
if (gso->vrgroup >= 0) {
|
|
return;
|
|
}
|
|
if (gso->object == nullptr) {
|
|
return;
|
|
}
|
|
|
|
DEG_relations_tag_update(gso->bmain);
|
|
gso->vrgroup = 0;
|
|
|
|
Object *ob_armature = BKE_modifiers_is_deformed_by_armature(gso->object);
|
|
if (ob_armature == nullptr) {
|
|
BKE_object_defgroup_add(gso->object);
|
|
return;
|
|
}
|
|
Bone *actbone = ((bArmature *)ob_armature->data)->act_bone;
|
|
if (actbone == nullptr) {
|
|
return;
|
|
}
|
|
bPoseChannel *pchan = BKE_pose_channel_find_name(ob_armature->pose, actbone->name);
|
|
if (pchan == nullptr) {
|
|
return;
|
|
}
|
|
bDeformGroup *dg = BKE_object_defgroup_find_name(gso->object, pchan->name);
|
|
if (dg == nullptr) {
|
|
BKE_object_defgroup_add_name(gso->object, pchan->name);
|
|
}
|
|
}
|
|
|
|
static void gpencil_select_buffer_avg_weight_set(tGP_BrushWeightpaintData *gso)
|
|
{
|
|
if (gso->pbuffer_used == 0) {
|
|
gso->pbuffer_avg_weight = 0.0f;
|
|
return;
|
|
}
|
|
float sum = 0;
|
|
for (int i = 0; i < gso->pbuffer_used; i++) {
|
|
tGP_Selected *selected = &gso->pbuffer[i];
|
|
sum += selected->weight;
|
|
}
|
|
gso->pbuffer_avg_weight = sum / gso->pbuffer_used;
|
|
CLAMP(gso->pbuffer_avg_weight, 0.0f, 1.0f);
|
|
}
|
|
|
|
/* Get boolean array of vertex groups deformed by GP armature modifiers.
|
|
* GP equivalent of `BKE_object_defgroup_validmap_get`.
|
|
*/
|
|
static bool *gpencil_vgroup_bone_deformed_map_get(Object *ob, const int defbase_tot)
|
|
{
|
|
bDeformGroup *dg;
|
|
bool *vgroup_bone_deformed;
|
|
GHash *gh;
|
|
int i;
|
|
const ListBase *defbase = BKE_object_defgroup_list(ob);
|
|
|
|
if (BLI_listbase_is_empty(defbase)) {
|
|
return nullptr;
|
|
}
|
|
|
|
/* Add all vertex group names to a hash table. */
|
|
gh = BLI_ghash_str_new_ex(__func__, defbase_tot);
|
|
LISTBASE_FOREACH (bDeformGroup *, dg, defbase) {
|
|
BLI_ghash_insert(gh, dg->name, nullptr);
|
|
}
|
|
BLI_assert(BLI_ghash_len(gh) == defbase_tot);
|
|
|
|
/* Now loop through the armature modifiers and identify deform bones. */
|
|
LISTBASE_FOREACH (GpencilModifierData *, md, &ob->greasepencil_modifiers) {
|
|
if (md->type != eGpencilModifierType_Armature) {
|
|
continue;
|
|
}
|
|
|
|
ArmatureGpencilModifierData *amd = (ArmatureGpencilModifierData *)md;
|
|
|
|
if (amd->object && amd->object->pose) {
|
|
bPose *pose = amd->object->pose;
|
|
|
|
LISTBASE_FOREACH (bPoseChannel *, chan, &pose->chanbase) {
|
|
void **val_p;
|
|
if (chan->bone->flag & BONE_NO_DEFORM) {
|
|
continue;
|
|
}
|
|
|
|
val_p = BLI_ghash_lookup_p(gh, chan->name);
|
|
if (val_p) {
|
|
*val_p = POINTER_FROM_INT(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Mark vertex groups with reference in the bone hash table. */
|
|
vgroup_bone_deformed = static_cast<bool *>(
|
|
MEM_mallocN(sizeof(*vgroup_bone_deformed) * defbase_tot, __func__));
|
|
for (dg = static_cast<bDeformGroup *>(defbase->first), i = 0; dg; dg = dg->next, i++) {
|
|
vgroup_bone_deformed[i] = (BLI_ghash_lookup(gh, dg->name) != nullptr);
|
|
}
|
|
|
|
BLI_assert(i == BLI_ghash_len(gh));
|
|
|
|
BLI_ghash_free(gh, nullptr, nullptr);
|
|
|
|
return vgroup_bone_deformed;
|
|
}
|
|
|
|
/* ************************************************ */
|
|
/* Auto-normalize Operations
|
|
* This section defines the functions for auto-normalizing the sum of weights to 1.0
|
|
* for points in vertex groups deformed by bones.
|
|
* The logic is copied from `editors/sculpt_paint/paint_vertex.cc`. */
|
|
|
|
static bool do_weight_paint_normalize(MDeformVert *dvert,
|
|
const int defbase_tot,
|
|
const bool *vgroup_bone_deformed,
|
|
const bool *vgroup_locked,
|
|
const bool lock_active_vgroup,
|
|
const int active_vgroup)
|
|
{
|
|
float sum = 0.0f, fac;
|
|
float sum_unlock = 0.0f;
|
|
float sum_lock = 0.0f;
|
|
uint lock_tot = 0, unlock_tot = 0;
|
|
MDeformWeight *dw;
|
|
|
|
if (dvert->totweight <= 1) {
|
|
return true;
|
|
}
|
|
|
|
dw = dvert->dw;
|
|
for (int i = dvert->totweight; i != 0; i--, dw++) {
|
|
/* Auto-normalize is only applied on bone-deformed vertex groups that have weight already. */
|
|
if (dw->def_nr < defbase_tot && vgroup_bone_deformed[dw->def_nr] && dw->weight > FLT_EPSILON) {
|
|
sum += dw->weight;
|
|
|
|
if (vgroup_locked[dw->def_nr] || (lock_active_vgroup && active_vgroup == dw->def_nr)) {
|
|
lock_tot++;
|
|
sum_lock += dw->weight;
|
|
}
|
|
else {
|
|
unlock_tot++;
|
|
sum_unlock += dw->weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sum == 1.0f) {
|
|
return true;
|
|
}
|
|
|
|
if (unlock_tot == 0) {
|
|
/* There are no unlocked vertex groups to normalize. We don't need
|
|
* a second pass when there is only one locked group (the active group). */
|
|
return (lock_tot == 1);
|
|
}
|
|
|
|
if (sum_lock >= 1.0f - VERTEX_WEIGHT_LOCK_EPSILON) {
|
|
/* Locked groups make it impossible to fully normalize,
|
|
* zero out what we can and return false. */
|
|
dw = dvert->dw;
|
|
for (int i = dvert->totweight; i != 0; i--, dw++) {
|
|
if (dw->def_nr < defbase_tot && vgroup_bone_deformed[dw->def_nr]) {
|
|
if ((vgroup_locked[dw->def_nr] == false) &&
|
|
!(lock_active_vgroup && active_vgroup == dw->def_nr)) {
|
|
dw->weight = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (sum_lock == 1.0f);
|
|
}
|
|
if (sum_unlock != 0.0f) {
|
|
fac = (1.0f - sum_lock) / sum_unlock;
|
|
|
|
dw = dvert->dw;
|
|
for (int i = dvert->totweight; i != 0; i--, dw++) {
|
|
if (dw->def_nr < defbase_tot && vgroup_bone_deformed[dw->def_nr] && dw->weight > FLT_EPSILON)
|
|
{
|
|
if ((vgroup_locked[dw->def_nr] == false) &&
|
|
!(lock_active_vgroup && active_vgroup == dw->def_nr)) {
|
|
dw->weight *= fac;
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
fac = (1.0f - sum_lock) / unlock_tot;
|
|
CLAMP(fac, 0.0f, 1.0f);
|
|
|
|
dw = dvert->dw;
|
|
for (int i = dvert->totweight; i != 0; i--, dw++) {
|
|
if (dw->def_nr < defbase_tot && vgroup_bone_deformed[dw->def_nr] && dw->weight > FLT_EPSILON)
|
|
{
|
|
if ((vgroup_locked[dw->def_nr] == false) &&
|
|
!(lock_active_vgroup && active_vgroup == dw->def_nr)) {
|
|
dw->weight = fac;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* A version of #do_weight_paint_normalize that only changes unlocked weights
|
|
* and does a second pass without the active vertex group locked when the first pass fails.
|
|
*/
|
|
static void do_weight_paint_normalize_try(MDeformVert *dvert, tGP_BrushWeightpaintData *gso)
|
|
{
|
|
/* First pass with both active and explicitly locked vertex groups restricted from change. */
|
|
bool succes = do_weight_paint_normalize(
|
|
dvert, gso->vgroup_tot, gso->vgroup_bone_deformed, gso->vgroup_locked, true, gso->vrgroup);
|
|
|
|
if (!succes) {
|
|
/* Second pass with active vertex group unlocked. */
|
|
do_weight_paint_normalize(
|
|
dvert, gso->vgroup_tot, gso->vgroup_bone_deformed, gso->vgroup_locked, false, -1);
|
|
}
|
|
}
|
|
|
|
/* Brush Operations ------------------------------- */
|
|
|
|
/* Compute strength of effect. */
|
|
static float brush_influence_calc(tGP_BrushWeightpaintData *gso, const int radius, const int co[2])
|
|
{
|
|
Brush *brush = gso->brush;
|
|
|
|
/* basic strength factor from brush settings */
|
|
float influence = brush->alpha;
|
|
|
|
/* use pressure? */
|
|
if (brush->gpencil_settings->flag & GP_BRUSH_USE_PRESSURE) {
|
|
influence *= gso->pressure;
|
|
}
|
|
|
|
/* distance fading */
|
|
int mouse_i[2];
|
|
round_v2i_v2fl(mouse_i, gso->mouse);
|
|
float distance = float(len_v2v2_int(mouse_i, co));
|
|
|
|
/* Apply Brush curve. */
|
|
float brush_falloff = BKE_brush_curve_strength(brush, distance, float(radius));
|
|
influence *= brush_falloff;
|
|
|
|
/* apply multi-frame falloff */
|
|
influence *= gso->mf_falloff;
|
|
|
|
/* return influence */
|
|
return influence;
|
|
}
|
|
|
|
/* Compute effect vector for directional brushes. */
|
|
static void brush_calc_brush_dir_2d(tGP_BrushWeightpaintData *gso)
|
|
{
|
|
sub_v2_v2v2(gso->brush_dir, gso->mouse, gso->mouse_prev);
|
|
|
|
/* Skip tiny changes in direction, we want the bigger movements only. */
|
|
if (len_squared_v2(gso->brush_dir) < 9.0f) {
|
|
return;
|
|
}
|
|
|
|
normalize_v2(gso->brush_dir);
|
|
|
|
gso->brush_dir_is_set = true;
|
|
copy_v2_v2(gso->mouse_prev, gso->mouse);
|
|
}
|
|
|
|
/* ************************************************ */
|
|
/* Brush Callbacks
|
|
* This section defines the callbacks used by each brush to perform their magic.
|
|
* These are called on each point within the brush's radius. */
|
|
|
|
/* Draw Brush */
|
|
static bool brush_draw_apply(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
int pt_index,
|
|
const int radius,
|
|
const int co[2])
|
|
{
|
|
MDeformVert *dvert = gps->dvert + pt_index;
|
|
|
|
/* Compute strength of effect. */
|
|
float inf = brush_influence_calc(gso, radius, co);
|
|
|
|
/* Get current weight. */
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, gso->vrgroup);
|
|
if (dw == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
/* Apply brush weight. */
|
|
float bweight = (gso->subtract) ? -gso->brush->weight : gso->brush->weight;
|
|
dw->weight = interpf(bweight, dw->weight, inf);
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
|
|
/* Perform auto-normalize. */
|
|
if (gso->auto_normalize) {
|
|
do_weight_paint_normalize_try(dvert, gso);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Average Brush */
|
|
static bool brush_average_apply(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
int pt_index,
|
|
const int radius,
|
|
const int co[2])
|
|
{
|
|
MDeformVert *dvert = gps->dvert + pt_index;
|
|
|
|
/* Compute strength of effect. */
|
|
float inf = brush_influence_calc(gso, radius, co);
|
|
|
|
/* Get current weight. */
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, gso->vrgroup);
|
|
if (dw == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
/* Blend weight with average weight under the brush. */
|
|
dw->weight = interpf(gso->pbuffer_avg_weight, dw->weight, inf);
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
|
|
/* Perform auto-normalize. */
|
|
if (gso->auto_normalize) {
|
|
do_weight_paint_normalize_try(dvert, gso);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Blur Brush */
|
|
static bool brush_blur_apply(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
int pt_index,
|
|
const int radius,
|
|
const int co[2])
|
|
{
|
|
MDeformVert *dvert = gps->dvert + pt_index;
|
|
|
|
/* Compute strength of effect. */
|
|
float inf = brush_influence_calc(gso, radius, co);
|
|
|
|
/* Get current weight. */
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, gso->vrgroup);
|
|
if (dw == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
/* Find the 5 nearest points (this includes the to-be-blurred point itself). */
|
|
KDTreeNearest_2d nearest[5];
|
|
float pc_f[2];
|
|
copy_v2fl_v2i(pc_f, co);
|
|
const int tot = BLI_kdtree_2d_find_nearest_n(gso->fn_kdtree, pc_f, nearest, 5);
|
|
|
|
/* Calculate the average (=blurred) weight. */
|
|
float blur_weight = 0.0f, dist_sum = 0.0f;
|
|
int count = 0;
|
|
for (int i = 0; i < tot; i++) {
|
|
dist_sum += nearest[i].dist;
|
|
count++;
|
|
}
|
|
if (count <= 1) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < tot; i++) {
|
|
/* Weighted average, based on distance to point. */
|
|
blur_weight += (1.0f - nearest[i].dist / dist_sum) * gso->fn_pbuffer[nearest[i].index].weight;
|
|
}
|
|
blur_weight /= (count - 1);
|
|
|
|
/* Blend weight with blurred weight. */
|
|
dw->weight = interpf(blur_weight, dw->weight, inf);
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
|
|
/* Perform auto-normalize. */
|
|
if (gso->auto_normalize) {
|
|
do_weight_paint_normalize_try(dvert, gso);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Smear Brush */
|
|
static bool brush_smear_apply(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
int pt_index,
|
|
const int radius,
|
|
const int co[2])
|
|
{
|
|
MDeformVert *dvert = gps->dvert + pt_index;
|
|
|
|
/* Get current weight. */
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, gso->vrgroup);
|
|
if (dw == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
/* Find the 8 nearest points (this includes the to-be-blurred point itself). */
|
|
KDTreeNearest_2d nearest[8];
|
|
float pc_f[2];
|
|
copy_v2fl_v2i(pc_f, co);
|
|
const int tot = BLI_kdtree_2d_find_nearest_n(gso->fn_kdtree, pc_f, nearest, 8);
|
|
|
|
/* For smearing a weight from point A to point B, we look for a point A 'behind' the brush,
|
|
* matching the brush angle best and with the shortest distance to B. */
|
|
float point_dot[8] = {0};
|
|
float point_dir[2];
|
|
float score_max = 0.0f, dist_min = FLT_MAX, dist_max = 0.0f;
|
|
int i_max = -1, count = 0;
|
|
|
|
for (int i = 0; i < tot; i++) {
|
|
/* Skip the point we are about to smear. */
|
|
if (nearest[i].dist > GP_FIND_NEAREST_EPSILON) {
|
|
sub_v2_v2v2(point_dir, pc_f, nearest[i].co);
|
|
normalize_v2(point_dir);
|
|
|
|
/* Match A-B direction with brush direction. */
|
|
point_dot[i] = dot_v2v2(point_dir, gso->brush_dir);
|
|
if (point_dot[i] > 0.0f) {
|
|
count++;
|
|
float dist = nearest[i].dist;
|
|
if (dist < dist_min) {
|
|
dist_min = dist;
|
|
}
|
|
if (dist > dist_max) {
|
|
dist_max = dist;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (count == 0) {
|
|
return false;
|
|
}
|
|
|
|
/* Find best match in angle and distance. */
|
|
float dist_f = (dist_min == dist_max) ? 1.0f : 0.95f / (dist_max - dist_min);
|
|
for (int i = 0; i < tot; i++) {
|
|
if (point_dot[i] > 0.0f) {
|
|
float score = point_dot[i] * (1.0f - (nearest[i].dist - dist_min) * dist_f);
|
|
if (score > score_max) {
|
|
score_max = score;
|
|
i_max = i;
|
|
}
|
|
}
|
|
}
|
|
if (i_max == -1) {
|
|
return false;
|
|
}
|
|
|
|
/* Compute strength of effect. */
|
|
float inf = brush_influence_calc(gso, radius, co);
|
|
|
|
/* Smear the weight. */
|
|
dw->weight = interpf(gso->fn_pbuffer[nearest[i_max].index].weight, dw->weight, inf);
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
|
|
/* Perform auto-normalize. */
|
|
if (gso->auto_normalize) {
|
|
do_weight_paint_normalize_try(dvert, gso);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* ************************************************ */
|
|
/* Header Info */
|
|
static void gpencil_weightpaint_brush_header_set(bContext *C, tGP_BrushWeightpaintData *gso)
|
|
{
|
|
switch (gso->brush->gpencil_weight_tool) {
|
|
case GPWEIGHT_TOOL_DRAW:
|
|
ED_workspace_status_text(C, TIP_("GPencil Weight Paint: LMB to paint | RMB/Escape to Exit"));
|
|
break;
|
|
case GPWEIGHT_TOOL_BLUR:
|
|
ED_workspace_status_text(C, TIP_("GPencil Weight Blur: LMB to blur | RMB/Escape to Exit"));
|
|
break;
|
|
case GPWEIGHT_TOOL_AVERAGE:
|
|
ED_workspace_status_text(
|
|
C, TIP_("GPencil Weight Average: LMB to set average | RMB/Escape to Exit"));
|
|
break;
|
|
case GPWEIGHT_TOOL_SMEAR:
|
|
ED_workspace_status_text(C, TIP_("GPencil Weight Smear: LMB to smear | RMB/Escape to Exit"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ************************************************ */
|
|
/* Grease Pencil Weight Paint Operator */
|
|
|
|
/* Init/Exit ----------------------------------------------- */
|
|
|
|
static bool gpencil_weightpaint_brush_init(bContext *C, wmOperator *op)
|
|
{
|
|
Scene *scene = CTX_data_scene(C);
|
|
ToolSettings *ts = CTX_data_tool_settings(C);
|
|
Object *ob = CTX_data_active_object(C);
|
|
Paint *paint = &ts->gp_weightpaint->paint;
|
|
|
|
/* set the brush using the tool */
|
|
tGP_BrushWeightpaintData *gso;
|
|
|
|
/* setup operator data */
|
|
gso = static_cast<tGP_BrushWeightpaintData *>(
|
|
MEM_callocN(sizeof(tGP_BrushWeightpaintData), "tGP_BrushWeightpaintData"));
|
|
op->customdata = gso;
|
|
|
|
gso->bmain = CTX_data_main(C);
|
|
|
|
gso->brush = paint->brush;
|
|
BKE_curvemapping_init(gso->brush->curve);
|
|
|
|
gso->is_painting = false;
|
|
gso->first = true;
|
|
|
|
gso->pbuffer = nullptr;
|
|
gso->pbuffer_size = 0;
|
|
gso->pbuffer_used = 0;
|
|
|
|
gso->fn_pbuffer = nullptr;
|
|
gso->fn_added = nullptr;
|
|
gso->fn_kdtree = nullptr;
|
|
gso->fn_used = 0;
|
|
gso->fn_size = 0;
|
|
gso->use_find_nearest = ELEM(
|
|
gso->brush->gpencil_weight_tool, GPWEIGHT_TOOL_BLUR, GPWEIGHT_TOOL_SMEAR);
|
|
|
|
gso->gpd = ED_gpencil_data_get_active(C);
|
|
gso->scene = scene;
|
|
gso->object = ob;
|
|
if (ob) {
|
|
gso->vrgroup = gso->gpd->vertex_group_active_index - 1;
|
|
if (!BLI_findlink(&gso->gpd->vertex_group_names, gso->vrgroup)) {
|
|
gso->vrgroup = -1;
|
|
}
|
|
}
|
|
else {
|
|
gso->vrgroup = -1;
|
|
}
|
|
|
|
gso->region = CTX_wm_region(C);
|
|
|
|
/* Multi-frame settings. */
|
|
gso->is_multiframe = bool(GPENCIL_MULTIEDIT_SESSIONS_ON(gso->gpd));
|
|
gso->use_multiframe_falloff = (ts->gp_sculpt.flag & GP_SCULPT_SETT_FLAG_FRAME_FALLOFF) != 0;
|
|
|
|
/* Init multi-edit falloff curve data before doing anything,
|
|
* so we won't have to do it again later. */
|
|
if (gso->is_multiframe) {
|
|
BKE_curvemapping_init(ts->gp_sculpt.cur_falloff);
|
|
}
|
|
|
|
/* Draw tool: add or subtract weight? */
|
|
gso->subtract = (gso->brush->gpencil_settings->sculpt_flag & BRUSH_DIR_IN);
|
|
|
|
/* Setup auto-normalize. */
|
|
gso->auto_normalize = (ts->auto_normalize && gso->vrgroup != -1);
|
|
if (gso->auto_normalize) {
|
|
gso->vgroup_tot = BLI_listbase_count(&gso->gpd->vertex_group_names);
|
|
/* Get boolean array of vertex groups deformed by bones. */
|
|
gso->vgroup_bone_deformed = gpencil_vgroup_bone_deformed_map_get(ob, gso->vgroup_tot);
|
|
if (gso->vgroup_bone_deformed != nullptr) {
|
|
/* Get boolean array of locked vertex groups. */
|
|
gso->vgroup_locked = BKE_object_defgroup_lock_flags_get(ob, gso->vgroup_tot);
|
|
if (gso->vgroup_locked == nullptr) {
|
|
gso->vgroup_locked = (bool *)MEM_callocN(sizeof(bool) * gso->vgroup_tot, __func__);
|
|
}
|
|
}
|
|
else {
|
|
gso->auto_normalize = false;
|
|
}
|
|
}
|
|
|
|
/* Setup space conversions. */
|
|
gpencil_point_conversion_init(C, &gso->gsc);
|
|
|
|
/* Update header. */
|
|
gpencil_weightpaint_brush_header_set(C, gso);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void gpencil_weightpaint_brush_exit(bContext *C, wmOperator *op)
|
|
{
|
|
tGP_BrushWeightpaintData *gso = static_cast<tGP_BrushWeightpaintData *>(op->customdata);
|
|
|
|
/* Clear status bar text. */
|
|
ED_workspace_status_text(C, nullptr);
|
|
|
|
/* Free operator data */
|
|
MEM_SAFE_FREE(gso->vgroup_bone_deformed);
|
|
MEM_SAFE_FREE(gso->vgroup_locked);
|
|
if (gso->fn_kdtree != nullptr) {
|
|
BLI_kdtree_2d_free(gso->fn_kdtree);
|
|
}
|
|
if (gso->fn_added != nullptr) {
|
|
BLI_ghash_free(gso->fn_added, nullptr, nullptr);
|
|
}
|
|
MEM_SAFE_FREE(gso->fn_pbuffer);
|
|
MEM_SAFE_FREE(gso->pbuffer);
|
|
MEM_SAFE_FREE(gso);
|
|
op->customdata = nullptr;
|
|
}
|
|
|
|
/* Poll callback for stroke weight paint operator. */
|
|
static bool gpencil_weightpaint_brush_poll(bContext *C)
|
|
{
|
|
if (!ED_operator_regionactive(C)) {
|
|
CTX_wm_operator_poll_msg_set(C, "Active region not set");
|
|
return false;
|
|
}
|
|
|
|
ScrArea *area = CTX_wm_area(C);
|
|
if (area->spacetype != SPACE_VIEW3D) {
|
|
return false;
|
|
}
|
|
|
|
bGPdata *gpd = ED_gpencil_data_get_active(C);
|
|
if ((gpd == nullptr) || !GPENCIL_WEIGHT_MODE(gpd)) {
|
|
return false;
|
|
}
|
|
|
|
ToolSettings *ts = CTX_data_scene(C)->toolsettings;
|
|
if (!ts->gp_weightpaint->paint.brush) {
|
|
CTX_wm_operator_poll_msg_set(C, "Grease Pencil has no active paint tool");
|
|
return false;
|
|
}
|
|
|
|
/* NOTE: this is a bit slower, but is the most accurate... */
|
|
return CTX_DATA_COUNT(C, editable_gpencil_strokes) != 0;
|
|
}
|
|
|
|
/* Helper to save the points selected by the brush. */
|
|
static void gpencil_save_selected_point(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
const int gps_index,
|
|
const int index,
|
|
const int pc[2],
|
|
const bool within_brush)
|
|
{
|
|
tGP_Selected *selected;
|
|
bGPDspoint *pt = &gps->points[index];
|
|
|
|
/* Ensure the array to save the list of selected points is big enough. */
|
|
gpencil_select_buffer_ensure(gso, false);
|
|
|
|
/* Copy point data. */
|
|
if (within_brush) {
|
|
selected = &gso->pbuffer[gso->pbuffer_used];
|
|
selected->gps = gps;
|
|
selected->pt_index = index;
|
|
copy_v2_v2_int(selected->pc, pc);
|
|
copy_v4_v4(selected->color, pt->vert_color);
|
|
gso->pbuffer_used++;
|
|
}
|
|
|
|
/* Ensure vertex group and dvert. */
|
|
gpencil_vertex_group_ensure(gso);
|
|
BKE_gpencil_dvert_ensure(gps);
|
|
|
|
/* Copy current weight. */
|
|
MDeformVert *dvert = gps->dvert + index;
|
|
MDeformWeight *dw = BKE_defvert_find_index(dvert, gso->vrgroup);
|
|
if (within_brush && (dw != nullptr)) {
|
|
selected->weight = dw->weight;
|
|
}
|
|
|
|
/* Store point for finding nearest points (blur, smear). */
|
|
if (gso->use_find_nearest) {
|
|
/* Create hash key, assuming there are no more than 65536 strokes in a frame
|
|
* and 65536 points in a stroke. */
|
|
const int point_hash = (gps_index << GP_STROKE_HASH_BITSHIFT) + index;
|
|
|
|
/* Prevent duplicate points in buffer. */
|
|
if (!BLI_ghash_haskey(gso->fn_added, POINTER_FROM_INT(point_hash))) {
|
|
/* Add stroke point to find-nearest buffer. */
|
|
selected = &gso->fn_pbuffer[gso->fn_used];
|
|
copy_v2_v2_int(selected->pc, pc);
|
|
selected->weight = (dw == nullptr) ? 0.0f : dw->weight;
|
|
|
|
BLI_ghash_insert(
|
|
gso->fn_added, POINTER_FROM_INT(point_hash), POINTER_FROM_INT(gso->fn_used));
|
|
|
|
float pc_f[2];
|
|
copy_v2_fl2(pc_f, float(pc[0]), float(pc[1]));
|
|
BLI_kdtree_2d_insert(gso->fn_kdtree, gso->fn_used, pc_f);
|
|
|
|
gso->fn_do_balance = true;
|
|
gso->fn_used++;
|
|
}
|
|
else {
|
|
/* Update weight of point in buffer. */
|
|
int *idx = static_cast<int *>(BLI_ghash_lookup(gso->fn_added, POINTER_FROM_INT(point_hash)));
|
|
selected = &gso->fn_pbuffer[POINTER_AS_INT(idx)];
|
|
selected->weight = (dw == nullptr) ? 0.0f : dw->weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Select points in this stroke and add to an array to be used later. */
|
|
static void gpencil_weightpaint_select_stroke(tGP_BrushWeightpaintData *gso,
|
|
bGPDstroke *gps,
|
|
const int gps_index,
|
|
const float diff_mat[4][4],
|
|
const float bound_mat[4][4])
|
|
{
|
|
GP_SpaceConversion *gsc = &gso->gsc;
|
|
rcti *rect = &gso->brush_rect;
|
|
Brush *brush = gso->brush;
|
|
/* For the blur tool, look a bit wider than the brush itself,
|
|
* because we need the weight of surrounding points to perform the blur. */
|
|
const bool widen_brush = (gso->brush->gpencil_weight_tool == GPWEIGHT_TOOL_BLUR);
|
|
int radius_brush = (brush->flag & GP_BRUSH_USE_PRESSURE) ? gso->brush->size * gso->pressure :
|
|
gso->brush->size;
|
|
int radius_wide = (widen_brush) ? radius_brush * 1.3f : radius_brush;
|
|
bool within_brush = true;
|
|
|
|
bGPDstroke *gps_active = (gps->runtime.gps_orig) ? gps->runtime.gps_orig : gps;
|
|
bGPDspoint *pt_active = nullptr;
|
|
|
|
bGPDspoint *pt1, *pt2;
|
|
bGPDspoint *pt = nullptr;
|
|
int pc1[2] = {0};
|
|
int pc2[2] = {0};
|
|
int i;
|
|
int index;
|
|
bool include_last = false;
|
|
|
|
/* Check if the stroke collides with brush. */
|
|
if (!ED_gpencil_stroke_check_collision(gsc, gps, gso->mouse, radius_wide, bound_mat)) {
|
|
return;
|
|
}
|
|
|
|
if (gps->totpoints == 1) {
|
|
bGPDspoint pt_temp;
|
|
pt = &gps->points[0];
|
|
gpencil_point_to_world_space(gps->points, diff_mat, &pt_temp);
|
|
gpencil_point_to_xy(gsc, gps, &pt_temp, &pc1[0], &pc1[1]);
|
|
|
|
pt_active = (pt->runtime.pt_orig) ? pt->runtime.pt_orig : pt;
|
|
/* Do bound-box check first. */
|
|
if (!ELEM(V2D_IS_CLIPPED, pc1[0], pc1[1]) && BLI_rcti_isect_pt(rect, pc1[0], pc1[1])) {
|
|
/* only check if point is inside */
|
|
int mouse_i[2];
|
|
round_v2i_v2fl(mouse_i, gso->mouse);
|
|
int mlen = len_v2v2_int(mouse_i, pc1);
|
|
if (mlen <= radius_wide) {
|
|
/* apply operation to this point */
|
|
if (pt_active != nullptr) {
|
|
if (widen_brush) {
|
|
within_brush = (mlen <= radius_brush);
|
|
}
|
|
gpencil_save_selected_point(gso, gps_active, gps_index, 0, pc1, within_brush);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
/* Loop over the points in the stroke, checking for intersections
|
|
* - an intersection means that we touched the stroke
|
|
*/
|
|
for (i = 0; (i + 1) < gps->totpoints; i++) {
|
|
/* Get points to work with */
|
|
pt1 = gps->points + i;
|
|
pt2 = gps->points + i + 1;
|
|
|
|
bGPDspoint npt;
|
|
gpencil_point_to_world_space(pt1, diff_mat, &npt);
|
|
gpencil_point_to_xy(gsc, gps, &npt, &pc1[0], &pc1[1]);
|
|
|
|
gpencil_point_to_world_space(pt2, diff_mat, &npt);
|
|
gpencil_point_to_xy(gsc, gps, &npt, &pc2[0], &pc2[1]);
|
|
|
|
/* Check that point segment of the bound-box of the selection stroke */
|
|
if ((!ELEM(V2D_IS_CLIPPED, pc1[0], pc1[1]) && BLI_rcti_isect_pt(rect, pc1[0], pc1[1])) ||
|
|
(!ELEM(V2D_IS_CLIPPED, pc2[0], pc2[1]) && BLI_rcti_isect_pt(rect, pc2[0], pc2[1])))
|
|
{
|
|
/* Check if point segment of stroke had anything to do with
|
|
* brush region (either within stroke painted, or on its lines)
|
|
* - this assumes that line-width is irrelevant.
|
|
*/
|
|
if (gpencil_stroke_inside_circle(gso->mouse, radius_wide, pc1[0], pc1[1], pc2[0], pc2[1]))
|
|
{
|
|
if (widen_brush) {
|
|
within_brush = gpencil_stroke_inside_circle(
|
|
gso->mouse, radius_brush, pc1[0], pc1[1], pc2[0], pc2[1]);
|
|
}
|
|
|
|
/* To each point individually... */
|
|
pt = &gps->points[i];
|
|
pt_active = pt->runtime.pt_orig;
|
|
if (pt_active != nullptr) {
|
|
index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i;
|
|
gpencil_save_selected_point(gso, gps_active, gps_index, index, pc1, within_brush);
|
|
}
|
|
|
|
/* Only do the second point if this is the last segment,
|
|
* and it is unlikely that the point will get handled
|
|
* otherwise.
|
|
*
|
|
* NOTE: There is a small risk here that the second point wasn't really
|
|
* actually in-range. In that case, it only got in because
|
|
* the line linking the points was!
|
|
*/
|
|
if (i + 1 == gps->totpoints - 1) {
|
|
pt = &gps->points[i + 1];
|
|
pt_active = pt->runtime.pt_orig;
|
|
if (pt_active != nullptr) {
|
|
index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i + 1;
|
|
gpencil_save_selected_point(gso, gps_active, gps_index, index, pc2, within_brush);
|
|
include_last = false;
|
|
}
|
|
}
|
|
else {
|
|
include_last = true;
|
|
}
|
|
}
|
|
else if (include_last) {
|
|
/* This case is for cases where for whatever reason the second vert (1st here)
|
|
* doesn't get included because the whole edge isn't in bounds,
|
|
* but it would've qualified since it did with the previous step
|
|
* (but wasn't added then, to avoid double-ups).
|
|
*/
|
|
pt = &gps->points[i];
|
|
pt_active = pt->runtime.pt_orig;
|
|
if (pt_active != nullptr) {
|
|
index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i;
|
|
gpencil_save_selected_point(gso, gps_active, gps_index, index, pc1, true);
|
|
include_last = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Apply weight paint brushes to strokes in the given frame. */
|
|
static bool gpencil_weightpaint_brush_do_frame(bContext *C,
|
|
tGP_BrushWeightpaintData *gso,
|
|
bGPDframe *gpf,
|
|
const float diff_mat[4][4],
|
|
const float bound_mat[4][4])
|
|
{
|
|
Object *ob = CTX_data_active_object(C);
|
|
char tool = gso->brush->gpencil_weight_tool;
|
|
const int radius = (gso->brush->flag & GP_BRUSH_USE_PRESSURE) ?
|
|
gso->brush->size * gso->pressure :
|
|
gso->brush->size;
|
|
tGP_Selected *selected = nullptr;
|
|
gso->fn_do_balance = false;
|
|
|
|
/*---------------------------------------------------------------------
|
|
* First step: select the points affected. This step is required to have
|
|
* all selected points before apply the effect, because it could be
|
|
* required to do some step. Now is not used, but the operator is ready.
|
|
*--------------------------------------------------------------------- */
|
|
int gps_index;
|
|
LISTBASE_FOREACH_INDEX (bGPDstroke *, gps, &gpf->strokes, gps_index) {
|
|
/* Skip strokes that are invalid for current view. */
|
|
if (ED_gpencil_stroke_can_use(C, gps) == false) {
|
|
continue;
|
|
}
|
|
/* Check if the color is visible. */
|
|
if (ED_gpencil_stroke_material_visible(ob, gps) == false) {
|
|
continue;
|
|
}
|
|
|
|
/* Check points below the brush. */
|
|
gpencil_weightpaint_select_stroke(gso, gps, gps_index, diff_mat, bound_mat);
|
|
}
|
|
|
|
bDeformGroup *defgroup = static_cast<bDeformGroup *>(
|
|
BLI_findlink(&gso->gpd->vertex_group_names, gso->vrgroup));
|
|
if ((defgroup == nullptr) || (defgroup->flag & DG_LOCK_WEIGHT)) {
|
|
return false;
|
|
}
|
|
|
|
/*---------------------------------------------------------------------
|
|
* Second step: Calculations on selected points.
|
|
*--------------------------------------------------------------------- */
|
|
/* For average tool, get average weight of affected points. */
|
|
if (tool == GPWEIGHT_TOOL_AVERAGE) {
|
|
gpencil_select_buffer_avg_weight_set(gso);
|
|
}
|
|
/* Balance find-nearest kdtree. */
|
|
if (gso->use_find_nearest && gso->fn_do_balance) {
|
|
BLI_kdtree_2d_balance(gso->fn_kdtree);
|
|
}
|
|
|
|
/*---------------------------------------------------------------------
|
|
* Third step: Apply effect.
|
|
*--------------------------------------------------------------------- */
|
|
bool changed = false;
|
|
for (int i = 0; i < gso->pbuffer_used; i++) {
|
|
selected = &gso->pbuffer[i];
|
|
|
|
switch (tool) {
|
|
case GPWEIGHT_TOOL_DRAW: {
|
|
changed |= brush_draw_apply(gso, selected->gps, selected->pt_index, radius, selected->pc);
|
|
break;
|
|
}
|
|
case GPWEIGHT_TOOL_AVERAGE: {
|
|
changed |= brush_average_apply(
|
|
gso, selected->gps, selected->pt_index, radius, selected->pc);
|
|
break;
|
|
}
|
|
case GPWEIGHT_TOOL_BLUR: {
|
|
changed |= brush_blur_apply(gso, selected->gps, selected->pt_index, radius, selected->pc);
|
|
break;
|
|
}
|
|
case GPWEIGHT_TOOL_SMEAR: {
|
|
changed |= brush_smear_apply(gso, selected->gps, selected->pt_index, radius, selected->pc);
|
|
break;
|
|
}
|
|
default:
|
|
printf("ERROR: Unknown type of GPencil Weight Paint brush\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Clear the selected array, but keep the memory allocation. */
|
|
gpencil_select_buffer_ensure(gso, true);
|
|
|
|
return changed;
|
|
}
|
|
|
|
/* Apply brush effect to all layers. */
|
|
static bool gpencil_weightpaint_brush_apply_to_layers(bContext *C, tGP_BrushWeightpaintData *gso)
|
|
{
|
|
ToolSettings *ts = CTX_data_tool_settings(C);
|
|
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
|
|
Object *obact = gso->object;
|
|
bool changed = false;
|
|
|
|
Object *ob_eval = (Object *)DEG_get_evaluated_id(depsgraph, &obact->id);
|
|
bGPdata *gpd = (bGPdata *)ob_eval->data;
|
|
|
|
/* Find visible strokes, and perform operations on those if hit */
|
|
LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
|
|
/* If locked or no active frame, don't do anything. */
|
|
if (!BKE_gpencil_layer_is_editable(gpl) || (gpl->actframe == nullptr)) {
|
|
continue;
|
|
}
|
|
|
|
/* Calculate transform matrix. */
|
|
float diff_mat[4][4], bound_mat[4][4];
|
|
BKE_gpencil_layer_transform_matrix_get(depsgraph, obact, gpl, diff_mat);
|
|
copy_m4_m4(bound_mat, diff_mat);
|
|
mul_m4_m4m4(diff_mat, diff_mat, gpl->layer_invmat);
|
|
|
|
/* Active Frame or MultiFrame? */
|
|
if (gso->is_multiframe) {
|
|
/* init multi-frame falloff options */
|
|
int f_init = 0;
|
|
int f_end = 0;
|
|
|
|
if (gso->use_multiframe_falloff) {
|
|
BKE_gpencil_frame_range_selected(gpl, &f_init, &f_end);
|
|
}
|
|
|
|
LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
|
|
/* Always do active frame; Otherwise, only include selected frames */
|
|
if ((gpf == gpl->actframe) || (gpf->flag & GP_FRAME_SELECT)) {
|
|
/* Compute multi-frame falloff factor. */
|
|
if (gso->use_multiframe_falloff) {
|
|
/* Falloff depends on distance to active frame
|
|
* (relative to the overall frame range). */
|
|
gso->mf_falloff = BKE_gpencil_multiframe_falloff_calc(
|
|
gpf, gpl->actframe->framenum, f_init, f_end, ts->gp_sculpt.cur_falloff);
|
|
}
|
|
else {
|
|
/* No falloff */
|
|
gso->mf_falloff = 1.0f;
|
|
}
|
|
|
|
/* affect strokes in this frame */
|
|
changed |= gpencil_weightpaint_brush_do_frame(C, gso, gpf, diff_mat, bound_mat);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
/* Apply to active frame's strokes */
|
|
gso->mf_falloff = 1.0f;
|
|
changed |= gpencil_weightpaint_brush_do_frame(C, gso, gpl->actframe, diff_mat, bound_mat);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
/* Calculate settings for applying brush */
|
|
static void gpencil_weightpaint_brush_apply(bContext *C, wmOperator *op, PointerRNA *itemptr)
|
|
{
|
|
tGP_BrushWeightpaintData *gso = static_cast<tGP_BrushWeightpaintData *>(op->customdata);
|
|
Brush *brush = gso->brush;
|
|
const int radius = ((brush->flag & GP_BRUSH_USE_PRESSURE) ? gso->brush->size * gso->pressure :
|
|
gso->brush->size);
|
|
float mousef[2];
|
|
int mouse[2];
|
|
bool changed = false;
|
|
|
|
/* Get latest mouse coordinates */
|
|
RNA_float_get_array(itemptr, "mouse", mousef);
|
|
gso->mouse[0] = mouse[0] = int(mousef[0]);
|
|
gso->mouse[1] = mouse[1] = int(mousef[1]);
|
|
|
|
gso->pressure = RNA_float_get(itemptr, "pressure");
|
|
|
|
/* Store coordinates as reference, if operator just started running */
|
|
if (gso->first) {
|
|
gso->mouse_prev[0] = gso->mouse[0];
|
|
gso->mouse_prev[1] = gso->mouse[1];
|
|
gso->brush_dir_is_set = false;
|
|
}
|
|
gso->first = false;
|
|
|
|
/* Update brush_rect, so that it represents the bounding rectangle of brush. */
|
|
gso->brush_rect.xmin = mouse[0] - radius;
|
|
gso->brush_rect.ymin = mouse[1] - radius;
|
|
gso->brush_rect.xmax = mouse[0] + radius;
|
|
gso->brush_rect.ymax = mouse[1] + radius;
|
|
|
|
/* Calculate brush direction. */
|
|
if (gso->brush->gpencil_weight_tool == GPWEIGHT_TOOL_SMEAR) {
|
|
brush_calc_brush_dir_2d(gso);
|
|
|
|
if (!gso->brush_dir_is_set) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Apply brush to layers. */
|
|
changed = gpencil_weightpaint_brush_apply_to_layers(C, gso);
|
|
|
|
/* Updates. */
|
|
if (changed) {
|
|
DEG_id_tag_update(&gso->gpd->id, ID_RECALC_GEOMETRY);
|
|
WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr);
|
|
}
|
|
}
|
|
|
|
/* Running --------------------------------------------- */
|
|
|
|
/* helper - a record stroke, and apply paint event */
|
|
static void gpencil_weightpaint_brush_apply_event(bContext *C,
|
|
wmOperator *op,
|
|
const wmEvent *event)
|
|
{
|
|
tGP_BrushWeightpaintData *gso = static_cast<tGP_BrushWeightpaintData *>(op->customdata);
|
|
PointerRNA itemptr;
|
|
float mouse[2];
|
|
|
|
mouse[0] = event->mval[0] + 1;
|
|
mouse[1] = event->mval[1] + 1;
|
|
|
|
/* fill in stroke */
|
|
RNA_collection_add(op->ptr, "stroke", &itemptr);
|
|
|
|
RNA_float_set_array(&itemptr, "mouse", mouse);
|
|
RNA_boolean_set(&itemptr, "is_start", gso->first);
|
|
|
|
/* Handle pressure sensitivity (which is supplied by tablets). */
|
|
float pressure = event->tablet.pressure;
|
|
CLAMP(pressure, 0.0f, 1.0f);
|
|
RNA_float_set(&itemptr, "pressure", pressure);
|
|
|
|
/* apply */
|
|
gpencil_weightpaint_brush_apply(C, op, &itemptr);
|
|
}
|
|
|
|
/* reapply */
|
|
static int gpencil_weightpaint_brush_exec(bContext *C, wmOperator *op)
|
|
{
|
|
if (!gpencil_weightpaint_brush_init(C, op)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
RNA_BEGIN (op->ptr, itemptr, "stroke") {
|
|
gpencil_weightpaint_brush_apply(C, op, &itemptr);
|
|
}
|
|
RNA_END;
|
|
|
|
gpencil_weightpaint_brush_exit(C, op);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
/* start modal painting */
|
|
static int gpencil_weightpaint_brush_invoke(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
tGP_BrushWeightpaintData *gso = nullptr;
|
|
const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input");
|
|
const bool is_playing = ED_screen_animation_playing(CTX_wm_manager(C)) != nullptr;
|
|
|
|
/* the operator cannot work while play animation */
|
|
if (is_playing) {
|
|
BKE_report(op->reports, RPT_ERROR, "Cannot Paint while play animation");
|
|
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
/* init painting data */
|
|
if (!gpencil_weightpaint_brush_init(C, op)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
gso = static_cast<tGP_BrushWeightpaintData *>(op->customdata);
|
|
|
|
/* register modal handler */
|
|
WM_event_add_modal_handler(C, op);
|
|
|
|
/* start drawing immediately? */
|
|
if (is_modal == false) {
|
|
ARegion *region = CTX_wm_region(C);
|
|
|
|
/* apply first dab... */
|
|
gso->is_painting = true;
|
|
gpencil_weightpaint_brush_apply_event(C, op, event);
|
|
|
|
/* redraw view with feedback */
|
|
ED_region_tag_redraw(region);
|
|
}
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
/* painting - handle events. */
|
|
static int gpencil_weightpaint_brush_modal(bContext *C, wmOperator *op, const wmEvent *event)
|
|
{
|
|
tGP_BrushWeightpaintData *gso = static_cast<tGP_BrushWeightpaintData *>(op->customdata);
|
|
const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input");
|
|
bool redraw_region = false;
|
|
bool redraw_toolsettings = false;
|
|
|
|
/* The operator can be in 2 states: Painting and Idling. */
|
|
if (gso->is_painting) {
|
|
/* Painting. */
|
|
switch (event->type) {
|
|
/* Mouse Move: Apply somewhere else. */
|
|
case MOUSEMOVE:
|
|
case INBETWEEN_MOUSEMOVE:
|
|
/* Apply brush effect at new position. */
|
|
gpencil_weightpaint_brush_apply_event(C, op, event);
|
|
|
|
/* Force redraw, so that the cursor will at least be valid. */
|
|
redraw_region = true;
|
|
break;
|
|
|
|
/* Painting mouse-button release: Stop painting (back to idle). */
|
|
case LEFTMOUSE:
|
|
if (is_modal) {
|
|
/* go back to idling... */
|
|
gso->is_painting = false;
|
|
}
|
|
else {
|
|
/* end painting, since we're not modal. */
|
|
gso->is_painting = false;
|
|
|
|
gpencil_weightpaint_brush_exit(C, op);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
break;
|
|
|
|
/* Abort painting if any of the usual things are tried. */
|
|
case MIDDLEMOUSE:
|
|
case RIGHTMOUSE:
|
|
case EVT_ESCKEY:
|
|
gpencil_weightpaint_brush_exit(C, op);
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
}
|
|
else {
|
|
/* Idling. */
|
|
BLI_assert(is_modal == true);
|
|
|
|
switch (event->type) {
|
|
/* Painting mouse-button press = Start painting (switch to painting state). */
|
|
case LEFTMOUSE:
|
|
/* do initial "click" apply. */
|
|
gso->is_painting = true;
|
|
gso->first = true;
|
|
|
|
gpencil_weightpaint_brush_apply_event(C, op, event);
|
|
break;
|
|
|
|
/* Exit modal operator, based on the "standard" ops. */
|
|
case RIGHTMOUSE:
|
|
case EVT_ESCKEY:
|
|
gpencil_weightpaint_brush_exit(C, op);
|
|
return OPERATOR_FINISHED;
|
|
|
|
/* MMB is often used for view manipulations. */
|
|
case MIDDLEMOUSE:
|
|
return OPERATOR_PASS_THROUGH;
|
|
|
|
/* Mouse movements should update the brush cursor - Just redraw the active region. */
|
|
case MOUSEMOVE:
|
|
case INBETWEEN_MOUSEMOVE:
|
|
redraw_region = true;
|
|
break;
|
|
|
|
/* Change Frame - Allowed. */
|
|
case EVT_LEFTARROWKEY:
|
|
case EVT_RIGHTARROWKEY:
|
|
case EVT_UPARROWKEY:
|
|
case EVT_DOWNARROWKEY:
|
|
return OPERATOR_PASS_THROUGH;
|
|
|
|
/* Camera/View Gizmo's - Allowed. */
|
|
/* See rationale in `gpencil_paint.cc`, #gpencil_draw_modal(). */
|
|
case EVT_PAD0:
|
|
case EVT_PAD1:
|
|
case EVT_PAD2:
|
|
case EVT_PAD3:
|
|
case EVT_PAD4:
|
|
case EVT_PAD5:
|
|
case EVT_PAD6:
|
|
case EVT_PAD7:
|
|
case EVT_PAD8:
|
|
case EVT_PAD9:
|
|
return OPERATOR_PASS_THROUGH;
|
|
|
|
/* Unhandled event. */
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Redraw region? */
|
|
if (redraw_region) {
|
|
ED_region_tag_redraw(CTX_wm_region(C));
|
|
}
|
|
|
|
/* Redraw toolsettings (brush settings)? */
|
|
if (redraw_toolsettings) {
|
|
DEG_id_tag_update(&gso->gpd->id, ID_RECALC_GEOMETRY);
|
|
WM_event_add_notifier(C, NC_SCENE | ND_TOOLSETTINGS, nullptr);
|
|
}
|
|
|
|
return OPERATOR_RUNNING_MODAL;
|
|
}
|
|
|
|
void GPENCIL_OT_weight_paint(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Stroke Weight Paint";
|
|
ot->idname = "GPENCIL_OT_weight_paint";
|
|
ot->description = "Draw weight on stroke points";
|
|
|
|
/* api callbacks */
|
|
ot->exec = gpencil_weightpaint_brush_exec;
|
|
ot->invoke = gpencil_weightpaint_brush_invoke;
|
|
ot->modal = gpencil_weightpaint_brush_modal;
|
|
ot->cancel = gpencil_weightpaint_brush_exit;
|
|
ot->poll = gpencil_weightpaint_brush_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING;
|
|
|
|
/* properties */
|
|
PropertyRNA *prop;
|
|
prop = RNA_def_collection_runtime(ot->srna, "stroke", &RNA_OperatorStrokeElement, "Stroke", "");
|
|
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
|
|
|
|
prop = RNA_def_boolean(ot->srna, "wait_for_input", true, "Wait for Input", "");
|
|
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/* Weight Toggle Add/Subtract Operator */
|
|
static int gpencil_weight_toggle_direction_invoke(bContext *C,
|
|
wmOperator * /*op*/,
|
|
const wmEvent * /*event*/)
|
|
{
|
|
ToolSettings *ts = CTX_data_tool_settings(C);
|
|
Paint *paint = &ts->gp_weightpaint->paint;
|
|
|
|
/* Toggle Add/Subtract flag. */
|
|
paint->brush->gpencil_settings->sculpt_flag ^= BRUSH_DIR_IN;
|
|
|
|
/* Update tool settings. */
|
|
WM_main_add_notifier(NC_BRUSH | NA_EDITED, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
void GPENCIL_OT_weight_toggle_direction(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Weight Paint Toggle Direction";
|
|
ot->idname = "GPENCIL_OT_weight_toggle_direction";
|
|
ot->description = "Toggle Add/Subtract for the weight paint draw tool";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = gpencil_weight_toggle_direction_invoke;
|
|
ot->poll = gpencil_weightpaint_brush_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------- */
|
|
/* Weight Sample Operator */
|
|
static int gpencil_weight_sample_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event)
|
|
{
|
|
/* Get mouse position. */
|
|
int mouse[2];
|
|
mouse[0] = event->mval[0] + 1;
|
|
mouse[1] = event->mval[1] + 1;
|
|
float mouse_f[2];
|
|
copy_v2fl_v2i(mouse_f, mouse);
|
|
|
|
/* Get active GP object. */
|
|
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
|
|
Object *ob = CTX_data_active_object(C);
|
|
bGPdata *gpd = ED_gpencil_data_get_active(C);
|
|
|
|
if ((ob == nullptr) || (gpd == nullptr)) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
/* Get active vertex group. */
|
|
int vgroup = gpd->vertex_group_active_index - 1;
|
|
bDeformGroup *defgroup = static_cast<bDeformGroup *>(
|
|
BLI_findlink(&gpd->vertex_group_names, vgroup));
|
|
if (!defgroup) {
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
/* Init space conversion. */
|
|
GP_SpaceConversion gsc = {nullptr};
|
|
gpencil_point_conversion_init(C, &gsc);
|
|
|
|
/* Get evaluated GP object. */
|
|
Object *ob_eval = (Object *)DEG_get_evaluated_id(depsgraph, &ob->id);
|
|
bGPdata *gpd_eval = (bGPdata *)ob_eval->data;
|
|
|
|
/* Get brush radius. */
|
|
ToolSettings *ts = CTX_data_tool_settings(C);
|
|
Brush *brush = ts->gp_weightpaint->paint.brush;
|
|
const int radius = brush->size;
|
|
|
|
/* Init closest points. */
|
|
float closest_dist[2] = {FLT_MAX, FLT_MAX};
|
|
float closest_weight[2] = {0.0f, 0.0f};
|
|
int closest_count = 0;
|
|
int pc[2] = {0};
|
|
|
|
/* Inspect all layers. */
|
|
LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd_eval->layers) {
|
|
/* If no active frame, don't do anything. */
|
|
if (gpl->actframe == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
/* Calculate transform matrix. */
|
|
float diff_mat[4][4], bound_mat[4][4];
|
|
BKE_gpencil_layer_transform_matrix_get(depsgraph, ob, gpl, diff_mat);
|
|
copy_m4_m4(bound_mat, diff_mat);
|
|
mul_m4_m4m4(diff_mat, diff_mat, gpl->layer_invmat);
|
|
|
|
/* Inspect all strokes in active frame. */
|
|
LISTBASE_FOREACH (bGPDstroke *, gps, &gpl->actframe->strokes) {
|
|
/* Look for strokes that collide with the brush. */
|
|
if (!ED_gpencil_stroke_check_collision(&gsc, gps, mouse_f, radius, bound_mat)) {
|
|
continue;
|
|
}
|
|
if (gps->dvert == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
/* Look for two closest points. */
|
|
for (int i = 0; i < gps->totpoints; i++) {
|
|
bGPDspoint *pt = &gps->points[i];
|
|
bGPDspoint npt;
|
|
|
|
gpencil_point_to_world_space(pt, diff_mat, &npt);
|
|
gpencil_point_to_xy(&gsc, gps, &npt, &pc[0], &pc[1]);
|
|
|
|
float dist = len_v2v2_int(pc, mouse);
|
|
|
|
if ((dist < closest_dist[0]) || (dist < closest_dist[1])) {
|
|
/* Get weight. */
|
|
MDeformVert *dvert = &gps->dvert[i];
|
|
MDeformWeight *dw = BKE_defvert_find_index(dvert, vgroup);
|
|
if (dw == nullptr) {
|
|
continue;
|
|
}
|
|
if (dist < closest_dist[0]) {
|
|
closest_dist[1] = closest_dist[0];
|
|
closest_weight[1] = closest_weight[0];
|
|
closest_dist[0] = dist;
|
|
closest_weight[0] = dw->weight;
|
|
closest_count++;
|
|
}
|
|
else if (dist < closest_dist[1]) {
|
|
closest_dist[1] = dist;
|
|
closest_weight[1] = dw->weight;
|
|
closest_count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Set brush weight, based on points found. */
|
|
if (closest_count > 0) {
|
|
if (closest_count == 1) {
|
|
brush->weight = closest_weight[0];
|
|
}
|
|
else {
|
|
CLAMP_MIN(closest_dist[1], 1e-6f);
|
|
float dist_sum = closest_dist[0] + closest_dist[1];
|
|
brush->weight = (1.0f - closest_dist[0] / dist_sum) * closest_weight[0] +
|
|
(1.0f - closest_dist[1] / dist_sum) * closest_weight[1];
|
|
}
|
|
|
|
/* Update tool settings. */
|
|
WM_main_add_notifier(NC_BRUSH | NA_EDITED, nullptr);
|
|
|
|
return OPERATOR_FINISHED;
|
|
}
|
|
|
|
return OPERATOR_CANCELLED;
|
|
}
|
|
|
|
void GPENCIL_OT_weight_sample(wmOperatorType *ot)
|
|
{
|
|
/* identifiers */
|
|
ot->name = "Weight Paint Sample Weight";
|
|
ot->idname = "GPENCIL_OT_weight_sample";
|
|
ot->description = "Use the mouse to sample a weight in the 3D view";
|
|
|
|
/* api callbacks */
|
|
ot->invoke = gpencil_weight_sample_invoke;
|
|
ot->poll = gpencil_weightpaint_brush_poll;
|
|
|
|
/* flags */
|
|
ot->flag = OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
|
|
}
|