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
This commit is contained in:
Jesse Yurkovich
2024-11-20 22:03:32 +01:00
committed by Jesse Yurkovich
parent 8bcb714b9e
commit 2523958e0e
4 changed files with 118 additions and 24 deletions

View File

@@ -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<PointCloud *>(object_->data);
pxr::VtArray<pxr::GfVec3f> positions;
pxr::VtArray<pxr::GfVec3f> scales;
pxr::VtArray<pxr::GfQuath> orientations;
pxr::VtArray<int> proto_indices;
std::vector<double> time_samples;
std::vector<bool> 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<bool> mask = point_instancer_prim_.ComputeMaskAtTime(sample_time);
PointCloud *point_cloud = BKE_pointcloud_new_nomain(positions.size());
MutableSpan<float3> point_positions = point_cloud->positions_for_write();
point_positions.copy_from(Span(positions.data(), positions.size()).cast<float3>());
@@ -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<PointCloud *>(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<bke::PointCloudComponent>().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

View File

@@ -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 <pxr/usd/usdGeom/pointInstancer.h>
@@ -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

View File

@@ -215,6 +215,9 @@ USDPrimReader *USDStageReader::create_reader(const pxr::UsdPrim &prim)
if (prim.IsA<pxr::UsdGeomPoints>()) {
return new USDPointsReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomPointInstancer>()) {
return new USDPointInstancerReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomImageable>()) {
return new USDXformReader(prim, params_, settings_);
}

View File

@@ -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."""