diff --git a/source/blender/io/usd/intern/usd_reader_curve.cc b/source/blender/io/usd/intern/usd_reader_curve.cc index 824c3a73af3..72b73bdab78 100644 --- a/source/blender/io/usd/intern/usd_reader_curve.cc +++ b/source/blender/io/usd/intern/usd_reader_curve.cc @@ -6,11 +6,14 @@ #include "usd_reader_curve.hh" #include "usd.hh" +#include "usd_attribute_utils.hh" +#include "usd_hash_types.hh" #include "BKE_attribute.hh" #include "BKE_curves.hh" #include "BKE_geometry_set.hh" #include "BKE_object.hh" +#include "BKE_report.hh" #include "BLI_index_range.hh" #include "BLI_math_vector_types.hh" @@ -20,8 +23,10 @@ #include #include +#include namespace blender::io::usd { + static inline float3 to_float3(pxr::GfVec3f vec3f) { return float3(vec3f.data()); @@ -105,6 +110,27 @@ static CurveType get_curve_type(pxr::TfToken type, pxr::TfToken basis) return CURVE_TYPE_POLY; } +static const std::optional convert_usd_interp_to_blender( + const pxr::TfToken usd_domain) +{ + static const blender::Map domain_map = []() { + blender::Map map; + map.add_new(pxr::UsdGeomTokens->vertex, bke::AttrDomain::Point); + map.add_new(pxr::UsdGeomTokens->varying, bke::AttrDomain::Point); + map.add_new(pxr::UsdGeomTokens->constant, bke::AttrDomain::Curve); + map.add_new(pxr::UsdGeomTokens->uniform, bke::AttrDomain::Curve); + return map; + }(); + + const bke::AttrDomain *value = domain_map.lookup_ptr(usd_domain); + + if (value == nullptr) { + return std::nullopt; + } + + return *value; +} + void USDCurvesReader::create_object(Main *bmain, const double /*motionSampleTime*/) { curve_ = static_cast(BKE_curves_add(bmain, name_.c_str())); @@ -258,6 +284,41 @@ void USDCurvesReader::read_curve_sample(Curves *curves_id, const double motionSa radii.finish(); } + + read_custom_data(curves, motionSampleTime); +} + +void USDCurvesReader::read_custom_data(bke::CurvesGeometry &curves, + const double motionSampleTime) const +{ + pxr::UsdGeomPrimvarsAPI pv_api(curve_prim_); + + std::vector primvars = pv_api.GetPrimvarsWithValues(); + for (const pxr::UsdGeomPrimvar &pv : primvars) { + if (!pv.HasValue()) { + continue; + } + + const pxr::SdfValueTypeName pv_type = pv.GetTypeName(); + const pxr::TfToken pv_interp = pv.GetInterpolation(); + + const std::optional domain = convert_usd_interp_to_blender(pv_interp); + const std::optional type = convert_usd_type_to_blender(pv_type); + + if (!domain.has_value() || !type.has_value()) { + const pxr::TfToken pv_name = pv.StripPrimvarsName(pv.GetPrimvarName()); + BKE_reportf(reports(), + RPT_WARNING, + "Primvar '%s' (interpolation %s, type %s) cannot be converted to Blender", + pv_name.GetText(), + pv_interp.GetText(), + pv_type.GetAsToken().GetText()); + continue; + } + + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + copy_primvar_to_blender_attribute(pv, motionSampleTime, *type, *domain, {}, attributes); + } } void USDCurvesReader::read_geometry(bke::GeometrySet &geometry_set, diff --git a/source/blender/io/usd/intern/usd_reader_curve.hh b/source/blender/io/usd/intern/usd_reader_curve.hh index 1d2832b6d2f..cb7357a45e8 100644 --- a/source/blender/io/usd/intern/usd_reader_curve.hh +++ b/source/blender/io/usd/intern/usd_reader_curve.hh @@ -13,7 +13,8 @@ struct Curves; namespace blender::bke { struct GeometrySet; -} +class CurvesGeometry; +} // namespace blender::bke namespace blender::io::usd { @@ -43,6 +44,8 @@ class USDCurvesReader : public USDGeomReader { void read_geometry(bke::GeometrySet &geometry_set, USDMeshReadParams params, const char **err_str) override; + + void read_custom_data(bke::CurvesGeometry &curves, const double motionSampleTime) const; }; } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_curves.cc b/source/blender/io/usd/intern/usd_writer_curves.cc index 00a04d9c11b..cecd4717ad7 100644 --- a/source/blender/io/usd/intern/usd_writer_curves.cc +++ b/source/blender/io/usd/intern/usd_writer_curves.cc @@ -3,18 +3,25 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ #include +#include #include #include #include +#include +#include #include #include #include +#include "usd_attribute_utils.hh" #include "usd_hierarchy_iterator.hh" #include "usd_writer_curves.hh" #include "BLI_array_utils.hh" +#include "BLI_generic_virtual_array.hh" +#include "BLI_span.hh" +#include "BLI_virtual_array.hh" #include "BKE_attribute.hh" #include "BKE_curve_legacy_convert.hh" @@ -354,6 +361,119 @@ void USDCurvesWriter::set_writer_attributes(pxr::UsdGeomCurves &usd_curves, } } +static std::optional convert_blender_domain_to_usd( + const bke::AttrDomain blender_domain, bool is_bezier) +{ + switch (blender_domain) { + case bke::AttrDomain::Point: + return is_bezier ? pxr::UsdGeomTokens->varying : pxr::UsdGeomTokens->vertex; + case bke::AttrDomain::Curve: + return pxr::UsdGeomTokens->uniform; + + default: + return std::nullopt; + } +} + +void USDCurvesWriter::write_generic_data(const bke::CurvesGeometry &curves, + const bke::AttributeIDRef &attribute_id, + const bke::AttributeMetaData &meta_data, + const pxr::UsdGeomCurves &usd_curves) +{ + const CurveType curve_type = CurveType(curves.curve_types().first()); + const bool is_bezier = curve_type == CURVE_TYPE_BEZIER; + + const std::optional pv_interp = convert_blender_domain_to_usd(meta_data.domain, + is_bezier); + const std::optional pv_type = convert_blender_type_to_usd( + meta_data.data_type); + + if (!pv_interp || !pv_type) { + BKE_reportf(this->reports(), + RPT_WARNING, + "Attribute '%s' (Blender domain %d, type %d) cannot be converted to USD", + std::string(attribute_id.name()).c_str(), + meta_data.domain, + meta_data.data_type); + return; + } + + const GVArray attribute = *curves.attributes().lookup( + attribute_id, meta_data.domain, meta_data.data_type); + if (attribute.is_empty()) { + return; + } + + const pxr::UsdTimeCode timecode = get_export_time_code(); + const pxr::TfToken pv_name(pxr::TfMakeValidIdentifier(attribute_id.name())); + const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_curves); + + pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type, *pv_interp); + + copy_blender_attribute_to_primvar( + attribute, meta_data.data_type, timecode, pv_attr, usd_value_writer_); +} + +void USDCurvesWriter::write_uv_data(const bke::CurvesGeometry &curves, + const bke::AttributeIDRef &attribute_id, + const pxr::UsdGeomCurves &usd_curves) +{ + const VArray buffer = *curves.attributes().lookup(attribute_id, + bke::AttrDomain::Curve); + if (buffer.is_empty()) { + return; + } + + const pxr::UsdTimeCode timecode = get_export_time_code(); + const pxr::TfToken pv_name(pxr::TfMakeValidIdentifier(attribute_id.name())); + const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_curves); + + pxr::UsdGeomPrimvar pv_uv = pv_api.CreatePrimvar( + pv_name, pxr::SdfValueTypeNames->TexCoord2fArray, pxr::UsdGeomTokens->uniform); + + copy_blender_buffer_to_primvar(buffer, timecode, pv_uv, usd_value_writer_); +} + +void USDCurvesWriter::write_custom_data(const bke::CurvesGeometry &curves, + pxr::UsdGeomCurves &usd_curves) +{ + const bke::AttributeAccessor attributes = curves.attributes(); + + attributes.for_all( + [&](const bke::AttributeIDRef &attribute_id, const bke::AttributeMetaData &meta_data) { + /* Skip "internal" Blender properties and attributes dealt with elsewhere. */ + const StringRef attr_name = attribute_id.name(); + if (attr_name[0] == '.' || attribute_id.is_anonymous() || + ELEM(attr_name, + "position", + "radius", + "resolution", + "id", + "curve_type", + "handle_left", + "handle_right", + "handle_type_left", + "handle_type_right")) + { + return true; + } + + /* Spline UV data */ + if (meta_data.domain == bke::AttrDomain::Curve && meta_data.data_type == CD_PROP_FLOAT2) { + if (usd_export_context_.export_params.export_uvmaps) { + this->write_uv_data(curves, attribute_id, usd_curves); + } + } + + /* Everything else. */ + else { + this->write_generic_data(curves, attribute_id, meta_data, usd_curves); + } + + return true; + }); +} + void USDCurvesWriter::do_write(HierarchyContext &context) { Curves *curves_id; @@ -481,6 +601,8 @@ void USDCurvesWriter::do_write(HierarchyContext &context) assign_materials(context, *usd_curves); + write_custom_data(curves, *usd_curves); + auto prim = usd_curves->GetPrim(); write_id_properties(prim, curves_id->id, timecode); } diff --git a/source/blender/io/usd/intern/usd_writer_curves.hh b/source/blender/io/usd/intern/usd_writer_curves.hh index 980cc624f7c..4dc67187c12 100644 --- a/source/blender/io/usd/intern/usd_writer_curves.hh +++ b/source/blender/io/usd/intern/usd_writer_curves.hh @@ -9,6 +9,12 @@ #include #include +namespace blender::bke { +class AttributeIDRef; +struct AttributeMetaData; +class CurvesGeometry; +} // namespace blender::bke + namespace blender::io::usd { /* Writer for writing Curves data as USD curves. */ @@ -38,6 +44,18 @@ class USDCurvesWriter final : public USDAbstractWriter { const pxr::VtArray &knots, const pxr::VtArray &orders, const pxr::UsdTimeCode timecode); + + void write_generic_data(const bke::CurvesGeometry &curves, + const bke::AttributeIDRef &attribute_id, + const bke::AttributeMetaData &meta_data, + const pxr::UsdGeomCurves &usd_curves); + + void write_uv_data(const bke::CurvesGeometry &curves, + const bke::AttributeIDRef &attribute_id, + const pxr::UsdGeomCurves &usd_curves); + + void write_custom_data(const blender::bke::CurvesGeometry &curves, + pxr::UsdGeomCurves &usd_curves); }; } // namespace blender::io::usd diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index e0edc5c20c6..76f6c87caa6 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -263,6 +263,54 @@ class USDExportTest(AbstractUSDTest): self.check_primvar(prim, "fc_quat", "VtArray", "faceVarying", 4) self.check_primvar_missing(prim, "fc_mat4x4") + prim = stage.GetPrimAtPath("/root/Curve_base/Curves/Curves") + + self.check_primvar(prim, "p_bool", "VtArray", "vertex", 24) + self.check_primvar(prim, "p_int8", "VtArray", "vertex", 24) + self.check_primvar(prim, "p_int32", "VtArray", "vertex", 24) + self.check_primvar(prim, "p_float", "VtArray", "vertex", 24) + self.check_primvar_missing(prim, "p_color") + self.check_primvar_missing(prim, "p_byte_color") + self.check_primvar(prim, "p_vec2", "VtArray", "vertex", 24) + self.check_primvar(prim, "p_vec3", "VtArray", "vertex", 24) + self.check_primvar(prim, "p_quat", "VtArray", "vertex", 24) + self.check_primvar_missing(prim, "p_mat4x4") + + self.check_primvar(prim, "sp_bool", "VtArray", "uniform", 2) + self.check_primvar(prim, "sp_int8", "VtArray", "uniform", 2) + self.check_primvar(prim, "sp_int32", "VtArray", "uniform", 2) + self.check_primvar(prim, "sp_float", "VtArray", "uniform", 2) + self.check_primvar_missing(prim, "sp_color") + self.check_primvar_missing(prim, "sp_byte_color") + self.check_primvar(prim, "sp_vec2", "VtArray", "uniform", 2) + self.check_primvar(prim, "sp_vec3", "VtArray", "uniform", 2) + self.check_primvar(prim, "sp_quat", "VtArray", "uniform", 2) + self.check_primvar_missing(prim, "sp_mat4x4") + + prim = stage.GetPrimAtPath("/root/Curve_bezier_base/Curves_bezier/Curves") + + self.check_primvar(prim, "p_bool", "VtArray", "varying", 10) + self.check_primvar(prim, "p_int8", "VtArray", "varying", 10) + self.check_primvar(prim, "p_int32", "VtArray", "varying", 10) + self.check_primvar(prim, "p_float", "VtArray", "varying", 10) + self.check_primvar_missing(prim, "p_color") + self.check_primvar_missing(prim, "p_byte_color") + self.check_primvar(prim, "p_vec2", "VtArray", "varying", 10) + self.check_primvar(prim, "p_vec3", "VtArray", "varying", 10) + self.check_primvar(prim, "p_quat", "VtArray", "varying", 10) + self.check_primvar_missing(prim, "p_mat4x4") + + self.check_primvar(prim, "sp_bool", "VtArray", "uniform", 3) + self.check_primvar(prim, "sp_int8", "VtArray", "uniform", 3) + self.check_primvar(prim, "sp_int32", "VtArray", "uniform", 3) + self.check_primvar(prim, "sp_float", "VtArray", "uniform", 3) + self.check_primvar_missing(prim, "sp_color") + self.check_primvar_missing(prim, "sp_byte_color") + self.check_primvar(prim, "sp_vec2", "VtArray", "uniform", 3) + self.check_primvar(prim, "sp_vec3", "VtArray", "uniform", 3) + self.check_primvar(prim, "sp_quat", "VtArray", "uniform", 3) + self.check_primvar_missing(prim, "sp_mat4x4") + def main(): global args diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 2ca645aa2cb..5918c2a25f2 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -737,6 +737,59 @@ class USDImportTest(AbstractUSDTest): self.check_attribute_missing(mesh, "fc_quat") self.check_attribute_missing(mesh, "fc_mat4x4") + # Find the non "bezier" Curves object -- Has 2 curves (12 vertices each) + all_curves = [o for o in bpy.data.objects if o.type == 'CURVES'] + curves = [o for o in all_curves if not o.parent.name.startswith("Curve_bezier")] + curves = curves[0].data + + self.check_attribute(curves, "p_bool", 'POINT', 'BOOLEAN', 24) + self.check_attribute(curves, "p_int8", 'POINT', 'INT', 24) + self.check_attribute(curves, "p_int32", 'POINT', 'INT', 24) + self.check_attribute(curves, "p_float", 'POINT', 'FLOAT', 24) + self.check_attribute_missing(curves, "p_byte_color") + self.check_attribute_missing(curves, "p_color") + self.check_attribute(curves, "p_vec2", 'POINT', 'FLOAT2', 24) + self.check_attribute(curves, "p_vec3", 'POINT', 'FLOAT_VECTOR', 24) + self.check_attribute(curves, "p_quat", 'POINT', 'QUATERNION', 24) + self.check_attribute_missing(curves, "p_mat4x4") + + self.check_attribute(curves, "sp_bool", 'CURVE', 'BOOLEAN', 2) + self.check_attribute(curves, "sp_int8", 'CURVE', 'INT', 2) + self.check_attribute(curves, "sp_int32", 'CURVE', 'INT', 2) + self.check_attribute(curves, "sp_float", 'CURVE', 'FLOAT', 2) + self.check_attribute_missing(curves, "sp_byte_color") + self.check_attribute_missing(curves, "sp_color") + self.check_attribute(curves, "sp_vec2", 'CURVE', 'FLOAT2', 2) + self.check_attribute(curves, "sp_vec3", 'CURVE', 'FLOAT_VECTOR', 2) + self.check_attribute(curves, "sp_quat", 'CURVE', 'QUATERNION', 2) + self.check_attribute_missing(curves, "sp_mat4x4") + + # Find the "bezier" Curves object -- Has 3 curves (2, 3, and 5 control points) + curves = [o for o in all_curves if o.parent.name.startswith("Curve_bezier")] + curves = curves[0].data + + self.check_attribute(curves, "p_bool", 'POINT', 'BOOLEAN', 10) + self.check_attribute(curves, "p_int8", 'POINT', 'INT', 10) + self.check_attribute(curves, "p_int32", 'POINT', 'INT', 10) + self.check_attribute(curves, "p_float", 'POINT', 'FLOAT', 10) + self.check_attribute_missing(curves, "p_byte_color") + self.check_attribute_missing(curves, "p_color") + self.check_attribute(curves, "p_vec2", 'POINT', 'FLOAT2', 10) + self.check_attribute(curves, "p_vec3", 'POINT', 'FLOAT_VECTOR', 10) + self.check_attribute(curves, "p_quat", 'POINT', 'QUATERNION', 10) + self.check_attribute_missing(curves, "p_mat4x4") + + self.check_attribute(curves, "sp_bool", 'CURVE', 'BOOLEAN', 3) + self.check_attribute(curves, "sp_int8", 'CURVE', 'INT', 3) + self.check_attribute(curves, "sp_int32", 'CURVE', 'INT', 3) + self.check_attribute(curves, "sp_float", 'CURVE', 'FLOAT', 3) + self.check_attribute_missing(curves, "sp_byte_color") + self.check_attribute_missing(curves, "sp_color") + self.check_attribute(curves, "sp_vec2", 'CURVE', 'FLOAT2', 3) + self.check_attribute(curves, "sp_vec3", 'CURVE', 'FLOAT_VECTOR', 3) + self.check_attribute(curves, "sp_quat", 'CURVE', 'QUATERNION', 3) + self.check_attribute_missing(curves, "sp_mat4x4") + def main(): global args