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:
committed by
Jesse Yurkovich
parent
8bcb714b9e
commit
2523958e0e
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user