Mesh: Add validation of CD_MDISPS tangent vector data

This commit adds support for validating and correcting Multires tangent
data stored in CD_MDISPS when called via `BKE_mesh_validate` when
importing or on demand when users call `mesh.validate()` via the Python
API.

Any non-finite values are set to 0. While this will result in likely odd
looking data on a multires mesh, it will prevent further propagation of
bad float data and will allow users to restore their mesh to a workable
state in the case of data corruption.

Ref: #149205

Pull Request: https://projects.blender.org/blender/blender/pulls/149320
This commit is contained in:
Sean Kim 2025-11-04 22:42:09 +01:00 • committed by Sean Kim
parent 279a3e1821
commit b72b204390
4 changed files with 84 additions and 2 deletions

View file

@ -762,6 +762,49 @@ static bool validate_generic_attributes(const Mesh &mesh, const bool verbose, Me
return all_attributes_valid;
}
static bool validate_mdisps(const Mesh &mesh, const bool verbose, Mesh *mesh_mut)
{
const MDisps *mdisps = static_cast<const MDisps *>(
CustomData_get_layer(&mesh.corner_data, CD_MDISPS));
if (!mdisps) {
return true;
}
IndexMaskMemory memory;
const IndexMask invalid = IndexMask::from_predicate(
IndexRange(mesh.corners_num), GrainSize(4096), memory, [&](const int i) {
const Span<float> disps = Span(reinterpret_cast<const float3 *>(mdisps[i].disps),
mdisps[i].totdisp)
.cast<float>();
return std::any_of(
disps.begin(), disps.end(), [&](const float &v) { return !std::isfinite(v); });
});
if (invalid.is_empty()) {
return true;
}
if (verbose) {
print_error_with_indices(invalid, "Multires displacement has invalid values");
}
if (mesh_mut) {
if (MDisps *mdisp_mut = static_cast<MDisps *>(
CustomData_get_layer_for_write(&mesh_mut->corner_data, CD_MDISPS, mesh.corners_num)))
{
invalid.foreach_index(GrainSize(512), [&](const int i) {
MutableSpan<float> disps = MutableSpan(reinterpret_cast<float3 *>(mdisp_mut[i].disps),
mdisp_mut[i].totdisp)
.cast<float>();
for (float &disp_component : disps) {
disp_component = std::isfinite(disp_component) ? disp_component : 0.0f;
}
});
}
}
return false;
}
static bool mesh_validate_impl(const Mesh &mesh, const bool verbose, Mesh *mesh_mut)
{
IndexMaskMemory memory;
@ -825,6 +868,7 @@ static bool mesh_validate_impl(const Mesh &mesh, const bool verbose, Mesh *mesh_
valid &= validate_material_indices(mesh, true, verbose, mesh_mut);
valid &= validate_selection_history(mesh, verbose, mesh_mut);
valid &= validate_generic_attributes(mesh, verbose, mesh_mut);
valid &= validate_mdisps(mesh, verbose, mesh_mut);
if (valid) {
return true;

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b399e423de04c458891984f225f22c5d9e6ae4fb74ed5f36a1379246747163eb
size 72185

View file

@ -417,6 +417,8 @@ if(TEST_SRC_DIR_EXISTS)
add_blender_test_allow_error(
mesh_validate
--python ${TEST_PYTHON_DIR}/mesh_validate.py
--
--testdir "${TEST_SRC_DIR}"
)
add_blender_test(

View file

@ -6,6 +6,11 @@
import unittest
import bpy
import sys
import pathlib
"""
blender -b --factory-startup --python tests/python/mesh_validate.py -- --testdir tests/files/
"""
class TestMeshValidate(unittest.TestCase):
@ -108,6 +113,34 @@ class TestMeshValidate(unittest.TestCase):
self.assertFalse(mesh.validate(verbose=True))
args = None
class TestMeshValidateExistingFile(unittest.TestCase):
def setUp(self):
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "sculpting/invalid_mdisp_cube.blend"), load_ui=False)
def test_mesh_with_bad_multires_displacements(self):
mesh = bpy.context.active_object.data
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def main():
global args
import argparse
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == '__main__':
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
unittest.main()
main()