USD Export: New Curves/Hair Support #105375
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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), ¤t_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
|
|
@ -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
|
|
@ -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(), ¶ms, 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
|
Loading…
Reference in New Issue