USD: Read and write custom attributes for Curves

Add support for reading and writing custom `Curves` attributes.

This allows us to roundtrip Blender's Hair grooms containing UVs and
other attribute data. Note that animated attribute values are not
supported with this change.

This will also address #120042

Pull Request: https://projects.blender.org/blender/blender/pulls/121928
This commit is contained in:
Jesse Yurkovich 2024-05-25 22:23:40 +02:00 • committed by Jesse Yurkovich
parent d3dbd7bdd0
commit 21db0daa4e
6 changed files with 306 additions and 1 deletions

View file

@ -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 <pxr/base/vt/types.h>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
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<bke::AttrDomain> convert_usd_interp_to_blender(
const pxr::TfToken usd_domain)
{
static const blender::Map<pxr::TfToken, bke::AttrDomain> domain_map = []() {
blender::Map<pxr::TfToken, bke::AttrDomain> 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<Curves *>(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<pxr::UsdGeomPrimvar> 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<bke::AttrDomain> domain = convert_usd_interp_to_blender(pv_interp);
const std::optional<eCustomDataType> 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,

View file

@ -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

View file

@ -3,18 +3,25 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <numeric>
#include <string>
#include <pxr/usd/usdGeom/basisCurves.h>
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/primvar.h>
#include <pxr/usd/usdGeom/primvarsAPI.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#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<pxr::TfToken> 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<pxr::TfToken> pv_interp = convert_blender_domain_to_usd(meta_data.domain,
is_bezier);
const std::optional<pxr::SdfValueTypeName> 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<float2> buffer = *curves.attributes().lookup<float2>(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<float2, pxr::GfVec2f>(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);
}

View file

@ -9,6 +9,12 @@
#include <pxr/usd/usdGeom/curves.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
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<double> &knots,
const pxr::VtArray<int> &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

View file

@ -263,6 +263,54 @@ class USDExportTest(AbstractUSDTest):
self.check_primvar(prim, "fc_quat", "VtArray<GfQuatf>", "faceVarying", 4)
self.check_primvar_missing(prim, "fc_mat4x4")
prim = stage.GetPrimAtPath("/root/Curve_base/Curves/Curves")
self.check_primvar(prim, "p_bool", "VtArray<bool>", "vertex", 24)
self.check_primvar(prim, "p_int8", "VtArray<int>", "vertex", 24)
self.check_primvar(prim, "p_int32", "VtArray<int>", "vertex", 24)
self.check_primvar(prim, "p_float", "VtArray<float>", "vertex", 24)
self.check_primvar_missing(prim, "p_color")
self.check_primvar_missing(prim, "p_byte_color")
self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "vertex", 24)
self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "vertex", 24)
self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "vertex", 24)
self.check_primvar_missing(prim, "p_mat4x4")
self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 2)
self.check_primvar(prim, "sp_int8", "VtArray<int>", "uniform", 2)
self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 2)
self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 2)
self.check_primvar_missing(prim, "sp_color")
self.check_primvar_missing(prim, "sp_byte_color")
self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 2)
self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 2)
self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "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<bool>", "varying", 10)
self.check_primvar(prim, "p_int8", "VtArray<int>", "varying", 10)
self.check_primvar(prim, "p_int32", "VtArray<int>", "varying", 10)
self.check_primvar(prim, "p_float", "VtArray<float>", "varying", 10)
self.check_primvar_missing(prim, "p_color")
self.check_primvar_missing(prim, "p_byte_color")
self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "varying", 10)
self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "varying", 10)
self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "varying", 10)
self.check_primvar_missing(prim, "p_mat4x4")
self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 3)
self.check_primvar(prim, "sp_int8", "VtArray<int>", "uniform", 3)
self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 3)
self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 3)
self.check_primvar_missing(prim, "sp_color")
self.check_primvar_missing(prim, "sp_byte_color")
self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 3)
self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 3)
self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "uniform", 3)
self.check_primvar_missing(prim, "sp_mat4x4")
def main():
global args

View file

@ -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