Alembic export: write custom properties

Write custom properties (aka ID properties) to Alembic, to the
`.userProperties` compound property.

Manifest Task: https://developer.blender.org/T50725

Scalar properties (so single-value/non-array properties) are written as
single-element array properties to Alembic. This is also what's done by
Houdini and Maya exporters, so it seems to be the standard way of doing
things. It also simplifies the implementation.

Two-dimensional arrays are flattened by concatenating all the numbers
into a single array. This is because ID properties have a limited type
system. This means that a 3x3 "matrix" could just as well be a list of
three 3D vectors.

Alembic has two container properties to store custom data:
- `.userProperties`, which is meant for properties that aren't
  necessarily understood by other software packages, and
- `.arbGeomParams`, which can contain the same kind of data as
  `.userProperties`, but can also specify that these vary per face of a
  mesh. This property is mostly intended for renderers.

Most industry packages write their custom data to `.arbGeomParams`.
However, given their goals I feel that `.userProperties` is the more
appropriate one for Blender's ID Properties.

The code is a bit more involved than I would have liked. An
`ABCAbstractWriter` has a `uniqueptr` to its `CustomPropertiesExporter`,
but the `CustomPropertiesExporter` also has a pointer back to its owning
`ABCAbstractWriter`. It's the latter pointer that I'm not too happy
with, but it has a reason. Getting the aforementioned `.userProperties`
from the Alembic library will automatically create it if it doesn't
exist already. If it's not used to actually add custom properties to, it
will crash the Alembic CLI tools (and maybe others too). This is what
the pointer back to the `ABCAbstractWriter` is used for: to get the
`.userProperties` at the last moment, when it's 100% sure at least one
custom property will be written.

Differential Revision: https://developer.blender.org/D8869

Reviewed by: sergey, dbystedt
This commit is contained in:
Sybren A. Stüvel 2020-09-11 14:06:13 +02:00
parent b8a25bbd8a
commit ee97add4c4
24 changed files with 594 additions and 0 deletions

View File

@ -133,6 +133,7 @@ static int wm_alembic_export_exec(bContext *C, wmOperator *op)
.use_subdiv_schema = RNA_boolean_get(op->ptr, "subdiv_schema"),
.export_hair = RNA_boolean_get(op->ptr, "export_hair"),
.export_particles = RNA_boolean_get(op->ptr, "export_particles"),
.export_custom_properties = RNA_boolean_get(op->ptr, "export_custom_properties"),
.use_instancing = RNA_boolean_get(op->ptr, "use_instancing"),
.packuv = RNA_boolean_get(op->ptr, "packuv"),
.triangulate = RNA_boolean_get(op->ptr, "triangulate"),
@ -191,6 +192,8 @@ static void ui_alembic_export_settings(uiLayout *layout, PointerRNA *imfptr)
uiItemR(col, imfptr, "flatten", 0, NULL, ICON_NONE);
uiItemR(sub, imfptr, "use_instancing", 0, IFACE_("Use Instancing"), ICON_NONE);
uiItemR(sub, imfptr, "export_custom_properties", 0, IFACE_("Custom Properties"), ICON_NONE);
sub = uiLayoutColumnWithHeading(col, true, IFACE_("Only"));
uiItemR(sub, imfptr, "selected", 0, IFACE_("Selected Objects"), ICON_NONE);
uiItemR(sub, imfptr, "renderable_only", 0, IFACE_("Renderable Objects"), ICON_NONE);
@ -449,6 +452,12 @@ void WM_OT_alembic_export(wmOperatorType *ot)
RNA_def_boolean(
ot->srna, "export_particles", 1, "Export Particles", "Exports non-hair particle systems");
RNA_def_boolean(ot->srna,
"export_custom_properties",
true,
"Export Custom Properties",
"Export custom properties to Alembic .userProperties");
RNA_def_boolean(
ot->srna,
"as_background_job",

View File

@ -60,6 +60,7 @@ struct AlembicExportParams {
bool triangulate;
bool export_hair;
bool export_particles;
bool export_custom_properties;
bool use_instancing;
/* See MOD_TRIANGULATE_NGON_xxx and MOD_TRIANGULATE_QUAD_xxx

View File

@ -56,6 +56,7 @@ set(SRC
intern/alembic_capi.cc
exporter/abc_archive.cc
exporter/abc_custom_props.cc
exporter/abc_export_capi.cc
exporter/abc_hierarchy_iterator.cc
exporter/abc_subdiv_disabler.cc
@ -84,6 +85,7 @@ set(SRC
intern/abc_util.h
exporter/abc_archive.h
exporter/abc_custom_props.h
exporter/abc_hierarchy_iterator.h
exporter/abc_subdiv_disabler.h
exporter/abc_writer_abstract.h

View File

@ -0,0 +1,268 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2020 Blender Foundation.
* All rights reserved.
*/
/** \file
* \ingroup Alembic
*/
#include "abc_custom_props.h"
#include "abc_writer_abstract.h"
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <Alembic/Abc/OTypedArrayProperty.h>
#include <Alembic/Abc/OTypedScalarProperty.h>
#include "BKE_idprop.h"
#include "DNA_ID.h"
using Alembic::Abc::ArraySample;
using Alembic::Abc::OArrayProperty;
using Alembic::Abc::OBoolArrayProperty;
using Alembic::Abc::OCompoundProperty;
using Alembic::Abc::ODoubleArrayProperty;
using Alembic::Abc::OFloatArrayProperty;
using Alembic::Abc::OInt32ArrayProperty;
using Alembic::Abc::OStringArrayProperty;
namespace blender::io::alembic {
CustomPropertiesExporter::CustomPropertiesExporter(ABCAbstractWriter *owner) : owner_(owner)
{
}
CustomPropertiesExporter::~CustomPropertiesExporter()
{
}
void CustomPropertiesExporter::write_all(const IDProperty *group)
{
if (group == nullptr) {
return;
}
BLI_assert(group->type == IDP_GROUP);
/* Loop over the properties, just like IDP_foreach_property() does, but without the recursion. */
LISTBASE_FOREACH (IDProperty *, id_property, &group->data.group) {
if (STREQ(id_property->name, "_RNA_UI")) {
continue;
}
write(id_property);
}
}
void CustomPropertiesExporter::write(const IDProperty *id_property)
{
BLI_assert(id_property->name[0] != '\0');
switch (id_property->type) {
case IDP_STRING: {
/* The Alembic library doesn't accept NULL-terminated character arrays. */
const std::string prop_value(IDP_String(id_property), id_property->len - 1);
set_scalar_property<OStringArrayProperty, std::string>(id_property->name, prop_value);
break;
}
case IDP_INT:
static_assert(sizeof(int) == sizeof(int32_t), "Expecting 'int' to be 32-bit");
set_scalar_property<OInt32ArrayProperty, int32_t>(id_property->name, IDP_Int(id_property));
break;
case IDP_FLOAT:
set_scalar_property<OFloatArrayProperty, float>(id_property->name, IDP_Float(id_property));
break;
case IDP_DOUBLE:
set_scalar_property<ODoubleArrayProperty, double>(id_property->name,
IDP_Double(id_property));
break;
case IDP_ARRAY:
write_array(id_property);
break;
case IDP_IDPARRAY:
write_idparray(id_property);
break;
}
}
void CustomPropertiesExporter::write_array(const IDProperty *id_property)
{
BLI_assert(id_property->type == IDP_ARRAY);
switch (id_property->subtype) {
case IDP_INT: {
const int *array = (int *)IDP_Array(id_property);
static_assert(sizeof(int) == sizeof(int32_t), "Expecting 'int' to be 32-bit");
set_array_property<OInt32ArrayProperty, int32_t>(id_property->name, array, id_property->len);
break;
}
case IDP_FLOAT: {
const float *array = (float *)IDP_Array(id_property);
set_array_property<OFloatArrayProperty, float>(id_property->name, array, id_property->len);
break;
}
case IDP_DOUBLE: {
const double *array = (double *)IDP_Array(id_property);
set_array_property<ODoubleArrayProperty, double>(id_property->name, array, id_property->len);
break;
}
}
}
void CustomPropertiesExporter::write_idparray(const IDProperty *idp_array)
{
BLI_assert(idp_array->type == IDP_IDPARRAY);
if (idp_array->len == 0) {
/* Don't bother writing dataless arrays. */
return;
}
IDProperty *idp_elements = (IDProperty *)IDP_Array(idp_array);
#ifndef NDEBUG
/* Sanity check that all elements of the array have the same type.
* Blender should already enforce this, hence it's only used in debug mode. */
for (int i = 1; i < idp_array->len; i++) {
if (idp_elements[i].type == idp_elements[0].type) {
continue;
}
std::cerr << "Custom property " << idp_array->name << " has elements of varying type";
BLI_assert(!"Mixed type IDP_ARRAY custom property found");
}
#endif
switch (idp_elements[0].type) {
case IDP_STRING:
write_idparray_of_strings(idp_array);
break;
case IDP_ARRAY:
write_idparray_of_numbers(idp_array);
break;
}
}
void CustomPropertiesExporter::write_idparray_of_strings(const IDProperty *idp_array)
{
BLI_assert(idp_array->type == IDP_IDPARRAY);
BLI_assert(idp_array->len > 0);
/* Convert to an array of std::strings, because Alembic doesn't like zero-delimited strings. */
IDProperty *idp_elements = (IDProperty *)IDP_Array(idp_array);
std::vector<std::string> strings(idp_array->len);
for (int i = 0; i < idp_array->len; i++) {
BLI_assert(idp_elements[i].type == IDP_STRING);
strings[i] = IDP_String(&idp_elements[i]);
}
/* Alembic needs a pointer to the first value of the array. */
const std::string *array_of_strings = &strings[0];
set_array_property<OStringArrayProperty, std::string>(
idp_array->name, array_of_strings, strings.size());
}
void CustomPropertiesExporter::write_idparray_of_numbers(const IDProperty *idp_array)
{
BLI_assert(idp_array->type == IDP_IDPARRAY);
BLI_assert(idp_array->len > 0);
/* This must be an array of arrays. */
IDProperty *idp_rows = (IDProperty *)IDP_Array(idp_array);
BLI_assert(idp_rows[0].type == IDP_ARRAY);
const int subtype = idp_rows[0].subtype;
if (!ELEM(subtype, IDP_INT, IDP_FLOAT, IDP_DOUBLE)) {
/* Non-numerical types are not supported. */
return;
}
switch (subtype) {
case IDP_INT:
static_assert(sizeof(int) == sizeof(int32_t), "Expecting 'int' to be 32-bit");
write_idparray_flattened_typed<OInt32ArrayProperty, int32_t>(idp_array);
break;
case IDP_FLOAT:
write_idparray_flattened_typed<OFloatArrayProperty, float>(idp_array);
break;
case IDP_DOUBLE:
write_idparray_flattened_typed<ODoubleArrayProperty, double>(idp_array);
break;
}
}
template<typename ABCPropertyType, typename BlenderValueType>
void CustomPropertiesExporter::write_idparray_flattened_typed(const IDProperty *idp_array)
{
BLI_assert(idp_array->type == IDP_IDPARRAY);
BLI_assert(idp_array->len > 0);
const IDProperty *idp_rows = (IDProperty *)IDP_Array(idp_array);
BLI_assert(idp_rows[0].type == IDP_ARRAY);
BLI_assert(ELEM(idp_rows[0].subtype, IDP_INT, IDP_FLOAT, IDP_DOUBLE));
const uint64_t num_rows = idp_array->len;
std::vector<BlenderValueType> matrix_values;
for (size_t row_idx = 0; row_idx < num_rows; ++row_idx) {
const BlenderValueType *row = (BlenderValueType *)IDP_Array(&idp_rows[row_idx]);
for (size_t col_idx = 0; col_idx < idp_rows[row_idx].len; col_idx++) {
matrix_values.push_back(row[col_idx]);
}
}
set_array_property<ABCPropertyType, BlenderValueType>(
idp_array->name, &matrix_values[0], matrix_values.size());
}
template<typename ABCPropertyType, typename BlenderValueType>
void CustomPropertiesExporter::set_scalar_property(const StringRef property_name,
const BlenderValueType property_value)
{
set_array_property<ABCPropertyType, BlenderValueType>(property_name, &property_value, 1);
}
template<typename ABCPropertyType, typename BlenderValueType>
void CustomPropertiesExporter::set_array_property(const StringRef property_name,
const BlenderValueType *array_values,
const size_t num_array_items)
{
auto create_callback = [this, property_name]() -> OArrayProperty {
return create_abc_property<ABCPropertyType>(property_name);
};
OArrayProperty array_prop = abc_properties_.lookup_or_add_cb(property_name, create_callback);
Alembic::Util::Dimensions array_dimensions(num_array_items);
ArraySample sample(array_values, array_prop.getDataType(), array_dimensions);
array_prop.set(sample);
}
template<typename ABCPropertyType>
OArrayProperty CustomPropertiesExporter::create_abc_property(const StringRef property_name)
{
/* Get the necessary info from our owner. */
OCompoundProperty abc_prop_for_custom_props = owner_->abc_prop_for_custom_props();
const uint32_t timesample_index = owner_->timesample_index();
/* Construct the Alembic property. */
ABCPropertyType abc_property(abc_prop_for_custom_props, property_name);
abc_property.setTimeSampling(timesample_index);
return abc_property;
}
} // namespace blender::io::alembic

View File

@ -0,0 +1,96 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* The Original Code is Copyright (C) 2020 Blender Foundation.
* All rights reserved.
*/
/** \file
* \ingroup Alembic
*/
#pragma once
#include <Alembic/Abc/OArrayProperty.h>
#include <Alembic/Abc/OCompoundProperty.h>
#include "BLI_map.hh"
#include <memory>
struct IDProperty;
namespace blender::io::alembic {
class ABCAbstractWriter;
/* Write values of Custom Properties (a.k.a. ID Properties) to Alembic.
*
* Each Alembic Writer instance optionally has one CustomPropertiesExporter (CPE). This CPE not
* only writes the custom properties to Alembic, but also keeps references in memory so that the
* Alembic library doesn't prematurely finalize the data. */
class CustomPropertiesExporter {
private:
/* Owner is used to get the OCompoundProperty and time sample index. The former should only be
* requested from the Alembic library when it's actually going to be used to add custom
* properties (otherwise an invalid Alembic file is written). */
ABCAbstractWriter *owner_;
/* The Compound Property that will contain the exported custom properties.
*
* Typically this the return value of abc_schema.getArbGeomParams() or
* abc_schema.getUserProperties(). */
Alembic::Abc::OCompoundProperty abc_compound_prop_;
/* Mapping from property name in Blender to property in Alembic.
* Here Blender does the same as other software (Maya, Houdini), and writes
* scalar properties as single-element arrays. */
Map<std::string, Alembic::Abc::OArrayProperty> abc_properties_;
public:
CustomPropertiesExporter(ABCAbstractWriter *owner);
virtual ~CustomPropertiesExporter();
void write_all(const IDProperty *group);
private:
void write(const IDProperty *id_property);
void write_array(const IDProperty *id_property);
/* IDProperty arrays are used to store arrays-of-arrays or arrays-of-strings. */
void write_idparray(const IDProperty *idp_array);
void write_idparray_of_strings(const IDProperty *idp_array);
void write_idparray_of_numbers(const IDProperty *idp_array);
/* Flatten an array-of-arrays into one long array, then write that.
* It is tempting to write an array of NxM numbers as a matrix, but there is
* no guarantee that the data actually represents a matrix. */
template<typename ABCPropertyType, typename BlenderValueType>
void write_idparray_flattened_typed(const IDProperty *idp_array);
/* Write a single scalar (i.e. non-array) property as single-value array. */
template<typename ABCPropertyType, typename BlenderValueType>
void set_scalar_property(StringRef property_name, const BlenderValueType property_value);
template<typename ABCPropertyType, typename BlenderValueType>
void set_array_property(StringRef property_name,
const BlenderValueType *array_values,
size_t num_array_items);
template<typename ABCPropertyType>
Alembic::Abc::OArrayProperty create_abc_property(StringRef property_name);
};
} // namespace blender::io::alembic

View File

@ -59,6 +59,7 @@ void ABCAbstractWriter::write(HierarchyContext &context)
if (!frame_has_been_written_) {
is_animated_ = (args_.export_params->frame_start != args_.export_params->frame_end) &&
check_is_animated(context);
ensure_custom_properties_exporter(context);
}
else if (!is_animated_) {
/* A frame has already been written, and without animation one frame is enough. */
@ -67,9 +68,49 @@ void ABCAbstractWriter::write(HierarchyContext &context)
do_write(context);
if (custom_props_) {
custom_props_->write_all(get_id_properties(context));
}
frame_has_been_written_ = true;
}
void ABCAbstractWriter::ensure_custom_properties_exporter(const HierarchyContext &context)
{
if (!args_.export_params->export_custom_properties) {
return;
}
if (custom_props_) {
/* Custom properties exporter already created. */
return;
}
/* Avoid creating a custom properties exporter if there are no custom properties to export. */
const IDProperty *id_properties = get_id_properties(context);
if (id_properties == nullptr || id_properties->len == 0) {
return;
}
custom_props_ = std::make_unique<CustomPropertiesExporter>(this);
}
const IDProperty *ABCAbstractWriter::get_id_properties(const HierarchyContext &context) const
{
Object *object = context.object;
if (object->data == nullptr) {
return nullptr;
}
/* Most subclasses write object data, so default to the object data's ID properties. */
return static_cast<ID *>(object->data)->properties;
}
uint32_t ABCAbstractWriter::timesample_index() const
{
return timesample_index_;
}
const Imath::Box3d &ABCAbstractWriter::bounding_box() const
{
return bounding_box_;

View File

@ -19,6 +19,7 @@
#pragma once
#include "IO_abstract_hierarchy_iterator.h"
#include "abc_custom_props.h"
#include "abc_hierarchy_iterator.h"
#include <Alembic/Abc/OObject.h>
@ -27,6 +28,7 @@
#include "DEG_depsgraph_query.h"
#include "DNA_material_types.h"
struct IDProperty;
struct Material;
struct Object;
@ -44,6 +46,9 @@ class ABCAbstractWriter : public AbstractHierarchyWriter {
/* Visibility of this writer's data in Alembic. */
Alembic::Abc::OCharProperty abc_visibility_;
/* Optional writer for custom properties. */
std::unique_ptr<CustomPropertiesExporter> custom_props_;
public:
explicit ABCAbstractWriter(const ABCWriterConstructorArgs &args);
virtual ~ABCAbstractWriter();
@ -59,6 +64,7 @@ class ABCAbstractWriter : public AbstractHierarchyWriter {
* Empty). */
virtual bool is_supported(const HierarchyContext *context) const;
uint32_t timesample_index() const;
const Imath::Box3d &bounding_box() const;
/* Called by AlembicHierarchyCreator after checking that the data is supported via
@ -67,12 +73,47 @@ class ABCAbstractWriter : public AbstractHierarchyWriter {
virtual Alembic::Abc::OObject get_alembic_object() const = 0;
/* Return the Alembic object's CompoundProperty that'll contain the custom properties.
*
* This function is called whenever there are custom properties to be written to Alembic. It
* should call abc_schema_prop_for_custom_props() with the writer's Alembic schema object.
*
* If custom properties are not supported by a specific subclass, it should return an empty
* OCompoundProperty() and override ensure_custom_properties_exporter() to do nothing.
*/
virtual Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() = 0;
protected:
virtual void do_write(HierarchyContext &context) = 0;
virtual void update_bounding_box(Object *object);
/* Return ID properties of whatever ID datablock is written by this writer. Defaults to the
* properties of the object data. Can return nullptr if no custom properties are to be written.
*/
virtual const IDProperty *get_id_properties(const HierarchyContext &context) const;
virtual void ensure_custom_properties_exporter(const HierarchyContext &context);
void write_visibility(const HierarchyContext &context);
/* Return the Alembic schema's compound property, which will be used for writing custom
* properties.
*
* This can return either abc_schema.getUserProperties() or abc_schema.getArbGeomParams(). The
* former only holds values similar to Blender's custom properties, whereas the latter can also
* specify that certain custom properties vary per mesh component (so per face, vertex, etc.). As
* such, .userProperties is more suitable for custom properties. However, Maya, Houdini use
* .arbGeomParams for custom data.
*
* Because of this, the code uses this templated function so that there is one place that
* determines where custom properties are exporter to.
*/
template<typename T>
Alembic::Abc::OCompoundProperty abc_schema_prop_for_custom_props(T abc_schema)
{
return abc_schema.getUserProperties();
}
};
} // namespace blender::io::alembic

View File

@ -65,6 +65,11 @@ Alembic::Abc::OObject ABCCameraWriter::get_alembic_object() const
return abc_camera_;
}
Alembic::Abc::OCompoundProperty ABCCameraWriter::abc_prop_for_custom_props()
{
return abc_schema_prop_for_custom_props(abc_camera_schema_);
}
void ABCCameraWriter::do_write(HierarchyContext &context)
{
Camera *cam = static_cast<Camera *>(context.object->data);

View File

@ -43,6 +43,7 @@ class ABCCameraWriter : public ABCAbstractWriter {
protected:
virtual bool is_supported(const HierarchyContext *context) const override;
virtual void do_write(HierarchyContext &context) override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
} // namespace blender::io::alembic

View File

@ -66,6 +66,11 @@ Alembic::Abc::OObject ABCCurveWriter::get_alembic_object() const
return abc_curve_;
}
Alembic::Abc::OCompoundProperty ABCCurveWriter::abc_prop_for_custom_props()
{
return abc_schema_prop_for_custom_props(abc_curve_schema_);
}
void ABCCurveWriter::do_write(HierarchyContext &context)
{
Curve *curve = static_cast<Curve *>(context.object->data);

View File

@ -41,6 +41,7 @@ class ABCCurveWriter : public ABCAbstractWriter {
virtual void create_alembic_objects(const HierarchyContext *context) override;
virtual Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
protected:
virtual void do_write(HierarchyContext &context) override;

View File

@ -62,6 +62,11 @@ Alembic::Abc::OObject ABCHairWriter::get_alembic_object() const
return abc_curves_;
}
Alembic::Abc::OCompoundProperty ABCHairWriter::abc_prop_for_custom_props()
{
return abc_schema_prop_for_custom_props(abc_curves_schema_);
}
bool ABCHairWriter::check_is_animated(const HierarchyContext & /*context*/) const
{
/* We assume that hair particles are always animated. */

View File

@ -44,6 +44,7 @@ class ABCHairWriter : public ABCAbstractWriter {
protected:
virtual void do_write(HierarchyContext &context) override;
virtual bool check_is_animated(const HierarchyContext &context) const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
private:
void write_hair_sample(const HierarchyContext &context,

View File

@ -50,6 +50,16 @@ void ABCInstanceWriter::create_alembic_objects(const HierarchyContext *context)
CLOG_INFO(&LOG, 2, "exporting instance %s", args_.abc_path.c_str());
}
void ABCInstanceWriter::ensure_custom_properties_exporter(const HierarchyContext & /*context*/)
{
/* Intentionally do nothing. Instances should not have their own custom properties. */
}
Alembic::Abc::OCompoundProperty ABCInstanceWriter::abc_prop_for_custom_props()
{
return Alembic::Abc::OCompoundProperty();
}
OObject ABCInstanceWriter::get_alembic_object() const
{
/* There is no OObject for an instance. */

View File

@ -39,6 +39,8 @@ class ABCInstanceWriter : public ABCAbstractWriter {
protected:
virtual bool is_supported(const HierarchyContext *context) const override;
virtual void do_write(HierarchyContext &context) override;
void ensure_custom_properties_exporter(const HierarchyContext &context) override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
} // namespace blender::io::alembic

View File

@ -125,6 +125,14 @@ Alembic::Abc::OObject ABCGenericMeshWriter::get_alembic_object() const
return abc_poly_mesh_;
}
Alembic::Abc::OCompoundProperty ABCGenericMeshWriter::abc_prop_for_custom_props()
{
if (is_subd_) {
return abc_schema_prop_for_custom_props(abc_subdiv_schema_);
}
return abc_schema_prop_for_custom_props(abc_poly_mesh_schema_);
}
bool ABCGenericMeshWriter::export_as_subdivision_surface(Object *ob_eval) const
{
ModifierData *md = static_cast<ModifierData *>(ob_eval->modifiers.last);

View File

@ -55,6 +55,7 @@ class ABCGenericMeshWriter : public ABCAbstractWriter {
virtual void create_alembic_objects(const HierarchyContext *context) override;
virtual Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
protected:
virtual bool is_supported(const HierarchyContext *context) const override;

View File

@ -78,6 +78,17 @@ OObject ABCNurbsWriter::get_alembic_object() const
return abc_nurbs_[0];
}
Alembic::Abc::OCompoundProperty ABCNurbsWriter::abc_prop_for_custom_props()
{
if (abc_nurbs_.empty()) {
return Alembic::Abc::OCompoundProperty();
}
/* A single NURBS object in Blender is expanded to multiple curves in Alembic.
* Just store the custom properties on the first one for simplicity. */
return abc_schema_prop_for_custom_props(abc_nurbs_schemas_[0]);
}
bool ABCNurbsWriter::check_is_animated(const HierarchyContext &context) const
{
/* Check if object has shape keys. */

View File

@ -40,6 +40,7 @@ class ABCNurbsWriter : public ABCAbstractWriter {
virtual bool is_supported(const HierarchyContext *context) const override;
virtual void do_write(HierarchyContext &context) override;
virtual bool check_is_animated(const HierarchyContext &context) const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
class ABCNurbsMeshWriter : public ABCGenericMeshWriter {

View File

@ -58,6 +58,11 @@ Alembic::Abc::OObject ABCPointsWriter::get_alembic_object() const
return abc_points_;
}
Alembic::Abc::OCompoundProperty ABCPointsWriter::abc_prop_for_custom_props()
{
return abc_schema_prop_for_custom_props(abc_points_schema_);
}
bool ABCPointsWriter::is_supported(const HierarchyContext *context) const
{
return ELEM(context->particle_system->part->type,

View File

@ -37,6 +37,7 @@ class ABCPointsWriter : public ABCAbstractWriter {
virtual void create_alembic_objects(const HierarchyContext *context) override;
virtual Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
virtual bool is_supported(const HierarchyContext *context) const override;

View File

@ -53,6 +53,17 @@ void ABCTransformWriter::create_alembic_objects(const HierarchyContext * /*conte
abc_xform_schema_ = abc_xform_.getSchema();
}
Alembic::Abc::OCompoundProperty ABCTransformWriter::abc_prop_for_custom_props()
{
return abc_schema_prop_for_custom_props<OXformSchema>(abc_xform_schema_);
}
const IDProperty *ABCTransformWriter::get_id_properties(const HierarchyContext &context) const
{
const Object *object = context.object;
return object->id.properties;
}
void ABCTransformWriter::do_write(HierarchyContext &context)
{
float parent_relative_matrix[4][4]; // The object matrix relative to the parent.

View File

@ -21,6 +21,8 @@
#include "abc_writer_abstract.h"
#include <memory>
#include <Alembic/AbcGeom/OXform.h>
namespace blender::io::alembic {
@ -38,6 +40,8 @@ class ABCTransformWriter : public ABCAbstractWriter {
virtual void do_write(HierarchyContext &context) override;
virtual bool check_is_animated(const HierarchyContext &context) const override;
virtual Alembic::Abc::OObject get_alembic_object() const override;
const IDProperty *get_id_properties(const HierarchyContext &context) const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
} // namespace blender::io::alembic

View File

@ -111,6 +111,7 @@ class AbstractAlembicTest(AbstractBlenderRunnerTest):
'uint64_t': int,
'float64_t': float,
'float32_t': float,
'string': str,
}
result = {}
@ -586,6 +587,69 @@ class InvisibleObjectExportTest(AbstractAlembicTest):
test('InvisibleAnimatedCube', False)
class CustomPropertiesExportTest(AbstractAlembicTest):
"""Test export of custom properties."""
def _run_export(self, tempdir: pathlib.Path) -> pathlib.Path:
abc = tempdir / 'custom-properties.abc'
script = "import bpy; bpy.context.scene.frame_set(1); bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1)" % abc.as_posix()
self.run_blender('custom-properties.blend', script)
return abc
@with_tempdir
def test_xform_props(self, tempdir: pathlib.Path) -> None:
abc = self._run_export(tempdir)
abcprop = self.abcprop(abc, '/Cube/.xform/.userProperties')
# Simple, single values.
self.assertEqual(abcprop['static_int'], [327])
self.assertEqual(abcprop['static_float'], [47.01])
self.assertEqual(abcprop['static_string'], ['Agents'])
self.assertEqual(abcprop['keyed_float'], [-1])
self.assertEqual(abcprop['keyed_int'], [-47])
# Arrays.
self.assertEqual(abcprop['keyed_array_float'], [-1.000, 0.000, 1.000])
self.assertEqual(abcprop['keyed_array_int'], [42, 47, 327])
# Multi-dimensional arrays.
self.assertEqual(abcprop['array_of_strings'], ['ผัดไทย', 'Pad Thai'])
self.assertEqual(
abcprop['matrix_tuple'],
[1.0, 0.0, 0.0, 3.33333, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
self.assertEqual(
abcprop['static_matrix'],
[1.0, 0.0, 0.0, 3.33333, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
self.assertEqual(
abcprop['nonuniform_array'],
[10, 20, 30, 1, 2, 47])
@with_tempdir
def test_mesh_props(self, tempdir: pathlib.Path) -> None:
abc = self._run_export(tempdir)
abcprop = self.abcprop(abc, '/Cube/Cube/.geom/.userProperties')
self.assertEqual(abcprop['mesh_tags'], ['cube', 'box', 'low-poly-sphere'])
@with_tempdir
def test_camera_props(self, tempdir: pathlib.Path) -> None:
abc = self._run_export(tempdir)
abcprop = self.abcprop(abc, '/Camera/Hasselblad/.geom/.userProperties')
self.assertEqual(abcprop['type'], ['500c/m'])
@with_tempdir
def test_disabled_export_option(self, tempdir: pathlib.Path) -> None:
abc = tempdir / 'custom-properties.abc'
script = (
"import bpy; bpy.context.scene.frame_set(1); "
"bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, export_custom_properties=False)" % abc.as_posix()
)
self.run_blender('custom-properties.blend', script)
abcprop = self.abcprop(abc, '/Camera/Hasselblad/.geom/.userProperties')
self.assertIn('eyeSeparation', abcprop, 'Regular non-standard properties should still be written')
self.assertNotIn('type', abcprop, 'Custom properties should not be written')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--blender', required=True)