When manually setting a group's vertex weight, auto-normalization would fail in some circumstances, such as when all other groups are locked. The root cause of this issue was our approach for ensuring that the weight specified by the user remained as-is when possible during normalization. Rather than "when possible", it erroneously *always* ensured the weight stayed as-is even when that made normalization impossible. It came down to this: 1. Normalization is done as a post process, with no knowledge of what changes were just made to the weights. 2. In order to (try to) make up for that and ensure that the just-set weight remains as the user specified, the active group was temporarily locked during normalization, which could prevent normalization in some cases. This PR fixes the issue by introducing a new internal-only concept of "soft locked" vertex groups to the normalization functions, intended to be used in exactly these cases where there are weights that have just been set and we want to avoid altering them when possible. Soft-locked groups are left untouched whenever normalization is achievable without touching them, but are still modified if normalization can't be achieved otherwise. This has been implemented by introducing a new bool array alongside the "locked" bool array in the core normalization functions. Although all uses in this PR only ever specify a single group as "soft locked", using a bool array will make it easy to use this concept in other weight painting tools in the future, which may modify more than one group at once. Pull Request: https://projects.blender.org/blender/blender/pulls/141045
228 lines
8.2 KiB
C++
228 lines
8.2 KiB
C++
/* 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_ex(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_ex(vert, {false}, {false}, {false});
|
|
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
|
|
|
|
/* Locked: shouldn't be touched. */
|
|
weights[0].weight = 0.5;
|
|
BKE_defvert_normalize_ex(vert, {true}, {true}, {false});
|
|
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
|
|
|
|
/* Unlocked: should get normalized to 1.0. */
|
|
weights[0].weight = 0.5;
|
|
BKE_defvert_normalize_ex(vert, {true}, {false}, {false});
|
|
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
|
|
|
|
/* Unlocked and soft-locked: should get normalized to 1.0. */
|
|
weights[0].weight = 0.5;
|
|
BKE_defvert_normalize_ex(vert, {true}, {false}, {true});
|
|
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
|
|
|
|
/* Locked and soft-locked: shouldn't be touched (locked takes precedent). */
|
|
weights[0].weight = 0.5;
|
|
BKE_defvert_normalize_ex(vert, {true}, {true}, {true});
|
|
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
|
|
|
|
/* An empty "subset" flag list should be equivalent to everything being included. */
|
|
weights[0].weight = 0.5;
|
|
BKE_defvert_normalize_ex(vert, {}, {false}, {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_ex(vert, {true}, {}, {false});
|
|
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_ex(vert, {}, {}, {});
|
|
EXPECT_FLOAT_EQ(1.0, weights[0].weight);
|
|
|
|
/* Zero weight locked: shouldn't be touched. */
|
|
weights[0].weight = 0.0;
|
|
BKE_defvert_normalize_ex(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_ex(vert, {false, false}, {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_ex(vert, {false, true}, {false, false}, {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_ex(vert, {true, true}, {false, false}, {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, unlocked, and not "just
|
|
* set". So this should behave as a simple normalization across both groups. */
|
|
weights[0].weight = 0.25;
|
|
weights[1].weight = 0.25;
|
|
BKE_defvert_normalize_ex(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_ex(vert, {}, {true, true}, {false, false});
|
|
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_ex(vert, {}, {true, false}, {false, false});
|
|
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
|
|
EXPECT_FLOAT_EQ(0.75, weights[1].weight);
|
|
|
|
/* Only one marked as soft-locked: soft-locked shouldn't be touched, the other
|
|
* should pick up the slack for normalization. */
|
|
weights[0].weight = 0.25;
|
|
weights[1].weight = 0.25;
|
|
BKE_defvert_normalize_ex(vert, {}, {false, false}, {true, false});
|
|
EXPECT_FLOAT_EQ(0.25, weights[0].weight);
|
|
EXPECT_FLOAT_EQ(0.75, weights[1].weight);
|
|
|
|
/* One locked, the other marked as soft-locked: soft-locked should pick up the
|
|
* slack for normalization. */
|
|
weights[0].weight = 0.25;
|
|
weights[1].weight = 0.25;
|
|
BKE_defvert_normalize_ex(vert, {}, {true, false}, {false, true});
|
|
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_ex(vert, {}, {false, false}, {false, false});
|
|
EXPECT_FLOAT_EQ(0.0, weights[0].weight);
|
|
EXPECT_FLOAT_EQ(0.0, weights[1].weight);
|
|
|
|
/* Zero weight with one group soft-locked: soft-locked should pick up the slack. */
|
|
weights[0].weight = 0.0;
|
|
weights[1].weight = 0.0;
|
|
BKE_defvert_normalize_ex(vert, {}, {false, false}, {false, true});
|
|
EXPECT_FLOAT_EQ(0.0, weights[0].weight);
|
|
EXPECT_FLOAT_EQ(1.0, weights[1].weight);
|
|
|
|
/* Zero weight with both groups soft-locked: both should pick up the slack equally. */
|
|
weights[0].weight = 0.0;
|
|
weights[1].weight = 0.0;
|
|
BKE_defvert_normalize_ex(vert, {}, {false, false}, {true, true});
|
|
EXPECT_FLOAT_EQ(0.5, weights[0].weight);
|
|
EXPECT_FLOAT_EQ(0.5, weights[1].weight);
|
|
}
|
|
|
|
TEST(vertex_weights_normalize, FourWeights)
|
|
{
|
|
/* Note the out-of-order `def_nr`, which is part of this test. Further below,
|
|
* we write the weights ordered to line up with the boolean arrays to make
|
|
* things easier to follow. */
|
|
MDeformWeight weights[4];
|
|
weights[0].def_nr = 3;
|
|
weights[1].def_nr = 0;
|
|
weights[2].def_nr = 1;
|
|
weights[3].def_nr = 2;
|
|
MDeformVert vert = {weights, 4, 0};
|
|
|
|
/* One locked, one soft-locked: the remaining two should pick up the slack. */
|
|
weights[1].weight = 0.125;
|
|
weights[2].weight = 0.125;
|
|
weights[3].weight = 0.125;
|
|
weights[0].weight = 0.0625;
|
|
BKE_defvert_normalize_ex(vert, {}, {true, false, false, false}, {false, false, true, false});
|
|
EXPECT_FLOAT_EQ(0.125, weights[1].weight);
|
|
EXPECT_FLOAT_EQ(0.75 * 2.0 / 3.0, weights[2].weight);
|
|
EXPECT_FLOAT_EQ(0.125, weights[3].weight);
|
|
EXPECT_FLOAT_EQ(0.75 / 3.0, weights[0].weight);
|
|
|
|
/* One locked, two soft-locked: the remaining one should pick up the slack. */
|
|
weights[1].weight = 0.125;
|
|
weights[2].weight = 0.125;
|
|
weights[3].weight = 0.125;
|
|
weights[0].weight = 0.125;
|
|
BKE_defvert_normalize_ex(vert, {}, {true, false, false, false}, {false, true, true, false});
|
|
EXPECT_FLOAT_EQ(0.125, weights[1].weight);
|
|
EXPECT_FLOAT_EQ(0.125, weights[2].weight);
|
|
EXPECT_FLOAT_EQ(0.125, weights[3].weight);
|
|
EXPECT_FLOAT_EQ(0.625, weights[0].weight);
|
|
|
|
/* One locked, one soft-locked, and the rest zero-weight: the soft-locked one
|
|
* should pick up the slack. */
|
|
weights[1].weight = 0.125;
|
|
weights[2].weight = 0.0;
|
|
weights[3].weight = 0.125;
|
|
weights[0].weight = 0.0;
|
|
BKE_defvert_normalize_ex(vert, {}, {true, false, false, false}, {false, false, true, false});
|
|
EXPECT_FLOAT_EQ(0.125, weights[1].weight);
|
|
EXPECT_FLOAT_EQ(0.0, weights[2].weight);
|
|
EXPECT_FLOAT_EQ(0.875, weights[3].weight);
|
|
EXPECT_FLOAT_EQ(0.0, weights[0].weight);
|
|
|
|
/* One locked, two soft-locked, and the last zero-weight: the soft-locked ones
|
|
* should pick up the slack. */
|
|
weights[1].weight = 0.125;
|
|
weights[2].weight = 0.125;
|
|
weights[3].weight = 0.25;
|
|
weights[0].weight = 0.0;
|
|
BKE_defvert_normalize_ex(vert, {}, {true, false, false, false}, {false, true, true, false});
|
|
EXPECT_FLOAT_EQ(0.125, weights[1].weight);
|
|
EXPECT_FLOAT_EQ(0.875 / 3.0, weights[2].weight);
|
|
EXPECT_FLOAT_EQ(0.875 * 2.0 / 3.0, weights[3].weight);
|
|
EXPECT_FLOAT_EQ(0.0f, weights[0].weight);
|
|
}
|
|
|
|
} // namespace blender::bke::tests
|