Alembic: Support new Curves object type for export #119894

Merged
Jesse Yurkovich merged 10 commits from deadpin/blender:fix-alembicexport into main 2024-04-12 21:27:25 +02:00
3 changed files with 288 additions and 109 deletions

View File

@ -191,6 +191,7 @@ ABCAbstractWriter *ABCHierarchyIterator::create_data_writer_for_object_type(
case OB_CAMERA:
return new ABCCameraWriter(writer_args);
case OB_CURVES_LEGACY:
case OB_CURVES:
if (params_.curves_as_mesh) {
return new ABCCurveMeshWriter(writer_args);
}

View File

@ -6,13 +6,22 @@
* \ingroup balembic
*/
#include <functional>
#include <memory>
#include "abc_writer_curves.h"
#include "intern/abc_axis_conversion.h"
#include "BLI_array_utils.hh"
#include "BLI_offset_indices.hh"
#include "DNA_curve_types.h"
#include "DNA_object_types.h"
#include "BKE_curve.hh"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curve_to_mesh.hh"
#include "BKE_curves.hh"
#include "BKE_lib_id.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
@ -30,6 +39,13 @@ namespace blender::io::alembic {
const std::string ABC_CURVE_RESOLUTION_U_PROPNAME("blender:resolution");
static inline Imath::V3f to_yup_V3f(float3 v)
{
Imath::V3f p;
copy_yup_from_zup(p.getValue(), v);
return p;
}
ABCCurveWriter::ABCCurveWriter(const ABCWriterConstructorArgs &args) : ABCAbstractWriter(args) {}
void ABCCurveWriter::create_alembic_objects(const HierarchyContext *context)
@ -38,10 +54,28 @@ void ABCCurveWriter::create_alembic_objects(const HierarchyContext *context)
abc_curve_ = OCurves(args_.abc_parent, args_.abc_name, timesample_index_);
abc_curve_schema_ = abc_curve_.getSchema();
Curve *cu = static_cast<Curve *>(context->object->data);
/* TODO: Blender supports per-curve resolutions but we're only using the first curve's data
* here. Investigate using OInt16ArrayProperty to write out all the data but do so efficiently.
* e.g. Write just a single value if all curves share the same resolution etc. */
deadpin marked this conversation as resolved Outdated

The style guide mentions that break is meant to go inside the brackets

The style guide mentions that `break` is meant to go inside the brackets
int resolution_u = 1;
switch (context->object->type) {
case OB_CURVES_LEGACY: {
Curve *curves_id = static_cast<Curve *>(context->object->data);
resolution_u = curves_id->resolu;
break;
}
case OB_CURVES: {
Curves *curves_id = static_cast<Curves *>(context->object->data);
deadpin marked this conversation as resolved Outdated

I guess Alembic requires the resolution to be just a single value?

I guess Alembic requires the resolution to be just a single value?

This is actually a blender-specific property we attach. It's used so our reader code can then pull this out if present. I suppose it was better for roundtripping in the past. For the new Curves object should we just not bother you think? When this is not present, or <= 0, the reader will simply not call curves.resolution_for_write().fill(...) which I think is fine?

This is actually a blender-specific property we attach. It's used so our reader code can then pull this out if present. I suppose it was better for roundtripping in the past. For the new Curves object should we just not bother you think? When this is not present, or <= 0, the reader will simply not call `curves.resolution_for_write().fill(...)` which I think is fine?

Hmm, well the new curves type has a resolution stored per curve (so did the old one actually, with Nurb::resolu, so ideally that would be written and read as well. But maybe that could wait until all generic attributes are supported here too.

Hmm, well the new curves type has a resolution stored per curve (so did the old one actually, with `Nurb::resolu`, so ideally that would be written and read as well. But maybe that could wait until all generic attributes are supported here too.

I see where you're coming from now. Would a TODO comment suffice for now to do some followup later since this is no better or worse than before?

I see where you're coming from now. Would a TODO comment suffice for now to do some followup later since this is no better or worse than before?

Yeah a TODO comment is fine IMO

Yeah a TODO comment is fine IMO
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
resolution_u = curves.resolution().first();
break;
}
}
OCompoundProperty user_props = abc_curve_schema_.getUserProperties();
OInt16Property user_prop_resolu(user_props, ABC_CURVE_RESOLUTION_U_PROPNAME);
user_prop_resolu.set(cu->resolu);
user_prop_resolu.set(resolution_u);
}
Alembic::Abc::OObject ABCCurveWriter::get_alembic_object() const
@ -56,7 +90,70 @@ Alembic::Abc::OCompoundProperty ABCCurveWriter::abc_prop_for_custom_props()
HooglyBoogly marked this conversation as resolved Outdated

Nitpicky thing but I think a lambda without a capture [](Curves *) { ... } is fine here instead of std::function

Nitpicky thing but I think a lambda without a capture `[](Curves *) { ... }` is fine here instead of `std::function`

What did you mean for this one? I do use a stateless lambda just below this as the actual deleter functor.

What did you mean for this one? I do use a stateless lambda just below this as the actual deleter functor.

Ah I see what you mean. I guess to give it a type this is necessary, I don't run into that much-- my bad!

Ah I see what you mean. I guess to give it a type this is necessary, I don't run into that much-- my bad!
void ABCCurveWriter::do_write(HierarchyContext &context)
{
Curve *curve = static_cast<Curve *>(context.object->data);
const Curves *curves_id;
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_id = converted_curves.get();
break;
}
deadpin marked this conversation as resolved
Review

Naming would be more typical if curves was curves_id and geometry was curves.

Naming would be more typical if `curves` was `curves_id` and `geometry` was `curves`.
case OB_CURVES:
curves_id = static_cast<Curves *>(context.object->data);
break;
default:
BLI_assert_unreachable();
return;
}
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
if (curves.points_num() == 0) {
return;
}
/* Alembic only supports 1 curve type / periodicity combination per object. Enforce this here.
* See: Alembic source code for OCurves.h as no documentation explicitly exists for this. */
const std::array<int, CURVE_TYPES_NUM> &curve_type_counts = curves.curve_type_counts();
const int number_of_curve_types = std::count_if(curve_type_counts.begin(),
curve_type_counts.end(),
deadpin marked this conversation as resolved
Review

const

Also we have a super advanced utility for this :D array_utils::booleans_mix_calc

`const` Also we have a super advanced utility for this :D `array_utils::booleans_mix_calc`
Review

Huh... I would never have guessed with that name :)

Huh... I would never have guessed with that name :)
[](const int count) { return count > 0; });
if (number_of_curve_types > 1) {
CLOG_WARN(&LOG, "Cannot export mixed curve types in the same Curves object");
deadpin marked this conversation as resolved Outdated

Cyclic won't be empty here, since it gives you a varray with a single default value when the attribute doesn't exist.

Cyclic won't be empty here, since it gives you a varray with a single default value when the attribute doesn't exist.
return;
}
if (array_utils::booleans_mix_calc(curves.cyclic()) == array_utils::BooleanMix::Mixed) {
CLOG_WARN(&LOG, "Cannot export mixed cyclic and non-cyclic curves in the same Curves object");
return;
}
const bool is_cyclic = curves.cyclic().first();
Alembic::AbcGeom::BasisType curve_basis = Alembic::AbcGeom::kNoBasis;
deadpin marked this conversation as resolved Outdated

cast to CurveType for compiler warnings in the switch if we ever add a new type

cast to `CurveType` for compiler warnings in the switch if we ever add a new type
Alembic::AbcGeom::CurveType curve_type = Alembic::AbcGeom::kVariableOrder;
Alembic::AbcGeom::CurvePeriodicity periodicity = is_cyclic ? Alembic::AbcGeom::kPeriodic :
Alembic::AbcGeom::kNonPeriodic;
const CurveType blender_curve_type = CurveType(curves.curve_types().first());
switch (blender_curve_type) {
case CURVE_TYPE_POLY:
curve_basis = Alembic::AbcGeom::kNoBasis;
curve_type = Alembic::AbcGeom::kVariableOrder;
break;
case CURVE_TYPE_CATMULL_ROM:
curve_basis = Alembic::AbcGeom::kCatmullromBasis;
curve_type = Alembic::AbcGeom::kVariableOrder;
break;
case CURVE_TYPE_BEZIER:
curve_basis = Alembic::AbcGeom::kBezierBasis;
curve_type = Alembic::AbcGeom::kCubic;
break;
case CURVE_TYPE_NURBS:
curve_basis = Alembic::AbcGeom::kBsplineBasis;
curve_type = Alembic::AbcGeom::kVariableOrder;
break;
}
std::vector<Imath::V3f> verts;
std::vector<int32_t> vert_counts;
@ -64,85 +161,75 @@ void ABCCurveWriter::do_write(HierarchyContext &context)
std::vector<float> weights;
std::vector<float> knots;
std::vector<uint8_t> orders;
Imath::V3f temp_vert;
Alembic::AbcGeom::BasisType curve_basis = Alembic::AbcGeom::kNoBasis;
Alembic::AbcGeom::CurveType curve_type = Alembic::AbcGeom::kVariableOrder;
Alembic::AbcGeom::CurvePeriodicity periodicity = Alembic::AbcGeom::kNonPeriodic;
const Span<float3> positions = curves.positions();
deadpin marked this conversation as resolved Outdated

I think this should be lookup_or_default with a 0.01f default. Also, you can use the* operator on the returned attribute reader to just deal with the varray.

I think this should be `lookup_or_default` with a `0.01f` default. Also, you can use the`*` operator on the returned attribute reader to just deal with the varray.
const Span<float> nurbs_weights = curves.nurbs_weights();
const VArray<int8_t> nurbs_orders = curves.nurbs_orders();
const bke::AttributeAccessor curve_attributes = curves.attributes();
const VArray<float> radii = *curve_attributes.lookup_or_default<float>(
"radius", bke::AttrDomain::Point, 0.01f);
Nurb *nurbs = static_cast<Nurb *>(curve->nurb.first);
for (; nurbs; nurbs = nurbs->next) {
const size_t current_point_count = verts.size();
if (nurbs->bp) {
curve_basis = Alembic::AbcGeom::kNoBasis;
curve_type = Alembic::AbcGeom::kVariableOrder;
vert_counts.resize(curves.curves_num());
const OffsetIndices points_by_curve = curves.points_by_curve();
deadpin marked this conversation as resolved Outdated

Could you pull the type switch out of this loop? Then the three types except Bezier will become very simple array copies and that can be reflected in the way the code looks (you could even resize the std::vectors to avoid reallocations)

Could you pull the type switch out of this loop? Then the three types except Bezier will become very simple array copies and that can be reflected in the way the code looks (you could even resize the std::vectors to avoid reallocations)

I think I can. The code is still a bit complicated though, maybe I'm going about it wrong? Here's a snippet of what it would look like to see if I'm on the right track. Notice that the orders and vert_counts arrays need filled in differently too.

  if (blender_curve_type == CURVE_TYPE_BEZIER) {
    ...
  }
  else {
    verts.resize(curves.points_num());    // can probably stick with reserve and push_back but...
    widths.resize(curves.points_num()); // ...this was just a test
    for (const int i_point : curves.points_range()) {
      verts[i_point] = to_yup_V3f(positions[i_point]);
      widths[i_point] = radii.varray[i_point] * 2.0f;
    }

    if (blender_curve_type == CURVE_TYPE_NURBS) {
      weights.resize(curves.points_num());
      std::copy_n(nurbs_weights.data(), weights.size(), weights.data());
    }

    orders.reserve(curves.curves_num());
    vert_counts.reserve(curves.curves_num());
    for (const int i_curve : curves.curves_range()) {
      orders.push_back(nurbs_orders[i_curve]);
      vert_counts.push_back(points_by_curve[i_curve].size());
    }
  }
I think I can. The code is still a bit complicated though, maybe I'm going about it wrong? Here's a snippet of what it would look like to see if I'm on the right track. Notice that the `orders` and `vert_counts` arrays need filled in differently too. ```cpp if (blender_curve_type == CURVE_TYPE_BEZIER) { ... } else { verts.resize(curves.points_num()); // can probably stick with reserve and push_back but... widths.resize(curves.points_num()); // ...this was just a test for (const int i_point : curves.points_range()) { verts[i_point] = to_yup_V3f(positions[i_point]); widths[i_point] = radii.varray[i_point] * 2.0f; } if (blender_curve_type == CURVE_TYPE_NURBS) { weights.resize(curves.points_num()); std::copy_n(nurbs_weights.data(), weights.size(), weights.data()); } orders.reserve(curves.curves_num()); vert_counts.reserve(curves.curves_num()); for (const int i_curve : curves.curves_range()) { orders.push_back(nurbs_orders[i_curve]); vert_counts.push_back(points_by_curve[i_curve].size()); } } ```

Yeah, something like that makes more sense I think.

I wouldn't expect push_back to be necessary for the loops at all TBH. Just copy_n like you use in on place (except for positions and radii which need special handling I suppose).

Also, seems like nurbs_orders could only be read from CurvesGeometry if it is exporting NURBS curves.

And also, vert_counts can be done like this: offset_indices::copy_group_sizes(points_by_curve, points_by_curve.index_range(), vert_counts);

Yeah, something like that makes more sense I think. I wouldn't expect `push_back` to be necessary for the loops at all TBH. Just `copy_n` like you use in on place (except for positions and radii which need special handling I suppose). Also, seems like `nurbs_orders` could only be read from `CurvesGeometry` if it is exporting NURBS curves. And also, `vert_counts` can be done like this: `offset_indices::copy_group_sizes(points_by_curve, points_by_curve.index_range(), vert_counts);`
if (blender_curve_type == CURVE_TYPE_BEZIER) {
const Span<float3> handles_l = curves.handle_positions_left();
const Span<float3> handles_r = curves.handle_positions_right();
const int totpoint = nurbs->pntsu * nurbs->pntsv;
for (const int i_curve : curves.curves_range()) {
const IndexRange points = points_by_curve[i_curve];
const size_t current_vert_count = verts.size();
const BPoint *point = nurbs->bp;
const int start_point_index = points.first();
const int last_point_index = points.last();
for (int i = 0; i < totpoint; i++, point++) {
copy_yup_from_zup(temp_vert.getValue(), point->vec);
verts.push_back(temp_vert);
weights.push_back(point->vec[3]);
widths.push_back(point->radius);
/* Vert order in the bezier curve representation is:
* [
* control point 0(+ width), right handle 0, left handle 1,
* control point 1(+ width), right handle 1, left handle 2,
* control point 2(+ width), ...
* ] */
for (const int i_point : points.drop_back(1)) {
verts.push_back(to_yup_V3f(positions[i_point]));
deadpin marked this conversation as resolved
Review

Just noticed, I think this can be simpler as for (const int point : points) {

Just noticed, I think this can be simpler as `for (const int point : points) {`
widths.push_back(radii[i_point] * 2.0f);
deadpin marked this conversation as resolved
Review

Seems the radius should be accessed from the current point rather than always the last point in the curve

Seems the radius should be accessed from the current point rather than always the last point in the curve
verts.push_back(to_yup_V3f(handles_r[i_point]));
verts.push_back(to_yup_V3f(handles_l[i_point + 1]));
}
/* The last vert in the array doesn't need a right handle because the curve stops
* at that point. */
verts.push_back(to_yup_V3f(positions[last_point_index]));
widths.push_back(radii[last_point_index] * 2.0f);
/* If the curve is cyclic, include the right handle of the last point and the
* left handle of the first point. */
if (is_cyclic) {
verts.push_back(to_yup_V3f(handles_r[last_point_index]));
verts.push_back(to_yup_V3f(handles_l[start_point_index]));
}
vert_counts[i_curve] = verts.size() - current_vert_count;
}
}
else {
verts.resize(curves.points_num());
deadpin marked this conversation as resolved Outdated

same break formatting thing here

same break formatting thing here
widths.resize(curves.points_num());
for (const int i_point : curves.points_range()) {
verts[i_point] = to_yup_V3f(positions[i_point]);
widths[i_point] = radii[i_point] * 2.0f;
}
else if (nurbs->bezt) {
curve_basis = Alembic::AbcGeom::kBezierBasis;
curve_type = Alembic::AbcGeom::kCubic;
const int totpoint = nurbs->pntsu;
if (blender_curve_type == CURVE_TYPE_NURBS) {
weights.resize(curves.points_num());
std::copy_n(nurbs_weights.data(), weights.size(), weights.data());
const BezTriple *bezier = nurbs->bezt;
/* TODO(kevin): store info about handles, Alembic doesn't have this. */
for (int i = 0; i < totpoint; i++, bezier++) {
copy_yup_from_zup(temp_vert.getValue(), bezier->vec[1]);
verts.push_back(temp_vert);
widths.push_back(bezier->radius);
orders.resize(curves.curves_num());
for (const int i_curve : curves.curves_range()) {
orders[i_curve] = nurbs_orders[i_curve];
}
}
if ((nurbs->flagu & CU_NURB_ENDPOINT) != 0) {
periodicity = Alembic::AbcGeom::kNonPeriodic;
}
else if ((nurbs->flagu & CU_NURB_CYCLIC) != 0) {
periodicity = Alembic::AbcGeom::kPeriodic;
/* Duplicate the start points to indicate that the curve is actually
* cyclic since other software need those.
*/
for (int i = 0; i < nurbs->orderu; i++) {
verts.push_back(verts[i]);
}
}
if (nurbs->knotsu != nullptr) {
const size_t num_knots = KNOTSU(nurbs);
/* Add an extra knot at the beginning and end of the array since most apps
* require/expect them. */
knots.resize(num_knots + 2);
for (int i = 0; i < num_knots; i++) {
knots[i + 1] = nurbs->knotsu[i];
}
if ((nurbs->flagu & CU_NURB_CYCLIC) != 0) {
knots[0] = nurbs->knotsu[0];
knots[num_knots - 1] = nurbs->knotsu[num_knots - 1];
}
else {
knots[0] = (2.0f * nurbs->knotsu[0] - nurbs->knotsu[1]);
knots[num_knots - 1] = (2.0f * nurbs->knotsu[num_knots - 1] -
nurbs->knotsu[num_knots - 2]);
}
}
orders.push_back(uint8_t(nurbs->orderu));
vert_counts.push_back(verts.size() - current_point_count);
offset_indices::copy_group_sizes(points_by_curve, points_by_curve.index_range(), vert_counts);
}
Alembic::AbcGeom::OFloatGeomParam::Sample width_sample;
@ -172,15 +259,27 @@ ABCCurveMeshWriter::ABCCurveMeshWriter(const ABCWriterConstructorArgs &args)
Mesh *ABCCurveMeshWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
Mesh *mesh_eval = BKE_object_get_evaluated_mesh(object_eval);
if (mesh_eval != nullptr) {
/* Mesh_eval only exists when generative modifiers are in use. */
r_needsfree = false;
return mesh_eval;
switch (object_eval->type) {
case OB_CURVES_LEGACY: {
Mesh *mesh_eval = BKE_object_get_evaluated_mesh(object_eval);
if (mesh_eval != nullptr) {
/* Mesh_eval only exists when generative modifiers are in use. */
r_needsfree = false;
return mesh_eval;
}
r_needsfree = true;
return BKE_mesh_new_nomain_from_curve(object_eval);
}
case OB_CURVES:
const bke::AnonymousAttributePropagationInfo propagation_info;
Curves *curves = static_cast<Curves *>(object_eval->data);
r_needsfree = true;
return bke::curve_to_wire_mesh(curves->geometry.wrap(), propagation_info);
}
r_needsfree = true;
return BKE_mesh_new_nomain_from_curve(object_eval);
return nullptr;
}
} // namespace blender::io::alembic

View File

@ -45,12 +45,12 @@ static int16_t get_curve_resolution(const ICurvesSchema &schema,
{
ICompoundProperty user_props = schema.getUserProperties();
if (!user_props) {
return 1;
return 0;
}
const PropertyHeader *header = user_props.getPropertyHeader(ABC_CURVE_RESOLUTION_U_PROPNAME);
if (!header || !header->isScalar() || !IInt16Property::matches(*header)) {
return 1;
return 0;
}
IInt16Property resolu(user_props, header->getName());
@ -75,17 +75,20 @@ static int16_t get_curve_order(const Alembic::AbcGeom::CurveType abc_curve_type,
}
}
static int get_curve_overlap(const Alembic::AbcGeom::CurvePeriodicity periodicity,
const P3fArraySamplePtr positions,
static int8_t get_knot_mode(const Alembic::AbcGeom::CurveType abc_curve_type)
{
if (abc_curve_type == Alembic::AbcGeom::kCubic) {
return NURBS_KNOT_MODE_ENDPOINT;
}
return NURBS_KNOT_MODE_NORMAL;
}
static int get_curve_overlap(const P3fArraySamplePtr positions,
const int idx,
const int num_verts,
const int16_t order)
{
if (periodicity != Alembic::AbcGeom::kPeriodic) {
/* kNonPeriodic is always assumed to have no overlap. */
return 0;
}
/* Check the number of points which overlap, we don't have overlapping points in Blender, but
* other software do use them to indicate that a curve is actually cyclic. Usually the number of
* overlapping points is equal to the order/degree of the curve.
@ -112,7 +115,6 @@ static int get_curve_overlap(const Alembic::AbcGeom::CurvePeriodicity periodicit
overlap = 1;
}
/* There is no real cycles. */
return overlap;
}
@ -135,6 +137,18 @@ static CurveType get_curve_type(const Alembic::AbcGeom::BasisType basis)
return CURVE_TYPE_POLY;
}
static inline int bezier_point_count(int alembic_count, bool is_cyclic)
{
return is_cyclic ? (alembic_count / 3) : ((alembic_count / 3) + 1);
}
static inline float3 to_zup_float3(Imath::V3f v)
{
float3 p;
copy_zup_from_yup(p, v.getValue());
return p;
}
static bool curves_topology_changed(const bke::CurvesGeometry &curves,
Span<int> preprocessed_offsets)
{
@ -165,7 +179,8 @@ struct PreprocessedSampleData {
bool do_cyclic = false;
/* Only one curve type for the whole objects. */
CurveType curve_type;
CurveType curve_type = CURVE_TYPE_POLY;
int8_t knot_mode = 0;
/* Store the pointers during preprocess so we do not have to look up the sample twice. */
P3fArraySamplePtr positions = nullptr;
@ -217,6 +232,8 @@ static std::optional<PreprocessedSampleData> preprocess_sample(StringRefNull iob
data.offset_in_alembic.resize(curve_count + 1);
data.curves_cyclic.resize(curve_count);
data.curve_type = get_curve_type(smp.getBasis());
data.knot_mode = get_knot_mode(smp.getType());
data.do_cyclic = periodicity == Alembic::AbcGeom::kPeriodic;
if (data.curve_type == CURVE_TYPE_NURBS) {
data.curves_orders.resize(curve_count);
@ -231,20 +248,28 @@ static std::optional<PreprocessedSampleData> preprocess_sample(StringRefNull iob
const int curve_order = get_curve_order(smp.getType(), orders, i);
/* Check if the curve is cyclic. */
const int overlap = get_curve_overlap(
periodicity, positions, alembic_offset, vertices_count, curve_order);
data.offset_in_blender[i] = blender_offset;
data.offset_in_alembic[i] = alembic_offset;
data.curves_cyclic[i] = overlap != 0;
data.curves_cyclic[i] = data.do_cyclic;
if (data.curve_type == CURVE_TYPE_NURBS) {
data.curves_orders[i] = curve_order;
}
data.do_cyclic |= data.curves_cyclic[i];
blender_offset += (overlap >= vertices_count) ? vertices_count : (vertices_count - overlap);
/* Some software writes repeated vertices to indicate periodicity but Blender
* should skip these if present. */
const int overlap = data.do_cyclic ?
get_curve_overlap(
positions, alembic_offset, vertices_count, curve_order) :
0;
if (data.curve_type == CURVE_TYPE_BEZIER) {
blender_offset += bezier_point_count(vertices_count, data.do_cyclic);
}
else {
blender_offset += (overlap >= vertices_count) ? vertices_count : (vertices_count - overlap);
}
alembic_offset += vertices_count;
}
data.offset_in_blender[curve_count] = blender_offset;
@ -313,6 +338,30 @@ void AbcCurveReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSele
}
}
static void add_bezier_control_point(int cp,
int offset,
const Span<Imath::V3f> alembic_positions,
MutableSpan<float3> positions,
MutableSpan<float3> handles_left,
MutableSpan<float3> handles_right)
deadpin marked this conversation as resolved Outdated

Typically I've seen the MutableSpan arguments come last compared to the non-mutable arguments. It's arbitrary, but worth being consistent about maybe.

Typically I've seen the `MutableSpan` arguments come last compared to the non-mutable arguments. It's arbitrary, but worth being consistent about maybe.
{
if (offset == 0) {
positions[cp] = to_zup_float3(alembic_positions[offset]);
handles_right[cp] = to_zup_float3(alembic_positions[offset + 1]);
handles_left[cp] = 2.0f * positions[cp] - handles_right[cp];
}
else if (offset == alembic_positions.size() - 1) {
positions[cp] = to_zup_float3(alembic_positions[offset]);
handles_left[cp] = to_zup_float3(alembic_positions[offset - 1]);
handles_right[cp] = 2.0f * positions[cp] - handles_left[cp];
}
else {
positions[cp] = to_zup_float3(alembic_positions[offset]);
handles_left[cp] = to_zup_float3(alembic_positions[offset - 1]);
handles_right[cp] = to_zup_float3(alembic_positions[offset + 1]);
}
}
void AbcCurveReader::read_curves_sample(Curves *curves_id,
const ICurvesSchema &schema,
const ISampleSelector &sample_sel)
@ -338,22 +387,53 @@ void AbcCurveReader::read_curves_sample(Curves *curves_id,
curves.fill_curve_types(data.curve_type);
if (data.curve_type != CURVE_TYPE_POLY) {
curves.resolution_for_write().fill(get_curve_resolution(schema, sample_sel));
int16_t curve_resolution = get_curve_resolution(schema, sample_sel);
if (curve_resolution > 0) {
curves.resolution_for_write().fill(curve_resolution);
}
}
MutableSpan<float3> curves_positions = curves.positions_for_write();
for (const int i_curve : curves.curves_range()) {
int position_offset = data.offset_in_alembic[i_curve];
for (const int i_point : curves.points_by_curve()[i_curve]) {
const Imath::V3f &pos = (*data.positions)[position_offset++];
copy_zup_from_yup(curves_positions[i_point], pos.getValue());
Span<Imath::V3f> alembic_points{&(*data.positions)[0], int64_t((*data.positions).size())};
if (data.curve_type == CURVE_TYPE_BEZIER) {
curves.handle_types_left_for_write().fill(BEZIER_HANDLE_ALIGN);
curves.handle_types_right_for_write().fill(BEZIER_HANDLE_ALIGN);
MutableSpan<float3> handles_right = curves.handle_positions_right_for_write();
MutableSpan<float3> handles_left = curves.handle_positions_left_for_write();
int point_offset = 0;
for (const int i_curve : curves.curves_range()) {
const int alembic_point_offset = data.offset_in_alembic[i_curve];
const int alembic_point_count = data.offset_in_alembic[i_curve + 1] - alembic_point_offset;
const int cp_count = data.offset_in_blender[i_curve + 1] - data.offset_in_blender[i_curve];
int cp_offset = 0;
for (const int cp : IndexRange(cp_count)) {
add_bezier_control_point(cp,
cp_offset,
alembic_points.slice(alembic_point_offset, alembic_point_count),
curves_positions.slice(point_offset, point_count),
handles_left.slice(point_offset, point_count),
handles_right.slice(point_offset, point_count));
cp_offset += 3;
}
point_offset += cp_count;
}
}
else {
for (const int i_curve : curves.curves_range()) {
int position_offset = data.offset_in_alembic[i_curve];
for (const int i_point : curves.points_by_curve()[i_curve]) {
curves_positions[i_point] = to_zup_float3(alembic_points[position_offset++]);
}
}
}
if (data.do_cyclic) {
curves.cyclic_for_write().copy_from(data.curves_cyclic);
curves.handle_types_left_for_write().fill(BEZIER_HANDLE_AUTO);
curves.handle_types_right_for_write().fill(BEZIER_HANDLE_AUTO);
}
if (data.radii) {
@ -361,11 +441,9 @@ void AbcCurveReader::read_curves_sample(Curves *curves_id,
curves.attributes_for_write().lookup_or_add_for_write_span<float>("radius",
bke::AttrDomain::Point);
for (const int i_curve : curves.curves_range()) {
int position_offset = data.offset_in_alembic[i_curve];
for (const int i_point : curves.points_by_curve()[i_curve]) {
radii.span[i_point] = (*data.radii)[position_offset++];
}
Alembic::Abc::FloatArraySample alembic_widths = *data.radii;
for (const int i_point : curves.points_range()) {
radii.span[i_point] = alembic_widths[i_point] / 2.0f;
}
radii.finish();
@ -373,6 +451,7 @@ void AbcCurveReader::read_curves_sample(Curves *curves_id,
if (data.curve_type == CURVE_TYPE_NURBS) {
curves.nurbs_orders_for_write().copy_from(data.curves_orders);
curves.nurbs_knots_modes_for_write().fill(data.knot_mode);
if (data.weights) {
MutableSpan<float> curves_weights = curves.nurbs_weights_for_write();