diff --git a/source/blender/io/usd/intern/usd_reader_curve.cc b/source/blender/io/usd/intern/usd_reader_curve.cc index a3df99c618d..88aa120592b 100644 --- a/source/blender/io/usd/intern/usd_reader_curve.cc +++ b/source/blender/io/usd/intern/usd_reader_curve.cc @@ -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 velocity = + attributes.lookup_or_add_for_write_only_span("velocity", bke::AttrDomain::Point); + + Span usd_data(velocities.data(), velocities.size()); + velocity.span.copy_from(usd_data.cast()); + 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 radii = attributes.lookup_or_add_for_write_only_span( - "radius", bke::AttrDomain::Point); - + MutableSpan 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 diff --git a/source/blender/io/usd/intern/usd_reader_curve.hh b/source/blender/io/usd/intern/usd_reader_curve.hh index ea869fccf79..9f5142072d2 100644 --- a/source/blender/io/usd/intern/usd_reader_curve.hh +++ b/source/blender/io/usd/intern/usd_reader_curve.hh @@ -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 diff --git a/source/blender/io/usd/intern/usd_writer_curves.cc b/source/blender/io/usd/intern/usd_writer_curves.cc index c1f9ee3756d..9355dfdc988 100644 --- a/source/blender/io/usd/intern/usd_writer_curves.cc +++ b/source/blender/io/usd/intern/usd_writer_curves.cc @@ -2,8 +2,8 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ +#include #include -#include #include #include @@ -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 &widths) { - const bke::AttributeAccessor curve_attributes = curves.attributes(); - const bke::AttributeReader radii = curve_attributes.lookup("radius", - bke::AttrDomain::Point); + const VArray 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 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 excluded_attrs = []() { + Set 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(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("velocity", + blender::bke::AttrDomain::Point); + if (velocity.is_empty()) { + return; + } + + /* Export per-vertex velocity vectors. */ + Span data = velocity.cast(); + 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); diff --git a/source/blender/io/usd/intern/usd_writer_curves.hh b/source/blender/io/usd/intern/usd_writer_curves.hh index 2b955c14287..c21a58ce196 100644 --- a/source/blender/io/usd/intern/usd_writer_curves.hh +++ b/source/blender/io/usd/intern/usd_writer_curves.hh @@ -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); }; diff --git a/tests/data b/tests/data index 355a198573d..b814bc10c02 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 355a198573d64b7e493a7c6101df4068ca83dbeb +Subproject commit b814bc10c0234c696a38d42b24e5f9d20de27cf6 diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 513fe2a399b..304bc6ae6ae 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -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")) diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 7e392db4efc..7d36dc015e1 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -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."""