From 2523958e0e3ec903c8e555baa01d985ea19384b1 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Wed, 20 Nov 2024 22:03:32 +0100 Subject: [PATCH] USD: Add support for animated point instancers The existing point instancer reader is slightly refactored to allow for animated setups. The primary change is simply to inherit from `USDGeomReader` rather than `USDXformReader`. This allows access to the `read_geometry` API used by the cache modifier. The existing `read_object_data` method is split into two parts with `read_geometry` loading per-frame USD data and `read_object_data` coordinating the initial loading process, including creating the GN node tree just once. A new test has been added (a variation of the nested point instancer file) with time samples on various attributes and on both point instancers. This also fixes #129502 and the file provided in that issue. ---- The already added test file is `tests/data/usd/usd_point_instancer_anim.usda` Pull Request: https://projects.blender.org/blender/blender/pulls/129881 --- .../usd/intern/usd_reader_pointinstancer.cc | 69 ++++++++++++++----- .../usd/intern/usd_reader_pointinstancer.hh | 14 +++- .../blender/io/usd/intern/usd_reader_stage.cc | 3 + tests/python/bl_usd_import_test.py | 56 ++++++++++++++- 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/source/blender/io/usd/intern/usd_reader_pointinstancer.cc b/source/blender/io/usd/intern/usd_reader_pointinstancer.cc index 59e9889eb33..2d9f1360c1f 100644 --- a/source/blender/io/usd/intern/usd_reader_pointinstancer.cc +++ b/source/blender/io/usd/intern/usd_reader_pointinstancer.cc @@ -5,6 +5,7 @@ #include "usd_reader_pointinstancer.hh" #include "BKE_attribute.hh" +#include "BKE_geometry_set.hh" #include "BKE_modifier.hh" #include "BKE_node.hh" #include "BKE_node_runtime.hh" @@ -43,33 +44,27 @@ void USDPointInstancerReader::create_object(Main *bmain, const double /*motionSa this->object_->data = point_cloud; } -void USDPointInstancerReader::read_object_data(Main *bmain, const double motionSampleTime) +void USDPointInstancerReader::read_geometry(bke::GeometrySet &geometry_set, + USDMeshReadParams params, + const char ** /*r_err_str*/) { - PointCloud *base_point_cloud = static_cast(object_->data); - pxr::VtArray positions; pxr::VtArray scales; pxr::VtArray orientations; pxr::VtArray proto_indices; - std::vector time_samples; + std::vector mask = point_instancer_prim_.ComputeMaskAtTime(params.motion_sample_time); - point_instancer_prim_.GetPositionsAttr().GetTimeSamples(&time_samples); + point_instancer_prim_.GetPositionsAttr().Get(&positions, params.motion_sample_time); + point_instancer_prim_.GetScalesAttr().Get(&scales, params.motion_sample_time); + point_instancer_prim_.GetOrientationsAttr().Get(&orientations, params.motion_sample_time); + point_instancer_prim_.GetProtoIndicesAttr().Get(&proto_indices, params.motion_sample_time); - double sample_time = motionSampleTime; - - if (!time_samples.empty()) { - sample_time = time_samples[0]; + PointCloud *point_cloud = geometry_set.get_pointcloud_for_write(); + if (point_cloud->totpoint != positions.size()) { + /* Size changed so we must reallocate. */ + point_cloud = BKE_pointcloud_new_nomain(positions.size()); } - point_instancer_prim_.GetPositionsAttr().Get(&positions, sample_time); - point_instancer_prim_.GetScalesAttr().Get(&scales, sample_time); - point_instancer_prim_.GetOrientationsAttr().Get(&orientations, sample_time); - point_instancer_prim_.GetProtoIndicesAttr().Get(&proto_indices, sample_time); - - std::vector mask = point_instancer_prim_.ComputeMaskAtTime(sample_time); - - PointCloud *point_cloud = BKE_pointcloud_new_nomain(positions.size()); - MutableSpan point_positions = point_cloud->positions_for_write(); point_positions.copy_from(Span(positions.data(), positions.size()).cast()); @@ -133,7 +128,32 @@ void USDPointInstancerReader::read_object_data(Main *bmain, const double motionS mask_attribute.finish(); - BKE_pointcloud_nomain_to_pointcloud(point_cloud, base_point_cloud); + geometry_set.replace_pointcloud(point_cloud); +} + +void USDPointInstancerReader::read_object_data(Main *bmain, const double motionSampleTime) +{ + PointCloud *point_cloud = static_cast(object_->data); + + bke::GeometrySet geometry_set = bke::GeometrySet::from_pointcloud( + point_cloud, bke::GeometryOwnershipType::Editable); + + const USDMeshReadParams params = create_mesh_read_params(motionSampleTime, + import_params_.mesh_read_flag); + + read_geometry(geometry_set, params, nullptr); + + PointCloud *read_point_cloud = + geometry_set.get_component_for_write().release(); + + if (read_point_cloud != point_cloud) { + BKE_pointcloud_nomain_to_pointcloud(read_point_cloud, point_cloud); + } + + if (is_animated()) { + /* If the point cloud has time-varying data, we add the cache modifier. */ + add_cache_modifier(); + } ModifierData *md = BKE_modifier_new(eModifierType_Nodes); BLI_addtail(&object_->modifiers, md); @@ -281,4 +301,15 @@ void USDPointInstancerReader::set_collection(Main *bmain, Collection &coll) } } +bool USDPointInstancerReader::is_animated() const +{ + bool is_animated = false; + is_animated |= point_instancer_prim_.GetPositionsAttr().ValueMightBeTimeVarying(); + is_animated |= point_instancer_prim_.GetScalesAttr().ValueMightBeTimeVarying(); + is_animated |= point_instancer_prim_.GetOrientationsAttr().ValueMightBeTimeVarying(); + is_animated |= point_instancer_prim_.GetProtoIndicesAttr().ValueMightBeTimeVarying(); + + return is_animated; +} + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_pointinstancer.hh b/source/blender/io/usd/intern/usd_reader_pointinstancer.hh index dbfbe9dec98..2b53cd0a132 100644 --- a/source/blender/io/usd/intern/usd_reader_pointinstancer.hh +++ b/source/blender/io/usd/intern/usd_reader_pointinstancer.hh @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once -#include "usd_reader_xform.hh" +#include "usd_reader_geom.hh" #include @@ -13,7 +13,7 @@ namespace blender::io::usd { /* Wraps the UsdGeomPointInstancer schema. Creates a Blender point cloud object. */ -class USDPointInstancerReader : public USDXformReader { +class USDPointInstancerReader : public USDGeomReader { private: pxr::UsdGeomPointInstancer point_instancer_prim_; @@ -21,7 +21,7 @@ class USDPointInstancerReader : public USDXformReader { USDPointInstancerReader(const pxr::UsdPrim &prim, const USDImportParams &import_params, const ImportSettings &settings) - : USDXformReader(prim, import_params, settings), point_instancer_prim_(prim) + : USDGeomReader(prim, import_params, settings), point_instancer_prim_(prim) { } @@ -34,6 +34,11 @@ class USDPointInstancerReader : public USDXformReader { void read_object_data(Main *bmain, double motionSampleTime) override; + /* This may be called by the cache modifier to update animated geometry. */ + void read_geometry(bke::GeometrySet &geometry_set, + USDMeshReadParams params, + const char **r_err_str) override; + pxr::SdfPathVector proto_paths() const; /** @@ -47,6 +52,9 @@ class USDPointInstancerReader : public USDXformReader { * \param coll: The collection to set */ void set_collection(Main *bmain, Collection &coll); + + /* Return true if the USD data may be time varying. */ + bool is_animated() const; }; } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_reader_stage.cc b/source/blender/io/usd/intern/usd_reader_stage.cc index e6be67aaf15..a9acaf5eb33 100644 --- a/source/blender/io/usd/intern/usd_reader_stage.cc +++ b/source/blender/io/usd/intern/usd_reader_stage.cc @@ -215,6 +215,9 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim) if (prim.IsA()) { return new USDPointsReader(prim, params_, settings_); } + if (prim.IsA()) { + return new USDPointInstancerReader(prim, params_, settings_); + } if (prim.IsA()) { return new USDXformReader(prim, params_, settings_); } diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 4d2dc88067b..94ef9a7bccf 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -36,8 +36,8 @@ class USDImportTest(AbstractUSDTest): # Utility function to round each component of a vector to a few digits. The "+ 0" is to # ensure that any negative zeros (-0.0) are converted to positive zeros (0.0). @staticmethod - def round_vector(vector): - return [round(c, 5) + 0 for c in vector] + def round_vector(vector, digits=5): + return [round(c, digits) + 0 for c in vector] def test_import_operator(self): """Test running the import operator on valid and invalid files.""" @@ -863,6 +863,58 @@ class USDImportTest(AbstractUSDTest): self.assertEqual(3, vertical_points) self.assertEqual(2, horizontal_points) + def test_import_point_instancer_animation(self): + """Test importing an animated point instancer setup.""" + + infile = str(self.testdir / "usd_point_instancer_anim.usda") + res = bpy.ops.wm.usd_import(filepath=infile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}") + + prev_unique_positions = set() + prev_unique_scales = set() + prev_unique_quats = set() + + # Check all frames to ensure instances are moving correctly + for frame in range(1, 5): + bpy.context.scene.frame_set(frame) + depsgraph = bpy.context.evaluated_depsgraph_get() + + # Gather the instance data in a set so we can detect unique values + unique_positions = set() + unique_scales = set() + unique_quats = set() + mesh_count = 0 + for inst in depsgraph.object_instances: + if inst.is_instance and inst.object.type == 'MESH': + mesh_count += 1 + unique_positions.add(tuple(self.round_vector(inst.matrix_world.to_translation()))) + unique_scales.add(tuple(self.round_vector(inst.matrix_world.to_scale(), 1))) + unique_quats.add(tuple(self.round_vector(inst.matrix_world.to_quaternion()))) + + # There should be 6 total mesh instances + self.assertEqual(mesh_count, 6) + + # Positions: All positions should be unique during each frame. + # Scale and Orientation: One unique value on frame 1. Subsequent frames have different + # combinations of unique values. + self.assertEqual(len(unique_positions), 6, f"Frame {frame}: positions are unexpected") + if frame == 1: + self.assertEqual(len(unique_scales), 1, f"Frame {frame}: scales are unexpected") + self.assertEqual(len(unique_quats), 1, f"Frame {frame}: orientations are unexpected") + else: + self.assertEqual(len(unique_scales), 2, f"Frame {frame}: scales are unexpected") + self.assertEqual(len(unique_quats), 3, f"Frame {frame}: orientations are unexpected") + + # Every frame is different. Ensure that the current frame's values do NOT match the + # previous frame's data. + self.assertNotEqual(unique_positions, prev_unique_positions) + self.assertNotEqual(unique_scales, prev_unique_scales) + self.assertNotEqual(unique_quats, prev_unique_quats) + + prev_unique_positions = unique_positions + prev_unique_scales = unique_scales + prev_unique_quats = unique_quats + def test_import_light_types(self): """Test importing light types and attributes."""