USD: Support additional animated Basis Curves data during import/export
Export Like we do for Mesh and PointCloud, export any "velocity" attribute on the Point domain as native USD "velocities". While testing, a few additional blender-internal attributes were discovered being exported which are now excluded during export. Import Add the cache modifier as appropriate when we detect that UsdBasisCurve data is animated. This includes time-varying positions, widths, velocities, and general attribute values. Before this PR, only the positions were considered. And like Export, the native USD "velocities" attribute is now processed. Adds test coverage as well. Pull Request: https://projects.blender.org/blender/blender/pulls/133027
This commit is contained in:
committed by
Jesse Yurkovich
parent
7456709b69
commit
49ae7ffc9c
@@ -141,15 +141,33 @@ void USDCurvesReader::create_object(Main *bmain, const double /*motionSampleTime
|
||||
void USDCurvesReader::read_object_data(Main *bmain, double motionSampleTime)
|
||||
{
|
||||
Curves *cu = (Curves *)object_->data;
|
||||
read_curve_sample(cu, motionSampleTime);
|
||||
this->read_curve_sample(cu, motionSampleTime);
|
||||
|
||||
if (is_animated()) {
|
||||
add_cache_modifier();
|
||||
if (this->is_animated()) {
|
||||
this->add_cache_modifier();
|
||||
}
|
||||
|
||||
USDXformReader::read_object_data(bmain, motionSampleTime);
|
||||
}
|
||||
|
||||
void USDCurvesReader::read_velocities(bke::CurvesGeometry &curves,
|
||||
const pxr::UsdGeomCurves &usd_curves,
|
||||
const double motionSampleTime) const
|
||||
{
|
||||
pxr::VtVec3fArray velocities;
|
||||
usd_curves.GetVelocitiesAttr().Get(&velocities, motionSampleTime);
|
||||
|
||||
if (!velocities.empty()) {
|
||||
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
|
||||
bke::SpanAttributeWriter<float3> velocity =
|
||||
attributes.lookup_or_add_for_write_only_span<float3>("velocity", bke::AttrDomain::Point);
|
||||
|
||||
Span<pxr::GfVec3f> usd_data(velocities.data(), velocities.size());
|
||||
velocity.span.copy_from(usd_data.cast<float3>());
|
||||
velocity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
void USDCurvesReader::read_custom_data(bke::CurvesGeometry &curves,
|
||||
const double motionSampleTime) const
|
||||
{
|
||||
@@ -196,7 +214,21 @@ void USDCurvesReader::read_geometry(bke::GeometrySet &geometry_set,
|
||||
|
||||
bool USDBasisCurvesReader::is_animated() const
|
||||
{
|
||||
return curve_prim_.GetPointsAttr().ValueMightBeTimeVarying();
|
||||
if (curve_prim_.GetPointsAttr().ValueMightBeTimeVarying() ||
|
||||
curve_prim_.GetWidthsAttr().ValueMightBeTimeVarying() ||
|
||||
curve_prim_.GetVelocitiesAttr().ValueMightBeTimeVarying())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
pxr::UsdGeomPrimvarsAPI pv_api(curve_prim_);
|
||||
for (const pxr::UsdGeomPrimvar &pv : pv_api.GetPrimvarsWithValues()) {
|
||||
if (pv.ValueMightBeTimeVarying()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void USDBasisCurvesReader::read_curve_sample(Curves *curves_id, const double motionSampleTime)
|
||||
@@ -287,13 +319,10 @@ void USDBasisCurvesReader::read_curve_sample(Curves *curves_id, const double mot
|
||||
}
|
||||
|
||||
if (!usdWidths.empty()) {
|
||||
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
|
||||
bke::SpanAttributeWriter<float> radii = attributes.lookup_or_add_for_write_only_span<float>(
|
||||
"radius", bke::AttrDomain::Point);
|
||||
|
||||
MutableSpan<float> radii = curves.radius_for_write();
|
||||
pxr::TfToken widths_interp = curve_prim_.GetWidthsInterpolation();
|
||||
if (widths_interp == pxr::UsdGeomTokens->constant) {
|
||||
radii.span.fill(usdWidths[0] / 2.0f);
|
||||
radii.fill(usdWidths[0] / 2.0f);
|
||||
}
|
||||
else {
|
||||
const bool is_bezier_vertex_interp = (type == pxr::UsdGeomTokens->cubic &&
|
||||
@@ -310,7 +339,7 @@ void USDBasisCurvesReader::read_curve_sample(Curves *curves_id, const double mot
|
||||
|
||||
int cp_offset = 0;
|
||||
for (const int cp : IndexRange(point_count)) {
|
||||
radii.span[point_offset + cp] = usdWidths[usd_point_offset + cp_offset] / 2.0f;
|
||||
radii[point_offset + cp] = usdWidths[usd_point_offset + cp_offset] / 2.0f;
|
||||
cp_offset += 3;
|
||||
}
|
||||
|
||||
@@ -320,15 +349,14 @@ void USDBasisCurvesReader::read_curve_sample(Curves *curves_id, const double mot
|
||||
}
|
||||
else {
|
||||
for (const int i_point : curves.points_range()) {
|
||||
radii.span[i_point] = usdWidths[i_point] / 2.0f;
|
||||
radii[i_point] = usdWidths[i_point] / 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
radii.finish();
|
||||
}
|
||||
|
||||
read_custom_data(curves, motionSampleTime);
|
||||
this->read_velocities(curves, curve_prim_, motionSampleTime);
|
||||
this->read_custom_data(curves, motionSampleTime);
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
@@ -37,10 +37,13 @@ class USDCurvesReader : public USDGeomReader {
|
||||
USDMeshReadParams params,
|
||||
const char **r_err_str) override;
|
||||
|
||||
void read_velocities(bke::CurvesGeometry &curves,
|
||||
const pxr::UsdGeomCurves &usd_curves,
|
||||
const double motionSampleTime) const;
|
||||
void read_custom_data(bke::CurvesGeometry &curves, const double motionSampleTime) const;
|
||||
|
||||
virtual void read_curve_sample(Curves *curves_id, double motionSampleTime) = 0;
|
||||
virtual bool is_animated() const = 0;
|
||||
virtual void read_curve_sample(Curves *curves_id, double motionSampleTime) = 0;
|
||||
};
|
||||
|
||||
class USDBasisCurvesReader : public USDCurvesReader {
|
||||
@@ -60,8 +63,8 @@ class USDBasisCurvesReader : public USDCurvesReader {
|
||||
return bool(curve_prim_);
|
||||
}
|
||||
|
||||
void read_curve_sample(Curves *curves_id, double motionSampleTime) override;
|
||||
bool is_animated() const override;
|
||||
void read_curve_sample(Curves *curves_id, double motionSampleTime) override;
|
||||
};
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include <cstdint>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
#include <pxr/usd/usdGeom/basisCurves.h>
|
||||
#include <pxr/usd/usdGeom/curves.h>
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "BLI_array_utils.hh"
|
||||
#include "BLI_generic_virtual_array.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_span.hh"
|
||||
#include "BLI_virtual_array.hh"
|
||||
|
||||
@@ -73,14 +74,11 @@ pxr::UsdGeomBasisCurves USDCurvesWriter::DefineUsdGeomBasisCurves(pxr::VtValue c
|
||||
|
||||
static void populate_curve_widths(const bke::CurvesGeometry &curves, pxr::VtArray<float> &widths)
|
||||
{
|
||||
const bke::AttributeAccessor curve_attributes = curves.attributes();
|
||||
const bke::AttributeReader<float> radii = curve_attributes.lookup<float>("radius",
|
||||
bke::AttrDomain::Point);
|
||||
const VArray<float> radii = curves.radius();
|
||||
|
||||
widths.resize(radii.varray.size());
|
||||
|
||||
for (const int i : radii.varray.index_range()) {
|
||||
widths[i] = radii.varray[i] * 2.0f;
|
||||
widths.resize(radii.size());
|
||||
for (const int i : radii.index_range()) {
|
||||
widths[i] = radii[i] * 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,15 +346,15 @@ void USDCurvesWriter::set_writer_attributes(pxr::UsdGeomCurves &usd_curves,
|
||||
const pxr::TfToken interpolation)
|
||||
{
|
||||
pxr::UsdAttribute attr_points = usd_curves.CreatePointsAttr(pxr::VtValue(), true);
|
||||
usd_value_writer_.SetAttribute(attr_points, pxr::VtValue(verts), timecode);
|
||||
set_attribute(attr_points, verts, timecode, usd_value_writer_);
|
||||
|
||||
pxr::UsdAttribute attr_vertex_counts = usd_curves.CreateCurveVertexCountsAttr(pxr::VtValue(),
|
||||
true);
|
||||
usd_value_writer_.SetAttribute(attr_vertex_counts, pxr::VtValue(control_point_counts), timecode);
|
||||
set_attribute(attr_vertex_counts, control_point_counts, timecode, usd_value_writer_);
|
||||
|
||||
if (!widths.empty()) {
|
||||
pxr::UsdAttribute attr_widths = usd_curves.CreateWidthsAttr(pxr::VtValue(), true);
|
||||
usd_value_writer_.SetAttribute(attr_widths, pxr::VtValue(widths), timecode);
|
||||
set_attribute(attr_widths, widths, timecode, usd_value_writer_);
|
||||
|
||||
usd_curves.SetWidthsInterpolation(interpolation);
|
||||
}
|
||||
@@ -376,6 +374,33 @@ static std::optional<pxr::TfToken> convert_blender_domain_to_usd(
|
||||
}
|
||||
}
|
||||
|
||||
/* Excluded attributes are those which are handled through native USD concepts
|
||||
* and should not be exported as generic attributes. */
|
||||
static bool is_excluded_attr(StringRefNull name)
|
||||
{
|
||||
static const Set<StringRefNull> excluded_attrs = []() {
|
||||
Set<StringRefNull> set;
|
||||
set.add_new("position");
|
||||
set.add_new("radius");
|
||||
set.add_new("resolution");
|
||||
set.add_new("id");
|
||||
set.add_new("cyclic");
|
||||
set.add_new("curve_type");
|
||||
set.add_new("normal_mode");
|
||||
set.add_new("handle_left");
|
||||
set.add_new("handle_right");
|
||||
set.add_new("handle_type_left");
|
||||
set.add_new("handle_type_right");
|
||||
set.add_new("knots_mode");
|
||||
set.add_new("nurbs_order");
|
||||
set.add_new("nurbs_weight");
|
||||
set.add_new("velocity");
|
||||
return set;
|
||||
}();
|
||||
|
||||
return excluded_attrs.contains(name);
|
||||
}
|
||||
|
||||
void USDCurvesWriter::write_generic_data(const bke::CurvesGeometry &curves,
|
||||
const bke::AttributeIter &attr,
|
||||
const pxr::UsdGeomCurves &usd_curves)
|
||||
@@ -432,6 +457,25 @@ void USDCurvesWriter::write_uv_data(const bke::AttributeIter &attr,
|
||||
copy_blender_buffer_to_primvar<float2, pxr::GfVec2f>(buffer, timecode, pv_uv, usd_value_writer_);
|
||||
}
|
||||
|
||||
void USDCurvesWriter::write_velocities(const bke::CurvesGeometry &curves,
|
||||
const pxr::UsdGeomCurves &usd_curves)
|
||||
{
|
||||
const VArraySpan velocity = *curves.attributes().lookup<float3>("velocity",
|
||||
blender::bke::AttrDomain::Point);
|
||||
if (velocity.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Export per-vertex velocity vectors. */
|
||||
Span<pxr::GfVec3f> data = velocity.cast<pxr::GfVec3f>();
|
||||
pxr::VtVec3fArray usd_velocities;
|
||||
usd_velocities.assign(data.begin(), data.end());
|
||||
|
||||
pxr::UsdTimeCode timecode = get_export_time_code();
|
||||
pxr::UsdAttribute attr_vel = usd_curves.CreateVelocitiesAttr(pxr::VtValue(), true);
|
||||
set_attribute(attr_vel, usd_velocities, timecode, usd_value_writer_);
|
||||
}
|
||||
|
||||
void USDCurvesWriter::write_custom_data(const bke::CurvesGeometry &curves,
|
||||
const pxr::UsdGeomCurves &usd_curves)
|
||||
{
|
||||
@@ -440,16 +484,7 @@ void USDCurvesWriter::write_custom_data(const bke::CurvesGeometry &curves,
|
||||
attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
/* Skip "internal" Blender properties and attributes dealt with elsewhere. */
|
||||
if (iter.name[0] == '.' || bke::attribute_name_is_anonymous(iter.name) ||
|
||||
ELEM(iter.name,
|
||||
"position",
|
||||
"radius",
|
||||
"resolution",
|
||||
"id",
|
||||
"curve_type",
|
||||
"handle_left",
|
||||
"handle_right",
|
||||
"handle_type_left",
|
||||
"handle_type_right"))
|
||||
is_excluded_attr(iter.name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -591,11 +626,13 @@ void USDCurvesWriter::do_write(HierarchyContext &context)
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
|
||||
set_writer_attributes(*usd_curves, verts, control_point_counts, widths, timecode, interpolation);
|
||||
this->set_writer_attributes(
|
||||
*usd_curves, verts, control_point_counts, widths, timecode, interpolation);
|
||||
|
||||
assign_materials(context, *usd_curves);
|
||||
this->assign_materials(context, *usd_curves);
|
||||
|
||||
write_custom_data(curves, *usd_curves);
|
||||
this->write_velocities(curves, *usd_curves);
|
||||
this->write_custom_data(curves, *usd_curves);
|
||||
|
||||
auto prim = usd_curves->GetPrim();
|
||||
write_id_properties(prim, curves_id->id, timecode);
|
||||
|
||||
@@ -50,6 +50,8 @@ class USDCurvesWriter final : public USDAbstractWriter {
|
||||
|
||||
void write_uv_data(const bke::AttributeIter &attr, const pxr::UsdGeomCurves &usd_curves);
|
||||
|
||||
void write_velocities(const bke::CurvesGeometry &curves, const pxr::UsdGeomCurves &usd_curves);
|
||||
|
||||
void write_custom_data(const blender::bke::CurvesGeometry &curves,
|
||||
const pxr::UsdGeomCurves &usd_curves);
|
||||
};
|
||||
|
||||
Submodule tests/data updated: 355a198573...b814bc10c0
@@ -684,6 +684,35 @@ class USDExportTest(AbstractUSDTest):
|
||||
self.assertEqual(UsdGeom.Boundable(points3).GetExtentAttr().GetTimeSamples(), sparse_frames)
|
||||
self.assertEqual(UsdGeom.Boundable(points4).GetExtentAttr().GetTimeSamples(), [])
|
||||
|
||||
#
|
||||
# Validate BasisCurve data
|
||||
#
|
||||
curves1 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane1/curves1/Curves"))
|
||||
curves2 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane2/curves2/Curves"))
|
||||
curves3 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane3/curves3/Curves"))
|
||||
curves4 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane4/curves4/Curves"))
|
||||
|
||||
# Positions (should be sparsely written)
|
||||
self.assertEqual(curves1.GetPointsAttr().GetTimeSamples(), sparse_frames)
|
||||
self.assertEqual(curves2.GetPointsAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves3.GetPointsAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves4.GetPointsAttr().GetTimeSamples(), [])
|
||||
# Velocity (should be sparsely written)
|
||||
self.assertEqual(curves1.GetVelocitiesAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
|
||||
self.assertEqual(curves3.GetVelocitiesAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves4.GetVelocitiesAttr().GetTimeSamples(), [])
|
||||
# Radius (should be sparsely written)
|
||||
self.assertEqual(curves1.GetWidthsAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves2.GetWidthsAttr().GetTimeSamples(), [])
|
||||
self.assertEqual(curves3.GetWidthsAttr().GetTimeSamples(), sparse_frames)
|
||||
self.assertEqual(curves4.GetWidthsAttr().GetTimeSamples(), [])
|
||||
# Regular primvar (should be sparsely written)
|
||||
self.assertEqual(UsdGeom.PrimvarsAPI(curves1).GetPrimvar("test").GetTimeSamples(), [])
|
||||
self.assertEqual(UsdGeom.PrimvarsAPI(curves2).GetPrimvar("test").GetTimeSamples(), [])
|
||||
self.assertEqual(UsdGeom.PrimvarsAPI(curves3).GetPrimvar("test").GetTimeSamples(), [])
|
||||
self.assertEqual(UsdGeom.PrimvarsAPI(curves4).GetPrimvar("test").GetTimeSamples(), sparse_frames)
|
||||
|
||||
def test_export_mesh_subd(self):
|
||||
"""Test exporting Subdivision Surface attributes and values"""
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_mesh_subd.blend"))
|
||||
|
||||
@@ -1391,6 +1391,68 @@ class USDImportTest(AbstractUSDTest):
|
||||
usd_test_data,
|
||||
f"Frame {frame}: {name} test attributes do not match")
|
||||
|
||||
#
|
||||
# Validate Curves data
|
||||
#
|
||||
blender_curves = [
|
||||
bpy.data.objects["Curves"],
|
||||
bpy.data.objects["Curves.001"],
|
||||
bpy.data.objects["Curves.002"],
|
||||
bpy.data.objects["Curves.003"]]
|
||||
usd_curves = [UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane1/curves1/Curves")),
|
||||
UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane2/curves2/Curves")),
|
||||
UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane3/curves3/Curves")),
|
||||
UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane4/curves4/Curves"))]
|
||||
curves_num = len(blender_curves)
|
||||
|
||||
# Workaround: GeometrySet processing loses the data-block name on export. This is why the
|
||||
# .001 etc. names are being used above. Since we need the order of Blender objects to match
|
||||
# the order of USD prims, sort by the Y location to make them match in our test setup.
|
||||
blender_curves.sort(key=lambda ob: ob.parent.location.y)
|
||||
|
||||
# A MeshSequenceCache modifier should be present on every imported object
|
||||
for i in range(0, curves_num):
|
||||
self.assertTrue(len(blender_curves[i].modifiers) == 1 and blender_curves[i].modifiers[0].type ==
|
||||
'MESH_SEQUENCE_CACHE', f"{blender_curves[i].name} has incorrect modifiers")
|
||||
|
||||
# Compare Blender and USD data against each other for every frame
|
||||
for frame in range(1, 16):
|
||||
bpy.context.scene.frame_set(frame)
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
for i in range(0, mesh_num):
|
||||
blender_curves[i] = blender_curves[i].evaluated_get(depsgraph)
|
||||
|
||||
# Check positions, velocity, radius, and test data
|
||||
for i in range(0, mesh_num):
|
||||
blender_pos_data = [self.round_vector(d.vector)
|
||||
for d in blender_curves[i].data.attributes["position"].data]
|
||||
blender_vel_data = [self.round_vector(d.vector)
|
||||
for d in blender_curves[i].data.attributes["velocity"].data]
|
||||
blender_radius_data = [round(d.value, 5) for d in blender_curves[i].data.attributes["radius"].data]
|
||||
blender_test_data = [round(d.value, 5) for d in blender_curves[i].data.attributes["test"].data]
|
||||
usd_pos_data = [self.round_vector(d) for d in usd_curves[i].GetPointsAttr().Get(frame)]
|
||||
usd_vel_data = [self.round_vector(d) for d in usd_curves[i].GetVelocitiesAttr().Get(frame)]
|
||||
usd_radius_data = [round(d / 2, 5) for d in usd_curves[i].GetWidthsAttr().Get(frame)]
|
||||
usd_test_data = [round(d, 5) for d in UsdGeom.PrimvarsAPI(usd_curves[i]).GetPrimvar("test").Get(frame)]
|
||||
|
||||
name = usd_curves[i].GetPath().GetParentPath().name
|
||||
self.assertEqual(
|
||||
blender_pos_data,
|
||||
usd_pos_data,
|
||||
f"Frame {frame}: {name} positions do not match")
|
||||
self.assertEqual(
|
||||
blender_vel_data,
|
||||
usd_vel_data,
|
||||
f"Frame {frame}: {name} velocities do not match")
|
||||
self.assertEqual(
|
||||
blender_radius_data,
|
||||
usd_radius_data,
|
||||
f"Frame {frame}: {name} radii do not match")
|
||||
self.assertEqual(
|
||||
blender_test_data,
|
||||
usd_test_data,
|
||||
f"Frame {frame}: {name} test attributes do not match")
|
||||
|
||||
def test_import_shapes(self):
|
||||
"""Test importing USD Shape prims with time-varying attributes."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user