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:
committed by
Jesse Yurkovich
parent
cdc7526aed
commit
1ceaaeeff7
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(), ¶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<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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user