USD: Write extents out for Curves and more consistently for other types

Cleanup and enhance our export of the USD `extent` attribute.

This does the following:
- The existing `author_extents` function now uses recently added common
  code to write out the extents attribute
- A new `author_extents` overload allows the use of Blender's native
  bounds for the types that support it. We now use this rather than
  asking USD to recompute it for us.
- Meshes will now have their extents correctly written during animations
- Curves will now have their extents written as they were not doing so
  prior to this PR
- Hair, Lights, Points, and Volumes make use of the `author_extents`
  functions now

Since Curves need their extents tested, this PR also moves the test from
C++ to Python. Python tests allow for faster iteration, are more
straightforward to write, and allow usage of the USD validator.

Pull Request: https://projects.blender.org/blender/blender/pulls/132531
This commit is contained in:
Jesse Yurkovich
2025-01-17 03:28:13 +01:00
committed by Jesse Yurkovich
parent cdc7526aed
commit 1ceaaeeff7
12 changed files with 116 additions and 417 deletions

View File

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

View File

@@ -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 <pxr/usd/usdGeom/scope.h>
#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<pxr::GfVec3f> 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<float3>> &bounds,
const pxr::UsdTimeCode timecode)
{
pxr::VtArray<pxr::GfVec3f> extent(2);
if (bounds) {
extent[0].Set(bounds->min);
extent[1].Set(bounds->max);
}
pxr::VtArray<pxr::GfVec3f> 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

View File

@@ -21,6 +21,10 @@
struct Material;
struct ReportList;
namespace blender {
template<typename T> 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<float3>> &bounds,
const pxr::UsdTimeCode timecode);
};
} // namespace blender::io::usd

View File

@@ -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,

View File

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

View File

@@ -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<pxr::GfVec3f> 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

View File

@@ -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<float3>> bounds = mesh->bounds_min_max()) {
pxr::VtArray<pxr::GfVec3f> 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)

View File

@@ -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<pxr::TfToken> 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<pxr::GfVec3f> 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

View File

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

View File

@@ -121,17 +121,7 @@ void USDVolumeWriter::do_write(HierarchyContext &context)
usd_volume.CreateFieldRelationship(pxr::TfToken(grid_id), grid_path);
}
if (const std::optional<Bounds<float3>> bounds = BKE_volume_min_max(volume)) {
pxr::VtArray<pxr::GfVec3f> 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);
}

View File

@@ -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 <pxr/base/plug/registry.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/base/vt/types.h>
#include <pxr/base/vt/value.h>
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/usd/object.h>
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/subset.h>
#include <pxr/usd/usdGeom/tokens.h>
#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(), &params, 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<pxr::TfToken>();
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<pxr::TfToken>();
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<pxr::TfToken>();
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<int> 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<pxr::TfToken>();
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<pxr::TfToken>();
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<pxr::TfToken>();
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<int> 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<int> 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<double> 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<int> 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<int> 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<double> 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<int> 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

View File

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