diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index 2bf8f7a3925..cc1a1e4052e 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -242,7 +242,6 @@ endif() if(WITH_GTESTS) set(TEST_SRC - tests/usd_curves_test.cc tests/usd_export_test.cc tests/usd_stage_creation_test.cc tests/usd_usdz_export_test.cc diff --git a/source/blender/io/usd/intern/usd_writer_abstract.cc b/source/blender/io/usd/intern/usd_writer_abstract.cc index 5d1939c861f..05c17a1c272 100644 --- a/source/blender/io/usd/intern/usd_writer_abstract.cc +++ b/source/blender/io/usd/intern/usd_writer_abstract.cc @@ -2,6 +2,7 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "usd_writer_abstract.hh" +#include "usd_attribute_utils.hh" #include "usd_utils.hh" #include "usd_writer_material.hh" @@ -10,9 +11,9 @@ #include #include "BKE_customdata.hh" -#include "BKE_report.hh" #include "BLI_assert.h" +#include "BLI_bounds_types.hh" #include "DNA_mesh_types.h" @@ -405,26 +406,36 @@ void USDAbstractWriter::write_user_properties(const pxr::UsdPrim &prim, } } -void USDAbstractWriter::author_extent(const pxr::UsdTimeCode timecode, pxr::UsdGeomBoundable &prim) +void USDAbstractWriter::author_extent(const pxr::UsdGeomBoundable &boundable, + const pxr::UsdTimeCode timecode) { /* Do not use any existing `extentsHint` that may be authored, instead recompute the extent when * authoring it. */ const bool useExtentsHint = false; const pxr::TfTokenVector includedPurposes{pxr::UsdGeomTokens->default_}; pxr::UsdGeomBBoxCache bboxCache(timecode, includedPurposes, useExtentsHint); - pxr::GfBBox3d bounds = bboxCache.ComputeLocalBound(prim.GetPrim()); - if (pxr::GfBBox3d() == bounds) { - /* This will occur, for example, if a mesh does not have any vertices. */ - BKE_reportf(reports(), - RPT_WARNING, - "USD Export: no bounds could be computed for %s", - prim.GetPrim().GetName().GetText()); - return; + pxr::GfBBox3d bounds = bboxCache.ComputeLocalBound(boundable.GetPrim()); + + /* Note: An empty 'bounds' is still valid (e.g. a mesh with no vertices). */ + pxr::VtArray extent{pxr::GfVec3f(bounds.GetRange().GetMin()), + pxr::GfVec3f(bounds.GetRange().GetMax())}; + + pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true); + set_attribute(attr_extent, extent, timecode, usd_value_writer_); +} + +void USDAbstractWriter::author_extent(const pxr::UsdGeomBoundable &boundable, + const std::optional> &bounds, + const pxr::UsdTimeCode timecode) +{ + pxr::VtArray extent(2); + if (bounds) { + extent[0].Set(bounds->min); + extent[1].Set(bounds->max); } - pxr::VtArray extent{(pxr::GfVec3f)bounds.GetRange().GetMin(), - (pxr::GfVec3f)bounds.GetRange().GetMax()}; - prim.CreateExtentAttr().Set(extent); + pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true); + set_attribute(attr_extent, extent, timecode, usd_value_writer_); } } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_abstract.hh b/source/blender/io/usd/intern/usd_writer_abstract.hh index 890195bcb48..dfbaa1d0dfd 100644 --- a/source/blender/io/usd/intern/usd_writer_abstract.hh +++ b/source/blender/io/usd/intern/usd_writer_abstract.hh @@ -21,6 +21,10 @@ struct Material; struct ReportList; +namespace blender { +template struct Bounds; +} + namespace blender::io::usd { using blender::io::AbstractHierarchyWriter; @@ -103,7 +107,14 @@ class USDAbstractWriter : public AbstractHierarchyWriter { * * TODO: also provide method for authoring extentsHint on every prim in a hierarchy. */ - virtual void author_extent(const pxr::UsdTimeCode timecode, pxr::UsdGeomBoundable &prim); + void author_extent(const pxr::UsdGeomBoundable &boundable, const pxr::UsdTimeCode timecode); + + /** + * Author the `extent` attribute for a boundable prim given the Blender `bounds`. + */ + void author_extent(const pxr::UsdGeomBoundable &boundable, + const std::optional> &bounds, + const pxr::UsdTimeCode timecode); }; } // 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 e4fde219387..58978a575c5 100644 --- a/source/blender/io/usd/intern/usd_writer_curves.cc +++ b/source/blender/io/usd/intern/usd_writer_curves.cc @@ -636,6 +636,8 @@ void USDCurvesWriter::do_write(HierarchyContext &context) auto prim = usd_curves->GetPrim(); write_id_properties(prim, curves_id->id, timecode); + + this->author_extent(*usd_curves, curves.bounds_min_max(), timecode); } void USDCurvesWriter::assign_materials(const HierarchyContext &context, diff --git a/source/blender/io/usd/intern/usd_writer_hair.cc b/source/blender/io/usd/intern/usd_writer_hair.cc index 410ae02f7d7..5840c299012 100644 --- a/source/blender/io/usd/intern/usd_writer_hair.cc +++ b/source/blender/io/usd/intern/usd_writer_hair.cc @@ -67,7 +67,7 @@ void USDHairWriter::do_write(HierarchyContext &context) write_id_properties(prim, psys->part->id, timecode); } - this->author_extent(timecode, curves); + this->author_extent(curves, timecode); } bool USDHairWriter::check_is_animated(const HierarchyContext & /*context*/) const diff --git a/source/blender/io/usd/intern/usd_writer_light.cc b/source/blender/io/usd/intern/usd_writer_light.cc index 953e995a27b..c6c85f4bb50 100644 --- a/source/blender/io/usd/intern/usd_writer_light.cc +++ b/source/blender/io/usd/intern/usd_writer_light.cc @@ -25,22 +25,6 @@ bool USDLightWriter::is_supported(const HierarchyContext * /*context*/) const return true; } -static void set_light_extents(const pxr::UsdPrim &prim, - const pxr::UsdTimeCode time, - pxr::UsdUtilsSparseValueWriter usd_value_writer) -{ - if (auto boundable = pxr::UsdGeomBoundable(prim)) { - pxr::VtArray extent; - pxr::UsdGeomBoundable::ComputeExtentFromPlugins(boundable, time, &extent); - pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true); - - set_attribute(attr_extent, extent, time, usd_value_writer); - } - - /* We're intentionally not setting an error on non-boundable lights, - * because overly noisy errors are annoying. */ -} - void USDLightWriter::do_write(HierarchyContext &context) { pxr::UsdStageRefPtr stage = usd_export_context_.stage; @@ -178,7 +162,10 @@ void USDLightWriter::do_write(HierarchyContext &context) pxr::UsdPrim prim = usd_light_api.GetPrim(); write_id_properties(prim, light->id, timecode); - set_light_extents(prim, timecode, usd_value_writer_); + /* Only a subset of light types are "boundable". */ + if (auto boundable = pxr::UsdGeomBoundable(prim)) { + this->author_extent(boundable, timecode); + } } } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_mesh.cc b/source/blender/io/usd/intern/usd_writer_mesh.cc index 5e1d423c8b0..a92f1859b6a 100644 --- a/source/blender/io/usd/intern/usd_writer_mesh.cc +++ b/source/blender/io/usd/intern/usd_writer_mesh.cc @@ -416,6 +416,8 @@ void USDGenericMeshWriter::write_mesh(HierarchyContext &context, write_normals(mesh, usd_mesh); } + this->author_extent(usd_mesh, mesh->bounds_min_max(), timecode); + /* TODO(Sybren): figure out what happens when the face groups change. */ if (frame_has_been_written_) { return; @@ -428,14 +430,6 @@ void USDGenericMeshWriter::write_mesh(HierarchyContext &context, if (usd_export_context_.export_params.export_materials) { assign_materials(context, usd_mesh, usd_mesh_data.face_groups); } - - /* Blender grows its bounds cache to cover animated meshes, so only author once. */ - if (const std::optional> bounds = mesh->bounds_min_max()) { - pxr::VtArray extent{ - pxr::GfVec3f{bounds->min[0], bounds->min[1], bounds->min[2]}, - pxr::GfVec3f{bounds->max[0], bounds->max[1], bounds->max[2]}}; - usd_mesh.CreateExtentAttr().Set(extent); - } } pxr::TfToken USDGenericMeshWriter::get_subdiv_scheme(const SubsurfModifierData *subsurfData) diff --git a/source/blender/io/usd/intern/usd_writer_points.cc b/source/blender/io/usd/intern/usd_writer_points.cc index 1ffa6d68188..e4de9a6a2ea 100644 --- a/source/blender/io/usd/intern/usd_writer_points.cc +++ b/source/blender/io/usd/intern/usd_writer_points.cc @@ -55,8 +55,7 @@ void USDPointsWriter::do_write(HierarchyContext &context) this->write_velocities(points, usd_points, timecode); this->write_custom_data(points, usd_points, timecode); - const pxr::UsdPrim usd_prim = usd_points.GetPrim(); - this->set_extents(usd_prim, timecode); + this->author_extent(usd_points, points->bounds_min_max(), timecode); } static std::optional convert_blender_domain_to_usd( @@ -143,19 +142,4 @@ void USDPointsWriter::write_velocities(const PointCloud *points, usd_value_writer_.SetAttribute(attr_vel, usd_velocities, timecode); } -void USDPointsWriter::set_extents(const pxr::UsdPrim &prim, const pxr::UsdTimeCode timecode) -{ - pxr::UsdGeomBoundable boundable(prim); - - pxr::VtArray extent; - pxr::UsdGeomBoundable::ComputeExtentFromPlugins(boundable, timecode, &extent); - - pxr::UsdAttribute attr_extent = boundable.CreateExtentAttr(pxr::VtValue(), true); - if (!attr_extent.HasValue()) { - attr_extent.Set(extent, pxr::UsdTimeCode::Default()); - } - - usd_value_writer_.SetAttribute(attr_extent, extent, timecode); -} - } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_points.hh b/source/blender/io/usd/intern/usd_writer_points.hh index 99cb3fd5a4b..24b19839e33 100644 --- a/source/blender/io/usd/intern/usd_writer_points.hh +++ b/source/blender/io/usd/intern/usd_writer_points.hh @@ -37,8 +37,6 @@ class USDPointsWriter final : public USDAbstractWriter { void write_velocities(const PointCloud *points, const pxr::UsdGeomPoints &usd_points, pxr::UsdTimeCode timecode); - - void set_extents(const pxr::UsdPrim &prim, pxr::UsdTimeCode timecode); }; } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_volume.cc b/source/blender/io/usd/intern/usd_writer_volume.cc index fd5d4db7880..bd3f2452832 100644 --- a/source/blender/io/usd/intern/usd_writer_volume.cc +++ b/source/blender/io/usd/intern/usd_writer_volume.cc @@ -121,17 +121,7 @@ void USDVolumeWriter::do_write(HierarchyContext &context) usd_volume.CreateFieldRelationship(pxr::TfToken(grid_id), grid_path); } - if (const std::optional> bounds = BKE_volume_min_max(volume)) { - pxr::VtArray volume_extent = {pxr::GfVec3f(&bounds->min.x), - pxr::GfVec3f(&bounds->max.x)}; - - pxr::UsdAttribute attr_extent = usd_volume.CreateExtentAttr(pxr::VtValue(), true); - if (!attr_extent.HasValue()) { - attr_extent.Set(volume_extent, pxr::UsdTimeCode::Default()); - } - - usd_value_writer_.SetAttribute(attr_extent, volume_extent, timecode); - } + this->author_extent(usd_volume, BKE_volume_min_max(volume), timecode); BKE_volume_unload(volume); } diff --git a/source/blender/io/usd/tests/usd_curves_test.cc b/source/blender/io/usd/tests/usd_curves_test.cc deleted file mode 100644 index 0fece3e6b2e..00000000000 --- a/source/blender/io/usd/tests/usd_curves_test.cc +++ /dev/null @@ -1,346 +0,0 @@ -/* SPDX-FileCopyrightText: 2023 Blender Authors - * - * SPDX-License-Identifier: GPL-2.0-or-later */ - -#include "testing/testing.h" -#include "tests/blendfile_loading_base_test.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "DNA_material_types.h" -#include "DNA_node_types.h" - -#include "BKE_context.hh" -#include "BKE_lib_id.hh" -#include "BKE_main.hh" -#include "BKE_mesh.hh" -#include "BKE_node.hh" -#include "BLI_fileops.h" -#include "BLI_math_vector_types.hh" -#include "BLI_path_utils.hh" -#include "BLO_readfile.hh" - -#include "BKE_node_runtime.hh" - -#include "DEG_depsgraph.hh" - -#include "WM_api.hh" - -#include "usd.hh" - -namespace blender::io::usd { - -const StringRefNull usd_curves_test_filename = "usd/usd_curves_test.blend"; -const StringRefNull output_filename = "usd/output.usda"; - -static void check_catmullRom_curve(const pxr::UsdPrim prim, - const bool is_periodic, - const int vertex_count); -static void check_bezier_curve(const pxr::UsdPrim bezier_prim, - const bool is_periodic, - const int vertex_count); -static void check_nurbs_curve(const pxr::UsdPrim nurbs_prim, - const int vertex_count, - const int knots_count, - const int order); -static void check_nurbs_circle(const pxr::UsdPrim nurbs_prim, - const int vertex_count, - const int knots_count, - const int order); - -class UsdCurvesTest : public BlendfileLoadingBaseTest { - protected: - bContext *context = nullptr; - - public: - bool load_file_and_depsgraph(const StringRefNull &filepath, - const eEvaluationMode eval_mode = DAG_EVAL_VIEWPORT) - { - if (!blendfile_load(filepath.c_str())) { - return false; - } - depsgraph_create(eval_mode); - - context = CTX_create(); - CTX_data_main_set(context, bfile->main); - CTX_data_scene_set(context, bfile->curscene); - - return true; - } - - virtual void SetUp() override - { - BlendfileLoadingBaseTest::SetUp(); - } - - virtual void TearDown() override - { - BlendfileLoadingBaseTest::TearDown(); - CTX_free(context); - context = nullptr; - - if (BLI_exists(output_filename.c_str())) { - BLI_delete(output_filename.c_str(), false, false); - } - } -}; - -TEST_F(UsdCurvesTest, usd_export_curves) -{ - if (!load_file_and_depsgraph(usd_curves_test_filename)) { - ADD_FAILURE(); - return; - } - - /* File sanity check. */ - EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 6); - - USDExportParams params; - - const bool result = USD_export(context, output_filename.c_str(), ¶ms, false, nullptr); - EXPECT_TRUE(result) << "USD export should succed."; - - pxr::UsdStageRefPtr stage = pxr::UsdStage::Open(output_filename); - ASSERT_NE(stage, nullptr) << "Stage should not be null after opening usd file."; - - { - std::string prim_name = pxr::TfMakeValidIdentifier("BezierCurve"); - pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/BezierCurve/" + prim_name)); - EXPECT_TRUE(test_prim.IsValid()); - check_bezier_curve(test_prim, false, 7); - } - - { - std::string prim_name = pxr::TfMakeValidIdentifier("BezierCircle"); - pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/BezierCircle/" + prim_name)); - EXPECT_TRUE(test_prim.IsValid()); - check_bezier_curve(test_prim, true, 12); - } - - { - std::string prim_name = pxr::TfMakeValidIdentifier("NurbsCurve"); - pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/NurbsCurve/" + prim_name)); - EXPECT_TRUE(test_prim.IsValid()); - check_nurbs_curve(test_prim, 6, 20, 4); - } - - { - std::string prim_name = pxr::TfMakeValidIdentifier("NurbsCircle"); - pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/NurbsCircle/" + prim_name)); - EXPECT_TRUE(test_prim.IsValid()); - check_nurbs_circle(test_prim, 8, 13, 3); - } - - { - std::string prim_name = pxr::TfMakeValidIdentifier("Curves"); - pxr::UsdPrim test_prim = stage->GetPrimAtPath(pxr::SdfPath("/Cube/Curves/" + prim_name)); - EXPECT_TRUE(test_prim.IsValid()); - check_catmullRom_curve(test_prim, false, 8); - } -} - -/** - * Test that the provided prim is a valid catmullRom curve. We also check it matches the expected - * wrap type, and has the expected number of vertices. - */ -static void check_catmullRom_curve(const pxr::UsdPrim prim, - const bool is_periodic, - const int vertex_count) -{ - auto curve = pxr::UsdGeomBasisCurves(prim); - - pxr::VtValue basis; - pxr::UsdAttribute basis_attr = curve.GetBasisAttr(); - basis_attr.Get(&basis); - auto basis_token = basis.Get(); - - EXPECT_EQ(basis_token, pxr::UsdGeomTokens->catmullRom) - << "Basis token should be catmullRom for catmullRom curve"; - - pxr::VtValue type; - pxr::UsdAttribute type_attr = curve.GetTypeAttr(); - type_attr.Get(&type); - auto type_token = type.Get(); - - EXPECT_EQ(type_token, pxr::UsdGeomTokens->cubic) - << "Type token should be cubic for catmullRom curve"; - - pxr::VtValue wrap; - pxr::UsdAttribute wrap_attr = curve.GetWrapAttr(); - wrap_attr.Get(&wrap); - auto wrap_token = wrap.Get(); - - if (is_periodic) { - EXPECT_EQ(wrap_token, pxr::UsdGeomTokens->periodic) - << "Wrap token should be periodic for periodic curve"; - } - else { - EXPECT_EQ(wrap_token, pxr::UsdGeomTokens->pinned) - << "Wrap token should be pinned for nonperiodic catmullRom curve"; - } - - pxr::UsdAttribute vert_count_attr = curve.GetCurveVertexCountsAttr(); - pxr::VtArray vert_counts; - vert_count_attr.Get(&vert_counts); - - EXPECT_EQ(vert_counts.size(), 3) << "Prim should contain verts for three curves"; - EXPECT_EQ(vert_counts[0], vertex_count) << "Curve 0 should have " << vertex_count << " verts."; - EXPECT_EQ(vert_counts[1], vertex_count) << "Curve 1 should have " << vertex_count << " verts."; - EXPECT_EQ(vert_counts[2], vertex_count) << "Curve 2 should have " << vertex_count << " verts."; -} - -/** - * Test that the provided prim is a valid bezier curve. We also check it matches the expected - * wrap type, and has the expected number of vertices. - */ -static void check_bezier_curve(const pxr::UsdPrim bezier_prim, - const bool is_periodic, - const int vertex_count) -{ - auto curve = pxr::UsdGeomBasisCurves(bezier_prim); - - pxr::VtValue basis; - pxr::UsdAttribute basis_attr = curve.GetBasisAttr(); - basis_attr.Get(&basis); - auto basis_token = basis.Get(); - - EXPECT_EQ(basis_token, pxr::UsdGeomTokens->bezier) - << "Basis token should be bezier for bezier curve"; - - pxr::VtValue type; - pxr::UsdAttribute type_attr = curve.GetTypeAttr(); - type_attr.Get(&type); - auto type_token = type.Get(); - - EXPECT_EQ(type_token, pxr::UsdGeomTokens->cubic) - << "Type token should be cubic for bezier curve"; - - pxr::VtValue wrap; - pxr::UsdAttribute wrap_attr = curve.GetWrapAttr(); - wrap_attr.Get(&wrap); - auto wrap_token = wrap.Get(); - - if (is_periodic) { - EXPECT_EQ(wrap_token, pxr::UsdGeomTokens->periodic) - << "Wrap token should be periodic for periodic curve"; - } - else { - EXPECT_EQ(wrap_token, pxr::UsdGeomTokens->nonperiodic) - << "Wrap token should be nonperiodic for nonperiodic curve"; - } - - auto widths_interp_token = curve.GetWidthsInterpolation(); - EXPECT_EQ(widths_interp_token, pxr::UsdGeomTokens->varying) - << "Widths interpolation token should be varying for bezier curve"; - - pxr::UsdAttribute vert_count_attr = curve.GetCurveVertexCountsAttr(); - pxr::VtArray vert_counts; - vert_count_attr.Get(&vert_counts); - - EXPECT_EQ(vert_counts.size(), 1) << "Prim should only contains verts for a single curve"; - EXPECT_EQ(vert_counts[0], vertex_count) << "Curve should have " << vertex_count << " verts."; -} - -/** - * Test that the provided prim is a valid NURBS curve. We also check it matches the expected - * wrap type, and has the expected number of vertices. For NURBS, we also validate that the knots - * layout matches the expected layout for periodic/non-periodic curves according to the USD spec. - */ -static void check_nurbs_curve(const pxr::UsdPrim nurbs_prim, - const int vertex_count, - const int knots_count, - const int order) -{ - auto curve = pxr::UsdGeomNurbsCurves(nurbs_prim); - - pxr::UsdAttribute order_attr = curve.GetOrderAttr(); - pxr::VtArray orders; - order_attr.Get(&orders); - - EXPECT_EQ(orders.size(), 2) << "Prim should contain orders for two curves"; - EXPECT_EQ(orders[0], order) << "Curves should have order " << order; - EXPECT_EQ(orders[1], order) << "Curves should have order " << order; - - pxr::UsdAttribute knots_attr = curve.GetKnotsAttr(); - pxr::VtArray knots; - knots_attr.Get(&knots); - - EXPECT_EQ(knots.size(), knots_count) << "Curve should have " << knots_count << " knots."; - for (int i = 0; i < 2; i++) { - int zeroth_knot_index = i * (knots_count / 2); - - EXPECT_EQ(knots[zeroth_knot_index], knots[zeroth_knot_index + 1]) - << "NURBS curve should satisfy this knots rule for a nonperiodic curve"; - EXPECT_EQ(knots[knots.size() - 1], knots[knots.size() - 2]) - << "NURBS curve should satisfy this knots rule for a nonperiodic curve"; - } - - auto widths_interp_token = curve.GetWidthsInterpolation(); - EXPECT_EQ(widths_interp_token, pxr::UsdGeomTokens->vertex) - << "Widths interpolation token should be vertex for NURBS curve"; - - pxr::UsdAttribute vert_count_attr = curve.GetCurveVertexCountsAttr(); - pxr::VtArray vert_counts; - vert_count_attr.Get(&vert_counts); - - EXPECT_EQ(vert_counts.size(), 2) << "Prim should contain verts for two curves"; - EXPECT_EQ(vert_counts[0], vertex_count) << "Curve should have " << vertex_count << " verts."; - EXPECT_EQ(vert_counts[1], vertex_count) << "Curve should have " << vertex_count << " verts."; -} - -/** - * Test that the provided prim is a valid NURBS curve. We also check it matches the expected - * wrap type, and has the expected number of vertices. For NURBS, we also validate that the knots - * layout matches the expected layout for periodic/non-periodic curves according to the USD spec. - */ -static void check_nurbs_circle(const pxr::UsdPrim nurbs_prim, - const int vertex_count, - const int knots_count, - const int order) -{ - auto curve = pxr::UsdGeomNurbsCurves(nurbs_prim); - - pxr::UsdAttribute order_attr = curve.GetOrderAttr(); - pxr::VtArray orders; - order_attr.Get(&orders); - - EXPECT_EQ(orders.size(), 1) << "Prim should contain orders for one curves"; - EXPECT_EQ(orders[0], order) << "Curve should have order " << order; - - pxr::UsdAttribute knots_attr = curve.GetKnotsAttr(); - pxr::VtArray knots; - knots_attr.Get(&knots); - - EXPECT_EQ(knots.size(), knots_count) << "Curve should have " << knots_count << " knots."; - - EXPECT_EQ(knots[0], knots[1] - (knots[knots.size() - 2] - knots[knots.size() - 3])) - << "NURBS curve should satisfy this knots rule for a periodic curve"; - EXPECT_EQ(knots[knots.size() - 1], knots[knots.size() - 2] + (knots[2] - knots[1])) - << "NURBS curve should satisfy this knots rule for a periodic curve"; - - auto widths_interp_token = curve.GetWidthsInterpolation(); - EXPECT_EQ(widths_interp_token, pxr::UsdGeomTokens->vertex) - << "Widths interpolation token should be vertex for NURBS curve"; - - pxr::UsdAttribute vert_count_attr = curve.GetCurveVertexCountsAttr(); - pxr::VtArray vert_counts; - vert_count_attr.Get(&vert_counts); - - EXPECT_EQ(vert_counts.size(), 1) << "Prim should contain verts for one curve"; - EXPECT_EQ(vert_counts[0], vertex_count) << "Curve should have " << vertex_count << " verts."; -} - -} // namespace blender::io::usd diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 304bc6ae6ae..a3589e98137 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -649,6 +649,10 @@ class USDExportTest(AbstractUSDTest): self.assertEqual(UsdGeom.PrimvarsAPI(mesh1).GetPrimvar("test").GetTimeSamples(), []) self.assertEqual(UsdGeom.PrimvarsAPI(mesh2).GetPrimvar("test").GetTimeSamples(), []) self.assertEqual(UsdGeom.PrimvarsAPI(mesh3).GetPrimvar("test").GetTimeSamples(), sparse_frames) + # Extents of the mesh (should be sparsely written) + self.assertEqual(UsdGeom.Boundable(mesh1).GetExtentAttr().GetTimeSamples(), sparse_frames) + self.assertEqual(UsdGeom.Boundable(mesh2).GetExtentAttr().GetTimeSamples(), []) + self.assertEqual(UsdGeom.Boundable(mesh3).GetExtentAttr().GetTimeSamples(), []) # # Validate PointCloud data @@ -836,6 +840,71 @@ class USDExportTest(AbstractUSDTest): self.assertEqual(len(indices2), 15) self.assertNotEqual(indices1, indices2) + def test_export_curves(self): + """Test exporting Curve types""" + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_curves_test.blend")) + export_path = self.tempdir / "usd_curves_test.usda" + self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER") + + stage = Usd.Stage.Open(str(export_path)) + + def check_basis_curve(prim, basis, curve_type, wrap, vert_counts, extent): + self.assertEqual(prim.GetBasisAttr().Get(), basis) + self.assertEqual(prim.GetTypeAttr().Get(), curve_type) + self.assertEqual(prim.GetWrapAttr().Get(), wrap) + self.assertEqual(prim.GetWidthsInterpolation(), "varying" if basis == "bezier" else "vertex") + self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts) + usd_extent = prim.GetExtentAttr().Get() + self.assertEqual(self.round_vector(usd_extent[0]), extent[0]) + self.assertEqual(self.round_vector(usd_extent[1]), extent[1]) + + def check_nurbs_curve(prim, cyclic, orders, vert_counts, knots_count, extent): + self.assertEqual(prim.GetOrderAttr().Get(), orders) + self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts) + self.assertEqual(prim.GetWidthsInterpolation(), "vertex") + knots = prim.GetKnotsAttr().Get() + usd_extent = prim.GetExtentAttr().Get() + self.assertEqual(self.round_vector(usd_extent[0]), extent[0]) + self.assertEqual(self.round_vector(usd_extent[1]), extent[1]) + + curve_count = len(vert_counts) + self.assertEqual(len(knots), knots_count * curve_count) + if not cyclic: + for i in range(0, curve_count): + zeroth_knot = i * len(knots) // curve_count + self.assertEqual(knots[zeroth_knot], knots[zeroth_knot + 1], "Knots start rule violated") + self.assertEqual( + knots[zeroth_knot + knots_count - 1], + knots[zeroth_knot + knots_count - 2], + "Knots end rule violated") + else: + self.assertEqual(curve_count, 1, "Validation is only correct for 1 cyclic curve currently") + self.assertEqual( + knots[0], knots[1] - (knots[knots_count - 2] - knots[knots_count - 3]), "Knots rule violated") + self.assertEqual( + knots[knots_count - 1], knots[knots_count - 2] + (knots[2] - knots[1]), "Knots rule violated") + + # Contains 3 CatmullRom curves + curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/Cube/Curves/Curves")) + check_basis_curve( + curve, "catmullRom", "cubic", "pinned", [8, 8, 8], [[-0.3784, -0.0866, 1], [0.2714, -0.0488, 1.3]]) + + # Contains 1 Bezier curve + curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCurve/BezierCurve")) + check_basis_curve(curve, "bezier", "cubic", "nonperiodic", [7], [[-2.644, -0.0777, 0], [1, 0.9815, 0]]) + + # Contains 1 Bezier curve + curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCircle/BezierCircle")) + check_basis_curve(curve, "bezier", "cubic", "periodic", [12], [[-1, -1, 0], [1, 1, 0]]) + + # Contains 2 NURBS curves + curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCurve/NurbsCurve")) + check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-0.75, -1.6898, -0.0117], [2.0896, 0.9583, 0.0293]]) + + # Contains 1 NURBS curve + curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCircle/NurbsCircle")) + check_nurbs_curve(curve, True, [3], [8], 13, [[-1, -1, 0], [1, 1, 0]]) + def test_export_animation(self): bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend")) export_path = self.tempdir / "usd_anim_test.usda"