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. */
SonnyCampbell_Unity marked this conversation as resolved Outdated

Should be this year.

Should be this year.
#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
SonnyCampbell_Unity marked this conversation as resolved Outdated

Use snake_case for variable names (https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Naming)

Use `snake_case` for variable names (https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Naming)
* 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.
SonnyCampbell_Unity marked this conversation as resolved Outdated
Comment formatting: https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Comments
* 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();
SonnyCampbell_Unity marked this conversation as resolved Outdated

for (const int i_curve : geometry.curves_range()) is a slightly nicer way to write this, and it allows the iterator variable to be const

`for (const int i_curve : geometry.curves_range())` is a slightly nicer way to write this, and it allows the iterator variable to be const
for (const int i_curve : geometry.curves_range()) {
SonnyCampbell_Unity marked this conversation as resolved Outdated

The "canonical" variable name used elsewhere for this is points instead of curve_points

The "canonical" variable name used elsewhere for this is `points` instead of `curve_points`
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();
SonnyCampbell_Unity marked this conversation as resolved Outdated

IndexRange can be iterated directly here: for (const int i : points) {

If you want to skip the last point, you can use for (const int i : points.drop_back(1)) {

`IndexRange` can be iterated directly here: `for (const int i : points) {` If you want to skip the last point, you can use `for (const int i : points.drop_back(1)) {`

Is this a necessary change? Personally I find "i_point < last_point_index" more readable than points.drop_back(1).

Is this a necessary change? Personally I find "i_point < last_point_index" more readable than points.drop_back(1).
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);
SonnyCampbell_Unity marked this conversation as resolved Outdated

Maybe I'm missing something, but doesn't this replace the knots completely with every new curve?

Maybe I'm missing something, but doesn't this replace the knots completely with every new curve?

Yeah you are right, thanks for catching this! I need to rethink what I was trying to do here.

Yeah you are right, thanks for catching this! I need to rethink what I was trying to do here.

I have resolved this issue with the knots now. According to the USD spec knots should be the concatentation of all batched curves so I've updated the logic there and added a test with a curve containing two NURBS curves.

https://openusd.org/dev/api/class_usd_geom_nurbs_curves.html#details

I have resolved this issue with the knots now. According to the USD spec `knots should be the concatentation of all batched curves` so I've updated the logic there and added a test with a curve containing two NURBS curves. https://openusd.org/dev/api/class_usd_geom_nurbs_curves.html#details
/* 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();
SonnyCampbell_Unity marked this conversation as resolved Outdated

Small thing, but Curve should be Curves here, same with below.

Small thing, but `Curve` should be `Curves` here, same with below.
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;
});
SonnyCampbell_Unity marked this conversation as resolved Outdated

It's weird, but the period is added automatically to these report strings AFAIK, so no need to add it here. Same below.

It's weird, but the period is added automatically to these report strings AFAIK, so no need to add it here. Same below.
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];
SonnyCampbell_Unity marked this conversation as resolved Outdated

If you want this to be extra pretty, you could use rna_enum_curves_types and IFACE_(RNA_enum_name_from_value(..) to print the UI name instead of the integer value.

If you want this to be extra pretty, you could use `rna_enum_curves_types` and `IFACE_(RNA_enum_name_from_value(..)` to print the UI name instead of the integer value.
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);
SonnyCampbell_Unity marked this conversation as resolved Outdated

(int)first_frame_curve_type -> int(first_frame_curve_type)

Functional style casts here (and below) (https://wiki.blender.org/wiki/Style_Guide/C_Cpp#C.2B.2B_Type_Cast)

`(int)first_frame_curve_type` -> `int(first_frame_curve_type)` Functional style casts here (and below) (https://wiki.blender.org/wiki/Style_Guide/C_Cpp#C.2B.2B_Type_Cast)
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,
SonnyCampbell_Unity marked this conversation as resolved Outdated

The above three functions (check_catmullRom_curve, check_bezier_curve, check_nurbs_curve) need to be marked as static to match their definition below, otherwise this code does not compile with clang on macOS.

The above three functions (`check_catmullRom_curve`, `check_bezier_curve`, `check_nurbs_curve`) need to be marked as static to match their definition below, otherwise this code does not compile with clang on macOS.
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