Refactor: Clean up and modernize core vertex weight normalize functions

- Add code documentation.
- Use Span and references rather than pointers everywhere.
- Move all core normalization logic into
  `BKE_defvert_normalize_lock_map()`, and have the other variants call
  that. This keeps all the logic in one place, which will help make
  future changes easier since they only need to be made in one place.
- Add unit tests for `BKE_defvert_normalize_lock_map()`.
- Refactor `vgroup_normalize_all()` to be clearer and avoid an
  unnecessary `goto`.
- Make both `vgroup_normalize_all()` and `paint_weight_gradient_exec()`
  only call into a single core vertex normalization function, rather
  than branching into one of many.

No functional change intended.
This commit is contained in:
Nathan Vegdahl
2025-07-08 14:30:54 +02:00
committed by Nathan Vegdahl
parent 4ce93a4b4e
commit caddde1eb7
6 changed files with 320 additions and 232 deletions

View File

@@ -6,6 +6,7 @@
#include "BLI_math_vector_types.hh"
#include "BLI_offset_indices.hh"
#include "BLI_span.hh"
#include "BLI_string_ref.hh"
#include "BLI_virtual_array_fwd.hh"
@@ -233,26 +234,49 @@ void BKE_defvert_sync_mapped(MDeformVert *dvert_dst,
void BKE_defvert_remap(MDeformVert *dvert, const int *map, int map_len);
void BKE_defvert_flip(MDeformVert *dvert, const int *flip_map, int flip_map_num);
void BKE_defvert_flip_merged(MDeformVert *dvert, const int *flip_map, int flip_map_num);
void BKE_defvert_normalize(MDeformVert *dvert);
/**
* Same as #BKE_defvert_normalize but takes a bool array.
* Normalize all the vertex group weights on a vertex.
*
* Note: this ignores whether groups are locked or not, and will therefore
* happily modify even locked groups.
*
* See #BKE_defvert_normalize_lock_map() for parameter documentation.
*/
void BKE_defvert_normalize_subset(MDeformVert *dvert, const bool *vgroup_subset, int vgroup_num);
void BKE_defvert_normalize(MDeformVert &dvert);
/**
* Same as BKE_defvert_normalize() if the locked vgroup is not a member of the subset
* Normalize a subset of vertex group weights among themselves.
*
* Note: this ignores whether groups are locked or not, and will therefore
* happily modify even locked groups.
*
* See #BKE_defvert_normalize_lock_map() for parameter documentation.
*/
void BKE_defvert_normalize_lock_single(MDeformVert *dvert,
const bool *vgroup_subset,
int vgroup_num,
uint def_nr_lock);
void BKE_defvert_normalize_subset(MDeformVert &dvert, blender::Span<bool> subset_flags);
/**
* Same as BKE_defvert_normalize() if no locked vgroup is a member of the subset
* Normalize the vertex groups of a vertex, with all the bells and whistles.
*
* \param dvert: the vertex weights to be normalized.
*
* \param subset_flags: span of bools indicating which vertex groups are
* included vs ignored in this function. True means included, false means
* ignored. Note that this is different than locking: locked groups are not
* *modified*, but their weights are still accounted for in the normalization
* process, whereas ignored groups aren't accounted for at all. May be empty,
* indicating all vertex groups are included. If not empty, its length must
* match the number of vertex groups in the source data (e.g. the mesh).
*
* \param lock_flags: span of bools with `true` indicating the vertex groups
* that are completely locked from modification, even if that prevents
* normalization. May be empty, indicating no locked groups. If not empty, its
* length must match the number of vertex groups in the source data (e.g. the
* mesh).
*/
void BKE_defvert_normalize_lock_map(MDeformVert *dvert,
const bool *vgroup_subset,
int vgroup_num,
const bool *lock_flags,
int defbase_num);
void BKE_defvert_normalize_lock_map(MDeformVert &dvert,
blender::Span<bool> subset_flags,
blender::Span<bool> lock_flags);
/* Utilities to 'extract' a given vgroup into a simple float array,
* for verts, but also edges/faces/loops. */

View File

@@ -842,6 +842,7 @@ if(WITH_GTESTS)
intern/brush_test.cc
intern/cryptomatte_test.cc
intern/curves_geometry_test.cc
intern/deform_test.cc
intern/fcurve_test.cc
intern/file_handler_test.cc
intern/grease_pencil_test.cc

View File

@@ -23,6 +23,7 @@
#include "BLI_listbase.h"
#include "BLI_math_vector.h"
#include "BLI_span.hh"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLI_utildefines.h"
@@ -227,178 +228,107 @@ void BKE_defvert_remap(MDeformVert *dvert, const int *map, const int map_len)
}
}
void BKE_defvert_normalize_subset(MDeformVert *dvert,
const bool *vgroup_subset,
const int vgroup_num)
void BKE_defvert_normalize_subset(MDeformVert &dvert, blender::Span<bool> subset_flags)
{
if (dvert->totweight == 0) {
/* nothing */
BKE_defvert_normalize_lock_map(dvert, subset_flags, {});
}
void BKE_defvert_normalize(MDeformVert &dvert)
{
BKE_defvert_normalize_lock_map(dvert, {}, {});
}
void BKE_defvert_normalize_lock_map(MDeformVert &dvert,
blender::Span<bool> subset_flags,
blender::Span<bool> lock_flags)
{
const bool use_subset = !subset_flags.is_empty();
const bool use_locks = !lock_flags.is_empty();
/* Note: confusingly, `totweight` isn't the total weight on the vertex, it's
* the number of vertex groups assigned to the vertex. It's a DNA field, so
* I'm leaving it named as-is for now despite it being confusing. */
if (dvert.totweight == 0) {
/* No vertex groups assigned: do nothing. */
return;
}
else if (dvert->totweight == 1) {
MDeformWeight *dw = dvert->dw;
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if (dvert.totweight == 1) {
/* Only one vertex group is assigned to the vertex.
*
* TODO: this special case for single-group vertices should be completely
* unnecessary. The code further below works just as well for one assigned
* group as for twenty. However, the old version of this function was *not*
* consistent in its behavior between single-group and multi-group vertices:
* single-group vertices would set the group weight to 1.0 even if the
* initial weight was zero, whereas multi-group vertices with all weights
* set to zero would be left as-is.
*
* I (Nathan Vegdahl) decided to leave this special case here just in case
* any other code depends on this odd behavior. But we should revisit this
* at some point to check if that's actually the case, and simply remove
* this special case if nothing is depending on it. */
MDeformWeight *dw = dvert.dw;
if (use_subset && !subset_flags[dw->def_nr]) {
return;
}
const bool is_unlocked = lock_flags.is_empty() || !lock_flags[dw->def_nr];
if (is_unlocked) {
dw->weight = 1.0f;
}
return;
}
else {
MDeformWeight *dw = dvert->dw;
float tot_weight = 0.0f;
for (int i = dvert->totweight; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
tot_weight += dw->weight;
}
blender::MutableSpan<MDeformWeight> vertex_weights = blender::MutableSpan(dvert.dw,
dvert.totweight);
/* Collect weights. */
float total_locked_weight = 0.0f;
float total_regular_weight = 0.0f; /* Unlocked. */
for (MDeformWeight &dw : vertex_weights) {
if (use_subset && !subset_flags[dw.def_nr]) {
/* Not part of the subset being normalized. */
continue;
}
if (tot_weight > 0.0f) {
float scalar = 1.0f / tot_weight;
dw = dvert->dw;
for (int i = dvert->totweight; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
dw->weight *= scalar;
/* in case of division errors with very low weights */
CLAMP(dw->weight, 0.0f, 1.0f);
}
}
if (use_locks && lock_flags[dw.def_nr]) {
/* Locked. */
total_locked_weight += dw.weight;
}
else {
total_regular_weight += dw.weight;
}
}
}
void BKE_defvert_normalize(MDeformVert *dvert)
{
if (dvert->totweight == 0) {
/* nothing */
}
else if (dvert->totweight == 1) {
dvert->dw[0].weight = 1.0f;
}
else {
MDeformWeight *dw;
uint i;
float tot_weight = 0.0f;
const float available_weight = max_ff(0.0f, 1.0f - total_locked_weight);
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
tot_weight += dw->weight;
/* Special case: all non-locked vertex groups have zero weight. */
if (total_regular_weight <= 0.0f) {
return;
}
/* Compute scale factor for unlocked group weights. */
const float scale = available_weight / total_regular_weight;
/* Normalize the weights via scaling by the appropriate factors. */
for (MDeformWeight &dw : vertex_weights) {
if (use_subset && !subset_flags[dw.def_nr]) {
/* Not part of the subset being normalized. */
continue;
}
if (tot_weight > 0.0f) {
float scalar = 1.0f / tot_weight;
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
dw->weight *= scalar;
/* in case of division errors with very low weights */
CLAMP(dw->weight, 0.0f, 1.0f);
}
}
}
}
void BKE_defvert_normalize_lock_single(MDeformVert *dvert,
const bool *vgroup_subset,
const int vgroup_num,
const uint def_nr_lock)
{
if (dvert->totweight == 0) {
/* nothing */
}
else if (dvert->totweight == 1) {
MDeformWeight *dw = dvert->dw;
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if (def_nr_lock != dw->def_nr) {
dw->weight = 1.0f;
}
}
}
else {
MDeformWeight *dw_lock = nullptr;
MDeformWeight *dw;
uint i;
float tot_weight = 0.0f;
float lock_iweight = 1.0f;
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if (dw->def_nr != def_nr_lock) {
tot_weight += dw->weight;
}
else {
dw_lock = dw;
lock_iweight = (1.0f - dw_lock->weight);
CLAMP(lock_iweight, 0.0f, 1.0f);
}
}
if (use_locks && lock_flags[dw.def_nr]) {
/* Locked. */
continue;
}
if (tot_weight > 0.0f) {
/* paranoid, should be 1.0 but in case of float error clamp anyway */
dw.weight *= scale;
float scalar = (1.0f / tot_weight) * lock_iweight;
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if (dw != dw_lock) {
dw->weight *= scalar;
/* in case of division errors with very low weights */
CLAMP(dw->weight, 0.0f, 1.0f);
}
}
}
}
}
}
void BKE_defvert_normalize_lock_map(MDeformVert *dvert,
const bool *vgroup_subset,
const int vgroup_num,
const bool *lock_flags,
const int defbase_num)
{
if (dvert->totweight == 0) {
/* nothing */
}
else if (dvert->totweight == 1) {
MDeformWeight *dw = dvert->dw;
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if ((dw->def_nr < defbase_num) && (lock_flags[dw->def_nr] == false)) {
dw->weight = 1.0f;
}
}
}
else {
MDeformWeight *dw;
uint i;
float tot_weight = 0.0f;
float lock_iweight = 0.0f;
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if ((dw->def_nr < defbase_num) && (lock_flags[dw->def_nr] == false)) {
tot_weight += dw->weight;
}
else {
/* invert after */
lock_iweight += dw->weight;
}
}
}
lock_iweight = max_ff(0.0f, 1.0f - lock_iweight);
if (tot_weight > 0.0f) {
/* paranoid, should be 1.0 but in case of float error clamp anyway */
float scalar = (1.0f / tot_weight) * lock_iweight;
for (i = dvert->totweight, dw = dvert->dw; i != 0; i--, dw++) {
if ((dw->def_nr < vgroup_num) && vgroup_subset[dw->def_nr]) {
if ((dw->def_nr < defbase_num) && (lock_flags[dw->def_nr] == false)) {
dw->weight *= scalar;
/* in case of division errors with very low weights */
CLAMP(dw->weight, 0.0f, 1.0f);
}
}
}
}
/* In case of division errors with very low weights. */
CLAMP(dw.weight, 0.0f, 1.0f);
}
}

View File

@@ -0,0 +1,128 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_deform.hh"
#include "DNA_meshdata_types.h"
#include "testing/testing.h"
namespace blender::bke::tests {
TEST(vertex_weights_normalize, EmptyWeights)
{
/* Just making sure we don't crash on a vertex with no weights. */
/* Some compilers don't like zero-length arrays, so we just do nullptr here as
* a stand-in. */
MDeformWeight *weights = nullptr;
MDeformVert vert = {weights, 0, 0};
BKE_defvert_normalize_lock_map(vert, {}, {});
}
TEST(vertex_weights_normalize, SingleWeight)
{
MDeformWeight weights[1];
weights[0].def_nr = 0;
MDeformVert vert = {weights, 1, 0};
/* Excluded from normalized set: shouldn't be touched. */
weights[0].weight = 0.5;
BKE_defvert_normalize_lock_map(vert, {false}, {false});
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
/* Locked: shouldn't be touched. */
weights[0].weight = 0.5;
BKE_defvert_normalize_lock_map(vert, {true}, {true});
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
/* Unlocked: should get normalized to 1.0. */
weights[0].weight = 0.5;
BKE_defvert_normalize_lock_map(vert, {true}, {false});
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
/* An empty "subset" flag list should be equivalent to everything being included. */
weights[0].weight = 0.5;
BKE_defvert_normalize_lock_map(vert, {}, {false});
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
/* An empty "locked" flag list should be equivalent to everything being unlocked. */
weights[0].weight = 0.5;
BKE_defvert_normalize_lock_map(vert, {true}, {});
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
/* Zero weight: single-group vertices are special cased for some reason to be
* set to 1.0. */
weights[0].weight = 0.0;
BKE_defvert_normalize_lock_map(vert, {}, {});
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
/* Zero weight locked: shouldn't be touched. */
weights[0].weight = 0.0;
BKE_defvert_normalize_lock_map(vert, {}, {true});
EXPECT_FLOAT_EQ(0.0, weights[0].weight);
}
TEST(vertex_weights_normalize, TwoWeights)
{
MDeformWeight weights[2];
weights[0].def_nr = 0;
weights[1].def_nr = 1;
MDeformVert vert = {weights, 2, 0};
/* Both excluded from normalized set: shouldn't be touched. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {false, false}, {false, false});
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
EXPECT_FLOAT_EQ(0.25, weights[1].weight);
/* One included: included one should be set to 1.0. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {false, true}, {false, false});
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
EXPECT_FLOAT_EQ(1.0, weights[1].weight);
/* Both included: should be normalized together. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {true, true}, {false, false});
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
EXPECT_FLOAT_EQ(0.5, weights[1].weight);
/* All flag arrays being empty should mean: included and unlocked. So this
* should behave as a simple normalization across both groups. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {}, {});
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
EXPECT_FLOAT_EQ(0.5, weights[1].weight);
/* Both included but locked: shouldn't be touched. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {}, {true, true});
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
EXPECT_FLOAT_EQ(0.25, weights[1].weight);
/* Only one locked: locked shouldn't be touched, unlocked should pick up the
* slack for normalization. */
weights[0].weight = 0.25;
weights[1].weight = 0.25;
BKE_defvert_normalize_lock_map(vert, {}, {true, false});
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
EXPECT_FLOAT_EQ(0.75, weights[1].weight);
/* Zero weight: shouldn't be touched. */
weights[0].weight = 0.0;
weights[1].weight = 0.0;
BKE_defvert_normalize_lock_map(vert, {}, {false, false});
EXPECT_FLOAT_EQ(0.0, weights[0].weight);
EXPECT_FLOAT_EQ(0.0, weights[1].weight);
}
} // namespace blender::bke::tests

View File

@@ -657,14 +657,12 @@ static bool vgroup_normalize_active_vertex(Object *ob, eVGroupSelect subset_type
bool *lock_flags = BKE_object_defgroup_lock_flags_get(ob, vgroup_tot);
if (lock_flags) {
const ListBase *defbase = BKE_object_defgroup_list(ob);
const int defbase_tot = BLI_listbase_count(defbase);
BKE_defvert_normalize_lock_map(
dvert_act, vgroup_validmap, vgroup_tot, lock_flags, defbase_tot);
*dvert_act, Span(vgroup_validmap, vgroup_tot), Span(lock_flags, vgroup_tot));
MEM_freeN(lock_flags);
}
else {
BKE_defvert_normalize_subset(dvert_act, vgroup_validmap, vgroup_tot);
BKE_defvert_normalize_subset(*dvert_act, Span(vgroup_validmap, vgroup_tot));
}
MEM_freeN(vgroup_validmap);
@@ -1372,6 +1370,19 @@ static void vgroup_levels_subset(Object *ob,
}
}
/**
* Normalize vertex group weights.
*
* \param vgroup_validmap: An array of bools indicating which vertex groups
* should be included (true) and excluded (false) from the normalization
* process. Must have #vgroup_tot elements.
*
* \param lock_active: If true, the active vertex group is temporarily locked
* during this normalization process.
*
* \return True if modification to weights might have happened, false if
* modification was impossible (e.g. due to all groups being locked.)
*/
static bool vgroup_normalize_all(Object *ob,
const bool *vgroup_validmap,
const int vgroup_tot,
@@ -1379,65 +1390,56 @@ static bool vgroup_normalize_all(Object *ob,
ReportList *reports,
std::optional<int> current_frame = {})
{
MDeformVert *dv, **dvert_array = nullptr;
int i, dvert_tot = 0;
BLI_assert(vgroup_tot == BLI_listbase_count(BKE_object_defgroup_list(ob)));
const int def_nr = BKE_object_defgroup_active_index_get(ob) - 1;
const bool use_vert_sel = vertex_group_use_vert_sel(ob);
BLI_assert(def_nr < vgroup_tot);
MDeformVert **dvert_array = nullptr;
int dvert_tot = 0;
vgroup_parray_alloc(
static_cast<ID *>(ob->data), &dvert_array, &dvert_tot, use_vert_sel, current_frame);
if (dvert_array) {
const ListBase *defbase = BKE_object_defgroup_list(ob);
const int defbase_tot = BLI_listbase_count(defbase);
bool *lock_flags = BKE_object_defgroup_lock_flags_get(ob, defbase_tot);
bool changed = false;
if ((lock_active == true) && (lock_flags != nullptr) && (def_nr < defbase_tot)) {
lock_flags[def_nr] = true;
}
if (lock_flags) {
for (i = 0; i < defbase_tot; i++) {
if (lock_flags[i] == false) {
break;
}
}
if (i == defbase_tot) {
BKE_report(reports, RPT_ERROR, "All groups are locked");
goto finally;
}
}
for (i = 0; i < dvert_tot; i++) {
/* in case its not selected */
if ((dv = dvert_array[i])) {
if (lock_flags) {
BKE_defvert_normalize_lock_map(dv, vgroup_validmap, vgroup_tot, lock_flags, defbase_tot);
}
else if (lock_active) {
BKE_defvert_normalize_lock_single(dv, vgroup_validmap, vgroup_tot, def_nr);
}
else {
BKE_defvert_normalize_subset(dv, vgroup_validmap, vgroup_tot);
}
}
}
changed = true;
finally:
if (lock_flags) {
MEM_freeN(lock_flags);
}
MEM_freeN(dvert_array);
return changed;
if (!dvert_array) {
return false;
}
return false;
Span<bool> subset_flags = Span(vgroup_validmap, vgroup_tot);
MutableSpan<bool> lock_flags = {};
bool *lock_flags_array = BKE_object_defgroup_lock_flags_get(ob, vgroup_tot);
if (lock_flags_array) {
lock_flags = MutableSpan(lock_flags_array, vgroup_tot);
}
else if (lock_active) {
lock_flags_array = MEM_malloc_arrayN<bool>(vgroup_tot, "lock_flags_array");
std::memset(lock_flags_array, 0, vgroup_tot); /* Clear to false. */
lock_flags = MutableSpan(lock_flags_array, vgroup_tot);
}
if (lock_active) {
lock_flags[def_nr] = true;
}
const bool all_locked = !lock_flags.contains(false);
if (all_locked) {
BKE_report(reports, RPT_ERROR, "All groups are locked");
}
else {
/* Normalize. */
for (int i = 0; i < dvert_tot; i++) {
MDeformVert *dv = dvert_array[i];
/* in case its not selected */
if (dv) {
BKE_defvert_normalize_lock_map(*dv, subset_flags, lock_flags);
}
}
}
MEM_freeN(dvert_array);
MEM_SAFE_FREE(lock_flags_array);
return !all_locked;
}
/**

View File

@@ -851,20 +851,23 @@ static wmOperatorStatus paint_weight_gradient_exec(bContext *C, wmOperator *op)
if (scene->toolsettings->auto_normalize) {
const int vgroup_num = BLI_listbase_count(&mesh->vertex_group_names);
bool *lock_flags = BKE_object_defgroup_lock_flags_get(ob, vgroup_num);
if (!lock_flags) {
lock_flags = MEM_malloc_arrayN<bool>(vgroup_num, "lock_flags");
std::memset(lock_flags, 0, vgroup_num); /* Clear to false. */
lock_flags[data.def_nr] = true;
}
bool *vgroup_validmap = BKE_object_defgroup_validmap_get(ob, vgroup_num);
if (vgroup_validmap != nullptr) {
MDeformVert *dvert = dverts;
Span<bool> subset_flags_span = Span(vgroup_validmap, vgroup_num);
Span<bool> lock_flags_span = Span(lock_flags, vgroup_num);
for (int i = 0; i < mesh->verts_num; i++) {
if ((data.vert_cache->elem[i].flag & WPGradient_vertStore::VGRAD_STORE_IS_MODIFIED) != 0) {
if (lock_flags != nullptr) {
BKE_defvert_normalize_lock_map(
&dvert[i], vgroup_validmap, vgroup_num, lock_flags, vgroup_num);
}
else {
BKE_defvert_normalize_lock_single(&dvert[i], vgroup_validmap, vgroup_num, data.def_nr);
}
BKE_defvert_normalize_lock_map(dvert[i], subset_flags_span, lock_flags_span);
}
}
MEM_SAFE_FREE(lock_flags);
MEM_freeN(vgroup_validmap);
}
}