USD Export: New Curves/Hair Support #105375

Merged
Michael Kowalski merged 19 commits from SonnyCampbell_Unity/blender:unity/T102376-USD-Curves-Export into main 2023-05-16 20:04:26 +02:00
5 changed files with 923 additions and 3 deletions

View File

@ -48,6 +48,7 @@ set(INC
../../blenkernel
../../blenlib
../../blenloader
../../blentranslation
../../bmesh
../../depsgraph
../../editors/include
@ -74,6 +75,7 @@ set(SRC
intern/usd_hierarchy_iterator.cc
intern/usd_writer_abstract.cc
intern/usd_writer_camera.cc
intern/usd_writer_curves.cc
intern/usd_writer_hair.cc
intern/usd_writer_light.cc
intern/usd_writer_material.cc
@ -103,6 +105,7 @@ set(SRC
intern/usd_hierarchy_iterator.h
intern/usd_writer_abstract.h
intern/usd_writer_camera.h
intern/usd_writer_curves.h
intern/usd_writer_hair.h
intern/usd_writer_light.h
intern/usd_writer_material.h
@ -159,6 +162,7 @@ if(WITH_GTESTS)
tests/usd_tests_common.cc
tests/usd_tests_common.h
tests/usd_usdz_export_test.cc
tests/usd_curves_test.cc
intern/usd_writer_material.h
)
if(USD_IMAGING_HEADERS)

View File

@ -5,6 +5,7 @@
#include "usd_hierarchy_iterator.h"
#include "usd_writer_abstract.h"
#include "usd_writer_camera.h"
#include "usd_writer_curves.h"
#include "usd_writer_hair.h"
#include "usd_writer_light.h"
#include "usd_writer_mesh.h"
@ -12,6 +13,7 @@
#include "usd_writer_transform.h"
#include "usd_writer_volume.h"
#include <memory>
#include <string>
#include <pxr/base/tf/stringUtils.h>
@ -105,12 +107,14 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
case OB_MBALL:
data_writer = new USDMetaballWriter(usd_export_context);
break;
case OB_CURVES_LEGACY:
case OB_CURVES:
data_writer = new USDCurvesWriter(usd_export_context);
break;
case OB_VOLUME:
data_writer = new USDVolumeWriter(usd_export_context);
break;
case OB_EMPTY:
case OB_CURVES_LEGACY:
case OB_SURF:
case OB_FONT:
case OB_SPEAKER:
@ -119,7 +123,6 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
case OB_ARMATURE:
case OB_GPENCIL_LEGACY:
case OB_POINTCLOUD:
case OB_CURVES:
return nullptr;
case OB_TYPE_MAX:
BLI_assert_msg(0, "OB_TYPE_MAX should not be used");

View File

@ -0,0 +1,522 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation. All rights reserved. */
#include <numeric>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include "usd_hierarchy_iterator.h"
#include "usd_writer_curves.h"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curves.hh"
#include "BKE_lib_id.h"
#include "BKE_material.h"
#include "BLI_math_geom.h"
#include "BLT_translation.h"
#include "RNA_access.h"
#include "RNA_enum_types.h"
#include "WM_api.h"
#include "WM_types.h"
namespace blender::io::usd {
USDCurvesWriter::USDCurvesWriter(const USDExporterContext &ctx) : USDAbstractWriter(ctx) {}
USDCurvesWriter::~USDCurvesWriter() {}
pxr::UsdGeomCurves USDCurvesWriter::DefineUsdGeomBasisCurves(pxr::VtValue curve_basis,
const bool is_cyclic,
const bool is_cubic)
{
pxr::UsdGeomCurves curves = pxr::UsdGeomBasisCurves::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
pxr::UsdGeomBasisCurves basis_curves = pxr::UsdGeomBasisCurves(curves);
/* Not required to set the basis attribute for linear curves
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_basis_curves.html#details */
if (is_cubic) {
basis_curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->cubic));
basis_curves.CreateBasisAttr(curve_basis);
}
else {
basis_curves.CreateTypeAttr(pxr::VtValue(pxr::UsdGeomTokens->linear));
}
if (is_cyclic) {
basis_curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->periodic));
}
else {
basis_curves.CreateWrapAttr(pxr::VtValue(pxr::UsdGeomTokens->nonperiodic));
}
return curves;
}
static void populate_curve_widths(const bke::CurvesGeometry &geometry, pxr::VtArray<float> &widths)
{
const bke::AttributeAccessor curve_attributes = geometry.attributes();
const bke::AttributeReader<float> radii = curve_attributes.lookup<float>("radius",
ATTR_DOMAIN_POINT);
widths.resize(radii.varray.size());
for (const int i : radii.varray.index_range()) {
widths[i] = radii.varray[i] * 2.0f;
}
}
static pxr::TfToken get_curve_width_interpolation(const pxr::VtArray<float> &widths,
const pxr::VtArray<int> &segments,
const pxr::VtIntArray &control_point_counts,
const bool is_cyclic)
{
if (widths.empty()) {
return pxr::TfToken();
}
const size_t accumulated_control_point_count = std::accumulate(
control_point_counts.begin(), control_point_counts.end(), 0);
/* For Blender curves, radii are always stored per point. For linear curves, this should match
* with USD's vertex interpolation. For cubic curves, this should match with USD's varying
* interpolation. */
if (widths.size() == accumulated_control_point_count) {
return pxr::UsdGeomTokens->vertex;
}
size_t expectedVaryingSize = std::accumulate(segments.begin(), segments.end(), 0);
if (!is_cyclic) {
expectedVaryingSize += control_point_counts.size();
}
if (widths.size() == expectedVaryingSize) {
return pxr::UsdGeomTokens->varying;
}
WM_report(RPT_WARNING, "Curve width size not supported for USD interpolation");
return pxr::TfToken();
}
static void populate_curve_verts(const bke::CurvesGeometry &geometry,
const Span<float3> positions,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<int> &segments,
const bool is_cyclic,
const bool is_cubic)
{
const OffsetIndices points_by_curve = geometry.points_by_curve();
for (const int i_curve : geometry.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
for (const int i_point : points) {
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
}
const int tot_points = points.size();
control_point_counts[i_curve] = tot_points;
/* For periodic linear curve, segment count = curveVertexCount.
* For periodic cubic curve, segment count = curveVertexCount / vstep.
* For nonperiodic linear curve, segment count = curveVertexCount - 1.
* For nonperiodic cubic curve, segment count = ((curveVertexCount - 4) / vstep) + 1.
* This function handles linear and Catmull-Rom curves. For Catmull-Rom, vstep is 1.
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_basis_curves.html */
if (is_cyclic) {
segments[i_curve] = tot_points;
}
else if (is_cubic) {
segments[i_curve] = (tot_points - 4) + 1;
}
else {
segments[i_curve] = tot_points - 1;
}
}
}
static void populate_curve_props(const bke::CurvesGeometry &geometry,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::TfToken &interpolation,
const bool is_cyclic,
const bool is_cubic)
{
const int num_curves = geometry.curve_num;
const Span<float3> positions = geometry.positions();
pxr::VtArray<int> segments(num_curves);
populate_curve_verts(
geometry, positions, verts, control_point_counts, segments, is_cyclic, is_cubic);
populate_curve_widths(geometry, widths);
interpolation = get_curve_width_interpolation(widths, segments, control_point_counts, is_cyclic);
}
static void populate_curve_verts_for_bezier(const bke::CurvesGeometry &geometry,
const Span<float3> positions,
const Span<float3> handles_l,
const Span<float3> handles_r,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<int> &segments,
const bool is_cyclic)
{
const int bezier_vstep = 3;
const OffsetIndices points_by_curve = geometry.points_by_curve();
for (const int i_curve : geometry.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
const int start_point_index = points[0];
const int last_point_index = points[points.size() - 1];
const int start_verts_count = verts.size();
for (int i_point = start_point_index; i_point < last_point_index; i_point++) {
/* The order verts in the USD bezier curve representation is [control point 0, right handle
* 0, left handle 1, control point 1, right handle 1, left handle 2, control point 2, ...].
* The last vert in the array doesn't need a right handle because the curve stops at that
* point. */
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
const blender::float3 right_handle = handles_r[i_point];
verts.push_back(pxr::GfVec3f(right_handle[0], right_handle[1], right_handle[2]));
const blender::float3 left_handle = handles_l[i_point + 1];
verts.push_back(pxr::GfVec3f(left_handle[0], left_handle[1], left_handle[2]));
}
verts.push_back(pxr::GfVec3f(positions[last_point_index][0],
positions[last_point_index][1],
positions[last_point_index][2]));
/* For USD representation of periodic bezier curve, one of the curve's points must be
* repeated to close the curve. The repeated point is the first point. Since the curve is
* closed, we now need to include the right handle of the last point and the left handle of
* the first point.
*/
if (is_cyclic) {
const blender::float3 right_handle = handles_r[last_point_index];
verts.push_back(pxr::GfVec3f(right_handle[0], right_handle[1], right_handle[2]));
const blender::float3 left_handle = handles_l[start_point_index];
verts.push_back(pxr::GfVec3f(left_handle[0], left_handle[1], left_handle[2]));
verts.push_back(pxr::GfVec3f(positions[start_point_index][0],
positions[start_point_index][1],
positions[start_point_index][2]));
}
const int tot_points = verts.size() - start_verts_count;
control_point_counts[i_curve] = tot_points;
if (is_cyclic) {
segments[i_curve] = tot_points / bezier_vstep;
}
else {
segments[i_curve] = ((tot_points - 4) / bezier_vstep) + 1;
}
}
}
static void populate_curve_props_for_bezier(const bke::CurvesGeometry &geometry,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::TfToken &interpolation,
const bool is_cyclic)
{
const int num_curves = geometry.curve_num;
const Span<float3> positions = geometry.positions();
const Span<float3> handles_l = geometry.handle_positions_left();
const Span<float3> handles_r = geometry.handle_positions_right();
pxr::VtArray<int> segments(num_curves);
populate_curve_verts_for_bezier(
geometry, positions, handles_l, handles_r, verts, control_point_counts, segments, is_cyclic);
populate_curve_widths(geometry, widths);
interpolation = get_curve_width_interpolation(widths, segments, control_point_counts, is_cyclic);
}
static void populate_curve_props_for_nurbs(const bke::CurvesGeometry &geometry,
pxr::VtArray<pxr::GfVec3f> &verts,
pxr::VtIntArray &control_point_counts,
pxr::VtArray<float> &widths,
pxr::VtArray<double> &knots,
pxr::VtArray<int> &orders,
pxr::TfToken &interpolation,
const bool is_cyclic)
{
/* Order and range, when representing a batched NurbsCurve should be authored one value per
* curve.*/
const int num_curves = geometry.curve_num;
orders.resize(num_curves);
const Span<float3> positions = geometry.positions();
VArray<int8_t> geom_orders = geometry.nurbs_orders();
VArray<int8_t> knots_modes = geometry.nurbs_knots_modes();
const OffsetIndices points_by_curve = geometry.points_by_curve();
for (const int i_curve : geometry.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
for (const int i_point : points) {
verts.push_back(
pxr::GfVec3f(positions[i_point][0], positions[i_point][1], positions[i_point][2]));
}
const int tot_points = points.size();
control_point_counts[i_curve] = tot_points;
const int8_t order = geom_orders[i_curve];
orders[i_curve] = int(geom_orders[i_curve]);
const KnotsMode mode = KnotsMode(knots_modes[i_curve]);
const int knots_num = bke::curves::nurbs::knots_num(tot_points, order, is_cyclic);
Array<float> temp_knots(knots_num);
bke::curves::nurbs::calculate_knots(tot_points, mode, order, is_cyclic, temp_knots);
/* Knots should be the concatentation of all batched curves.
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_nurbs_curves.html#details */
for (int i_knot = 0; i_knot < knots_num; i_knot++) {
knots.push_back(double(temp_knots[i_knot]));
}
/* For USD it is required to set specific end knots for periodic/non-periodic curves
* https://graphics.pixar.com/usd/dev/api/class_usd_geom_nurbs_curves.html#details */
int zeroth_knot_index = knots.size() - knots_num;
if (is_cyclic) {
knots[zeroth_knot_index] = knots[zeroth_knot_index + 1] -
(knots[knots.size() - 2] - knots[knots.size() - 3]);
knots[knots.size() - 1] = knots[knots.size() - 2] +
(knots[zeroth_knot_index + 2] - knots[zeroth_knot_index + 1]);
}
else {
knots[zeroth_knot_index] = knots[zeroth_knot_index + 1];
knots[knots.size() - 1] = knots[knots.size() - 2];
}
}
populate_curve_widths(geometry, widths);
interpolation = pxr::UsdGeomTokens->vertex;
}
void USDCurvesWriter::set_writer_attributes_for_nurbs(const pxr::UsdGeomCurves usd_curves,
const pxr::VtArray<double> knots,
const pxr::VtArray<int> orders,
const pxr::UsdTimeCode timecode)
{
pxr::UsdAttribute attr_knots =
pxr::UsdGeomNurbsCurves(usd_curves).CreateKnotsAttr(pxr::VtValue(), true);
usd_value_writer_.SetAttribute(attr_knots, pxr::VtValue(knots), timecode);
pxr::UsdAttribute attr_order =
pxr::UsdGeomNurbsCurves(usd_curves).CreateOrderAttr(pxr::VtValue(), true);
usd_value_writer_.SetAttribute(attr_order, pxr::VtValue(orders), timecode);
}
void USDCurvesWriter::set_writer_attributes(pxr::UsdGeomCurves &usd_curves,
const pxr::VtArray<pxr::GfVec3f> verts,
const pxr::VtIntArray control_point_counts,
const pxr::VtArray<float> widths,
const pxr::UsdTimeCode timecode,
const pxr::TfToken interpolation)
{
pxr::UsdAttribute attr_points = usd_curves.CreatePointsAttr(pxr::VtValue(), true);
usd_value_writer_.SetAttribute(attr_points, pxr::VtValue(verts), timecode);
pxr::UsdAttribute attr_vertex_counts = usd_curves.CreateCurveVertexCountsAttr(pxr::VtValue(),
true);
usd_value_writer_.SetAttribute(attr_vertex_counts, pxr::VtValue(control_point_counts), timecode);
if (widths.size() > 0) {
pxr::UsdAttribute attr_widths = usd_curves.CreateWidthsAttr(pxr::VtValue(), true);
usd_value_writer_.SetAttribute(attr_widths, pxr::VtValue(widths), timecode);
usd_curves.SetWidthsInterpolation(interpolation);
}
}
void USDCurvesWriter::do_write(HierarchyContext &context)
{
Curves *curves;
std::unique_ptr<Curves, std::function<void(Curves *)>> converted_curves;
switch (context.object->type) {
case OB_CURVES_LEGACY: {
const Curve *legacy_curve = static_cast<Curve *>(context.object->data);
converted_curves = std::unique_ptr<Curves, std::function<void(Curves *)>>(
bke::curve_legacy_to_curves(*legacy_curve), [](Curves *c) { BKE_id_free(nullptr, c); });
curves = converted_curves.get();
break;
}
case OB_CURVES:
curves = static_cast<Curves *>(context.object->data);
break;
default:
BLI_assert_unreachable();
return;
}
const bke::CurvesGeometry &geometry = curves->geometry.wrap();
if (geometry.points_num() == 0) {
return;
}
const std::array<int, CURVE_TYPES_NUM> curve_type_counts = geometry.curve_type_counts();
const int number_of_curve_types = std::reduce(
curve_type_counts.begin(), curve_type_counts.end(), 0, [](int previous_result, int item) {
return item > 0 ? ++previous_result : previous_result;
});
if (number_of_curve_types > 1) {
WM_report(RPT_WARNING, "Cannot export mixed curve types in the same Curves object");
return;
}
const VArray<bool> cyclic_values = geometry.cyclic();
const bool is_cyclic = cyclic_values[0];
bool all_same_cyclic_type = true;
for (const int i : cyclic_values.index_range()) {
if (cyclic_values[i] != is_cyclic) {
all_same_cyclic_type = false;
break;
}
}
if (!all_same_cyclic_type) {
WM_report(RPT_WARNING,
"Cannot export mixed cyclic and non-cyclic curves in the same Curves object");
return;
}
const pxr::UsdTimeCode timecode = get_export_time_code();
pxr::UsdGeomCurves usd_curves;
pxr::VtArray<pxr::GfVec3f> verts;
pxr::VtIntArray control_point_counts;
control_point_counts.resize(geometry.curves_num());
pxr::VtArray<float> widths;
pxr::TfToken interpolation;
const int8_t curve_type = geometry.curve_types()[0];
if (first_frame_curve_type == -1) {
first_frame_curve_type = curve_type;
}
else if (first_frame_curve_type != curve_type) {
const char *first_frame_curve_type_name = nullptr;
RNA_enum_name_from_value(
rna_enum_curves_types, int(first_frame_curve_type), &first_frame_curve_type_name);
const char *current_curve_type_name = nullptr;
RNA_enum_name_from_value(rna_enum_curves_types, int(curve_type), &current_curve_type_name);
WM_reportf(RPT_WARNING,
"USD does not support animating curve types. The curve type changes from %s to "
"%s on frame %f",
IFACE_(first_frame_curve_type_name),
IFACE_(current_curve_type_name),
timecode);
return;
}
switch (curve_type) {
case CURVE_TYPE_POLY:
usd_curves = DefineUsdGeomBasisCurves(pxr::VtValue(), is_cyclic, false);
populate_curve_props(
geometry, verts, control_point_counts, widths, interpolation, is_cyclic, false);
break;
case CURVE_TYPE_CATMULL_ROM:
usd_curves = DefineUsdGeomBasisCurves(
pxr::VtValue(pxr::UsdGeomTokens->catmullRom), is_cyclic, true);
populate_curve_props(
geometry, verts, control_point_counts, widths, interpolation, is_cyclic, true);
break;
case CURVE_TYPE_BEZIER:
usd_curves = DefineUsdGeomBasisCurves(
pxr::VtValue(pxr::UsdGeomTokens->bezier), is_cyclic, true);
populate_curve_props_for_bezier(
geometry, verts, control_point_counts, widths, interpolation, is_cyclic);
break;
case CURVE_TYPE_NURBS: {
pxr::VtArray<double> knots;
pxr::VtArray<int> orders;
orders.resize(geometry.curves_num());
usd_curves = pxr::UsdGeomNurbsCurves::Define(usd_export_context_.stage,
usd_export_context_.usd_path);
populate_curve_props_for_nurbs(
geometry, verts, control_point_counts, widths, knots, orders, interpolation, is_cyclic);
set_writer_attributes_for_nurbs(usd_curves, knots, orders, timecode);
break;
}
default:
BLI_assert_unreachable();
}
set_writer_attributes(usd_curves, verts, control_point_counts, widths, timecode, interpolation);
assign_materials(context, usd_curves);
}
void USDCurvesWriter::assign_materials(const HierarchyContext &context,
pxr::UsdGeomCurves usd_curve)
{
if (context.object->totcol == 0) {
return;
}
bool curve_material_bound = false;
for (short mat_num = 0; mat_num < context.object->totcol; mat_num++) {
Material *material = BKE_object_material_get(context.object, mat_num + 1);
if (material == nullptr) {
continue;
}
pxr::UsdShadeMaterialBindingAPI api = pxr::UsdShadeMaterialBindingAPI(usd_curve.GetPrim());
pxr::UsdShadeMaterial usd_material = ensure_usd_material(context, material);
api.Bind(usd_material);
/* USD seems to support neither per-material nor per-face-group double-sidedness, so we just
* use the flag from the first non-empty material slot. */
usd_curve.CreateDoubleSidedAttr(
pxr::VtValue((material->blend_flag & MA_BL_CULL_BACKFACE) == 0));
curve_material_bound = true;
break;
}
if (!curve_material_bound) {
/* Blender defaults to double-sided, but USD to single-sided. */
usd_curve.CreateDoubleSidedAttr(pxr::VtValue(true));
}
}
} // namespace blender::io::usd

View File

@ -0,0 +1,41 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2022 Blender Foundation. All rights reserved. */
#pragma once
#include <memory>
#include "DNA_curves_types.h"
#include "usd_writer_abstract.h"
#include <pxr/usd/usdGeom/basisCurves.h>
namespace blender::io::usd {
/* Writer for writing Curves data as USD curves. */
class USDCurvesWriter : public USDAbstractWriter {
public:
USDCurvesWriter(const USDExporterContext &ctx);
~USDCurvesWriter();
protected:
virtual void do_write(HierarchyContext &context) override;
void assign_materials(const HierarchyContext &context, pxr::UsdGeomCurves usd_curve);
private:
int8_t first_frame_curve_type = -1;
pxr::UsdGeomCurves DefineUsdGeomBasisCurves(pxr::VtValue curve_basis, bool cyclic, bool cubic);
void set_writer_attributes(pxr::UsdGeomCurves &usd_curves,
const pxr::VtArray<pxr::GfVec3f> verts,
const pxr::VtIntArray control_point_counts,
const pxr::VtArray<float> widths,
const pxr::UsdTimeCode timecode,
const pxr::TfToken interpolation);
void set_writer_attributes_for_nurbs(const pxr::UsdGeomCurves usd_curves,
const pxr::VtArray<double> knots,
const pxr::VtArray<int> orders,
const pxr::UsdTimeCode timecode);
};
} // namespace blender::io::usd

View File

@ -0,0 +1,350 @@
/* 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.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_mesh.h"
#include "BKE_node.h"
#include "BLI_fileops.h"
#include "BLI_math.h"
#include "BLI_math_vector_types.hh"
#include "BLI_path_util.h"
#include "BLO_readfile.h"
#include "BKE_node_runtime.hh"
#include "DEG_depsgraph.h"
#include "WM_api.h"
#include "usd.h"
#include "usd_tests_common.h"
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:
struct 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();
std::string usd_plugin_path = register_usd_plugins_for_tests();
if (usd_plugin_path.empty()) {
FAIL();
}
}
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);
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, 13);
}
{
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->nonperiodic)
<< "Wrap token should be nonperiodic for nonperiodic 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