Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include <string>
#include "BLI_vector.hh"
#include "DEG_depsgraph.hh"
namespace blender {
struct CacheArchiveHandle;
struct CacheFileLayer;
struct CacheObjectPath;
struct CacheReader;
struct Main;
struct Mesh;
struct Object;
struct Scene;
struct bContext;
int ABC_get_version();
struct AlembicExportParams {
double frame_start;
double frame_end;
unsigned int frame_samples_xform;
unsigned int frame_samples_shape;
double shutter_open;
double shutter_close;
bool selected_only;
bool uvs;
bool normals;
bool vcolors;
bool orcos;
bool apply_subdiv;
bool curves_as_mesh;
bool flatten_hierarchy;
bool face_sets;
bool use_subdiv_schema;
bool packuv;
bool triangulate;
bool export_hair;
bool export_particles;
bool export_custom_properties;
bool use_instancing;
enum eEvaluationMode evaluation_mode;
/* See MOD_TRIANGULATE_NGON_xxx and MOD_TRIANGULATE_QUAD_xxx
* in DNA_modifier_types.h */
int quad_method;
int ngon_method;
float global_scale;
char collection[MAX_ID_NAME - 2] = "";
};
struct AlembicImportParams {
/* Multiplier for the cached data scale. Mostly useful if the data is stored in a different unit
* as what Blender expects (e.g. centimeters instead of meters). */
float global_scale;
Vector<std::string> paths;
/* Last frame number of consecutive files to expect if the cached animation is split in a
* sequence. */
int sequence_max_frame;
/* Start frame of the sequence, offset from 0. */
int sequence_min_frame;
/* True if the cache is split in multiple files. */
bool is_sequence;
/* True if the importer should set the current scene's start and end frame based on the start and
* end frames of the cached animation. */
bool set_frame_range;
/* True if imported meshes should be validated. Error messages are sent to the console. */
bool validate_meshes;
/* True if a cache reader should be added regardless of whether there is animated data in the
* cached file. */
bool always_add_cache_reader;
};
/* The ABC_export and ABC_import functions both take a as_background_job
* parameter, and return a boolean.
*
* When as_background_job=true, returns false immediately after scheduling
* a background job.
*
* When as_background_job=false, performs the export synchronously, and returns
* true when the export was ok, and false if there were any errors.
*/
bool ABC_export(struct Scene *scene,
struct bContext *C,
const char *filepath,
const struct AlembicExportParams *params,
bool as_background_job);
bool ABC_import(struct bContext *C,
const struct AlembicImportParams *params,
bool as_background_job);
struct CacheArchiveHandle *ABC_create_handle(const struct Main *bmain,
const char *filepath,
const struct CacheFileLayer *layers,
ListBaseT<CacheObjectPath> *object_paths);
void ABC_free_handle(struct CacheArchiveHandle *handle);
void ABC_get_transform(struct CacheReader *reader,
float r_mat_world[4][4],
double time,
float scale);
struct ABCReadParams {
double time;
int read_flags;
const char *velocity_name;
float velocity_scale;
};
#ifdef __cplusplus
namespace bke {
struct GeometrySet;
}
/* Either modifies the existing geometry component, or create a new one. */
void ABC_read_geometry(CacheReader *reader,
Object *ob,
bke::GeometrySet &geometry_set,
const ABCReadParams *params,
const char **r_err_str);
#endif
bool ABC_mesh_topology_changed(struct CacheReader *reader,
struct Object *ob,
const struct Mesh *existing_mesh,
double time,
const char **r_err_str);
void ABC_CacheReader_free(struct CacheReader *reader);
struct CacheReader *CacheReader_open_alembic_object(struct CacheArchiveHandle *handle,
struct CacheReader *reader,
struct Object *object,
const char *object_path,
bool is_sequence);
} // namespace blender

View File

@@ -0,0 +1,106 @@
# SPDX-FileCopyrightText: 2006 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
../common
../../editors/include
../../makesrna
../../../../intern/utfconv
)
set(INC_SYS
)
set(SRC
intern/abc_axis_conversion.cc
intern/abc_customdata.cc
intern/abc_keyframing.cc
intern/abc_reader_archive.cc
intern/abc_reader_camera.cc
intern/abc_reader_curves.cc
intern/abc_reader_mesh.cc
intern/abc_reader_nurbs.cc
intern/abc_reader_object.cc
intern/abc_reader_points.cc
intern/abc_reader_transform.cc
intern/abc_util.cc
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_writer_abstract.cc
exporter/abc_writer_camera.cc
exporter/abc_writer_curves.cc
exporter/abc_writer_hair.cc
exporter/abc_writer_instance.cc
exporter/abc_writer_mball.cc
exporter/abc_writer_mesh.cc
exporter/abc_writer_nurbs.cc
exporter/abc_writer_points.cc
exporter/abc_writer_transform.cc
ABC_alembic.h
intern/abc_axis_conversion.h
intern/abc_customdata.h
intern/abc_keyframing.h
intern/abc_reader_archive.h
intern/abc_reader_camera.h
intern/abc_reader_curves.h
intern/abc_reader_mesh.h
intern/abc_reader_nurbs.h
intern/abc_reader_object.h
intern/abc_reader_points.h
intern/abc_reader_transform.h
intern/abc_util.h
exporter/abc_archive.h
exporter/abc_custom_props.h
exporter/abc_hierarchy_iterator.h
exporter/abc_writer_abstract.h
exporter/abc_writer_camera.h
exporter/abc_writer_curves.h
exporter/abc_writer_hair.h
exporter/abc_writer_instance.h
exporter/abc_writer_mball.h
exporter/abc_writer_mesh.h
exporter/abc_writer_nurbs.h
exporter/abc_writer_points.h
exporter/abc_writer_transform.h
)
set(LIB
PRIVATE bf::animrig
PRIVATE bf::blenkernel
PRIVATE bf::blenlib
PRIVATE bf::blenloader
PRIVATE bf::blentranslation
PRIVATE bf::bmesh
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
bf_io_common
PRIVATE bf::windowmanager
PRIVATE bf::dependencies::optional::alembic
PRIVATE bf::dependencies::openexr
)
blender_add_lib(bf_io_alembic "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(WITH_GTESTS)
set(TEST_SRC
tests/abc_export_test.cc
tests/abc_matrix_test.cc
)
set(TEST_INC
)
set(TEST_LIB
bf_io_alembic
)
blender_add_test_suite_lib(io_alembic "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,250 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "abc_archive.h"
#include "BKE_blender_version.h"
#include "BKE_main.hh"
#include "DNA_scene_types.h"
#include <Alembic/Abc/ArchiveInfo.h>
#include <Alembic/Abc/ErrorHandler.h>
#include <Alembic/Abc/Foundation.h>
#include <Alembic/Abc/OArchive.h>
#include <Alembic/AbcCoreAbstract/MetaData.h>
#include <Alembic/AbcCoreAbstract/TimeSampling.h>
#include <Alembic/AbcCoreAbstract/TimeSamplingType.h>
#include <Alembic/AbcCoreOgawa/ReadWrite.h>
#include <Alembic/AbcGeom/ArchiveBounds.h>
#ifdef WIN32
# include "BLI_path_utils.hh"
# include "BLI_string.h"
# include "utfconv.hh"
#endif
namespace blender::io::alembic {
using Alembic::Abc::ErrorHandler;
using Alembic::Abc::kWrapExisting;
using Alembic::Abc::MetaData;
using Alembic::Abc::OArchive;
using Alembic::Abc::TimeSampling;
using Alembic::Abc::TimeSamplingPtr;
using Alembic::Abc::TimeSamplingType;
static MetaData create_abc_metadata(const Main *bmain, double scene_fps)
{
MetaData abc_metadata;
std::string abc_user_description(bmain->filepath);
if (abc_user_description.empty()) {
abc_user_description = "unknown";
}
abc_metadata.set(Alembic::Abc::kApplicationNameKey, "Blender");
abc_metadata.set(Alembic::Abc::kUserDescriptionKey, abc_user_description);
abc_metadata.set("blender_version", std::string("v") + BKE_blender_version_string());
abc_metadata.set("FramesPerTimeUnit", std::to_string(scene_fps));
time_t raw_time;
time(&raw_time);
char buffer[128];
#if defined _WIN32 || defined _WIN64
ctime_s(buffer, 128, &raw_time);
#else
ctime_r(&raw_time, buffer);
#endif
const std::size_t buffer_len = strlen(buffer);
if (buffer_len > 0 && buffer[buffer_len - 1] == '\n') {
buffer[buffer_len - 1] = '\0';
}
abc_metadata.set(Alembic::Abc::kDateWrittenKey, buffer);
return abc_metadata;
}
static OArchive *create_archive(std::ofstream *abc_ostream,
const std::string &filepath,
MetaData &abc_metadata)
{
/* Use stream to support unicode character paths on Windows. */
#ifdef WIN32
char filepath_cstr[FILE_MAX];
BLI_strncpy(filepath_cstr, filepath.c_str(), FILE_MAX);
UTF16_ENCODE(filepath_cstr);
std::wstring wstr(filepath_cstr_16);
abc_ostream->open(wstr.c_str(), std::ios::out | std::ios::binary);
UTF16_UN_ENCODE(filepath_cstr);
#else
abc_ostream->open(filepath, std::ios::out | std::ios::binary);
#endif
ErrorHandler::Policy policy = ErrorHandler::kThrowPolicy;
Alembic::AbcCoreOgawa::WriteArchive archive_writer;
return new OArchive(archive_writer(abc_ostream, abc_metadata), kWrapExisting, policy);
}
/* Construct list of shutter samples.
*
* These are taken from the interval [shutter open, shutter close),
* uniformly sampled with 'nr_of_samples' samples.
*
* TODO(Sybren): test that the above interval is indeed half-open.
*
* If 'time_relative' is true, samples are returned as time (in seconds) from params.frame_start.
* If 'time_relative' is false, samples are returned as fractional frames from 0.
*/
static void get_shutter_samples(double scene_fps,
const AlembicExportParams &params,
int nr_of_samples,
bool time_relative,
std::vector<double> &r_samples)
{
double frame_offset = time_relative ? params.frame_start : 0.0;
double time_factor = time_relative ? scene_fps : 1.0;
double shutter_open = params.shutter_open;
double shutter_close = params.shutter_close;
double time_inc = (shutter_close - shutter_open) / nr_of_samples;
/* sample between shutter open & close */
for (int sample = 0; sample < nr_of_samples; sample++) {
double sample_time = shutter_open + time_inc * sample;
double time = (frame_offset + sample_time) / time_factor;
r_samples.push_back(time);
}
}
static TimeSamplingPtr create_time_sampling(double scene_fps,
const AlembicExportParams &params,
int nr_of_samples)
{
std::vector<double> samples;
if (params.frame_start == params.frame_end) {
return TimeSamplingPtr(new TimeSampling()); // NOLINT: modernize-make-shared
}
get_shutter_samples(scene_fps, params, nr_of_samples, true, samples);
TimeSamplingType ts(uint32_t(samples.size()), 1.0 / scene_fps);
return TimeSamplingPtr(new TimeSampling(ts, samples)); // NOLINT: modernize-make-shared
}
static void get_frames(double scene_fps,
const AlembicExportParams &params,
uint nr_of_samples,
std::set<double> &r_frames)
{
/* Get one set of shutter samples, then add those around each frame to export. */
std::vector<double> shutter_samples;
get_shutter_samples(scene_fps, params, nr_of_samples, false, shutter_samples);
for (double frame = params.frame_start; frame <= params.frame_end; frame += 1.0) {
for (size_t j = 0; j < nr_of_samples; j++) {
r_frames.insert(frame + shutter_samples[j]);
}
}
}
/* ****************************************************************** */
ABCArchive::ABCArchive(const Main *bmain,
const Scene *scene,
AlembicExportParams params,
const std::string &filepath)
: archive(nullptr)
{
double scene_fps = scene->frames_per_second();
MetaData abc_metadata = create_abc_metadata(bmain, scene_fps);
/* Create the Archive. */
archive = create_archive(&abc_ostream_, filepath, abc_metadata);
/* Create time samples for transforms and shapes. */
TimeSamplingPtr ts_xform;
TimeSamplingPtr ts_shapes;
ts_xform = create_time_sampling(scene_fps, params, params.frame_samples_xform);
time_sampling_index_transforms_ = archive->addTimeSampling(*ts_xform);
const bool export_animation = params.frame_start != params.frame_end;
if (!export_animation || params.frame_samples_shape == params.frame_samples_xform) {
ts_shapes = ts_xform;
time_sampling_index_shapes_ = time_sampling_index_transforms_;
}
else {
ts_shapes = create_time_sampling(scene_fps, params, params.frame_samples_shape);
time_sampling_index_shapes_ = archive->addTimeSampling(*ts_shapes);
}
/* Construct the frames to export. */
get_frames(scene_fps, params, params.frame_samples_xform, xform_frames_);
get_frames(scene_fps, params, params.frame_samples_shape, shape_frames_);
/* Merge all frames to get the final set of frames to export. */
export_frames_.insert(xform_frames_.begin(), xform_frames_.end());
export_frames_.insert(shape_frames_.begin(), shape_frames_.end());
abc_archive_bbox_ = Alembic::AbcGeom::CreateOArchiveBounds(*archive,
time_sampling_index_transforms_);
}
ABCArchive::~ABCArchive()
{
delete archive;
}
uint32_t ABCArchive::time_sampling_index_transforms() const
{
return time_sampling_index_transforms_;
}
uint32_t ABCArchive::time_sampling_index_shapes() const
{
return time_sampling_index_shapes_;
}
ABCArchive::Frames::const_iterator ABCArchive::frames_begin() const
{
return export_frames_.begin();
}
ABCArchive::Frames::const_iterator ABCArchive::frames_end() const
{
return export_frames_.end();
}
size_t ABCArchive::total_frame_count() const
{
return export_frames_.size();
}
bool ABCArchive::is_xform_frame(double frame) const
{
return xform_frames_.contains(frame);
}
bool ABCArchive::is_shape_frame(double frame) const
{
return shape_frames_.contains(frame);
}
ExportSubset ABCArchive::export_subset_for_frame(double frame) const
{
ExportSubset subset;
subset.transforms = is_xform_frame(frame);
subset.shapes = is_shape_frame(frame);
return subset;
}
void ABCArchive::update_bounding_box(const Imath::Box3d &bounds)
{
abc_archive_bbox_.set(bounds);
}
} // namespace blender::io::alembic

View File

@@ -0,0 +1,71 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup Alembic
*/
#pragma once
#include "ABC_alembic.h"
#include "IO_abstract_hierarchy_iterator.h"
#include <Alembic/Abc/OArchive.h>
#include <Alembic/Abc/OTypedScalarProperty.h>
#include <fstream>
#include <set>
#include <string>
namespace blender {
struct Main;
struct Scene;
namespace io::alembic {
/* Container for an Alembic archive and time sampling info.
*
* Constructor arguments are used to create the correct output stream and to set the archive's
* metadata. */
class ABCArchive {
public:
using Frames = std::set<double>;
Alembic::Abc::OArchive *archive;
ABCArchive(const Main *bmain,
const Scene *scene,
AlembicExportParams params,
const std::string &filepath);
~ABCArchive();
uint32_t time_sampling_index_transforms() const;
uint32_t time_sampling_index_shapes() const;
Frames::const_iterator frames_begin() const;
Frames::const_iterator frames_end() const;
size_t total_frame_count() const;
bool is_xform_frame(double frame) const;
bool is_shape_frame(double frame) const;
ExportSubset export_subset_for_frame(double frame) const;
void update_bounding_box(const Imath::Box3d &bounds);
private:
std::ofstream abc_ostream_;
uint32_t time_sampling_index_transforms_;
uint32_t time_sampling_index_shapes_;
Frames xform_frames_;
Frames shape_frames_;
Frames export_frames_;
Alembic::Abc::OBox3dProperty abc_archive_bbox_;
};
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,266 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup Alembic
*/
#include "abc_custom_props.h"
#include "abc_writer_abstract.h"
#include <string>
#include <Alembic/Abc/OTypedArrayProperty.h>
#include "BLI_listbase.h"
#include "BKE_idprop.hh"
#include "DNA_ID.h"
namespace blender {
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 io::alembic {
CustomPropertiesExporter::CustomPropertiesExporter(ABCAbstractWriter *owner) : owner_(owner) {}
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. */
for (IDProperty &id_property : group->data.group) {
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_get(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_get(id_property));
break;
case IDP_FLOAT:
set_scalar_property<OFloatArrayProperty, float>(id_property->name,
IDP_float_get(id_property));
break;
case IDP_DOUBLE:
set_scalar_property<ODoubleArrayProperty, double>(id_property->name,
IDP_double_get(id_property));
break;
case IDP_BOOLEAN:
set_scalar_property<OBoolArrayProperty, bool>(id_property->name, IDP_bool_get(id_property));
break;
case IDP_ARRAY:
write_array(id_property);
break;
case IDP_IDPARRAY:
write_idparray(id_property);
break;
case IDP_GROUP:
case IDP_ID:
/* Not supported. */
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 = IDP_array_int_get(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 = IDP_array_float_get(id_property);
set_array_property<OFloatArrayProperty, float>(id_property->name, array, id_property->len);
break;
}
case IDP_DOUBLE: {
const double *array = IDP_array_double_get(id_property);
set_array_property<ODoubleArrayProperty, double>(id_property->name, array, id_property->len);
break;
}
case IDP_BOOLEAN: {
const int8_t *array = IDP_array_bool_get(id_property);
set_array_property<OBoolArrayProperty, int8_t>(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 = IDP_property_array_get(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_msg(0, "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;
default:
/* Other types not supported. */
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 = IDP_property_array_get(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_get(&idp_elements[i]);
}
/* Alembic needs a pointer to the first value of the array. */
const std::string *array_of_strings = strings.data();
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 = IDP_property_array_get(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, IDP_BOOLEAN)) {
/* 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;
case IDP_BOOLEAN:
write_idparray_flattened_typed<OBoolArrayProperty, int8_t>(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 = IDP_property_array_get(idp_array);
BLI_assert(idp_rows[0].type == IDP_ARRAY);
BLI_assert(ELEM(idp_rows[0].subtype, IDP_INT, IDP_FLOAT, IDP_DOUBLE, IDP_BOOLEAN));
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 = static_cast<BlenderValueType *> IDP_array_voidp_get(
&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.data(), 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 io::alembic
} // namespace blender

View File

@@ -0,0 +1,84 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup Alembic
*/
#pragma once
#include <Alembic/Abc/OArrayProperty.h>
#include <Alembic/Abc/OCompoundProperty.h>
#include "BLI_map.hh"
#include <string>
namespace blender {
struct IDProperty;
namespace 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() = default;
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 io::alembic
} // namespace blender

View File

@@ -0,0 +1,264 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ABC_alembic.h"
#include "IO_subdiv_disabler.hh"
#include "abc_archive.h"
#include "abc_hierarchy_iterator.h"
#include "MEM_guardedalloc.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_scene_types.h"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "BLI_fileops.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_timeit.hh"
#include "ED_util.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
#include <memory>
struct ExportJobData {
Main *bmain = nullptr;
Depsgraph *depsgraph = nullptr;
wmWindowManager *wm = nullptr;
char filepath[FILE_MAX] = {};
AlembicExportParams params = {};
bool was_canceled = false;
bool export_ok = false;
timeit::TimePoint start_time = {};
};
namespace io::alembic {
/* Construct the depsgraph for exporting. */
static bool build_depsgraph(ExportJobData *job)
{
if (job->params.collection[0]) {
Collection *collection = reinterpret_cast<Collection *>(
BKE_libblock_find_name(job->bmain, ID_GR, job->params.collection));
if (!collection) {
WM_global_reportf(
RPT_ERROR, "Alembic Export: Unable to find collection '%s'", job->params.collection);
return false;
}
DEG_graph_build_from_collection(job->depsgraph, collection);
}
else {
DEG_graph_build_from_view_layer(job->depsgraph);
}
return true;
}
static void report_job_duration(const ExportJobData *data)
{
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
std::cout << "Alembic export of '" << data->filepath << "' took ";
timeit::print_duration(duration);
std::cout << '\n';
}
static void export_startjob(void *customdata, wmJobWorkerStatus *worker_status)
{
ExportJobData *data = static_cast<ExportJobData *>(customdata);
data->was_canceled = false;
data->start_time = timeit::Clock::now();
G.is_rendering = true;
WM_locked_interface_set(data->wm, true);
G.is_break = false;
worker_status->progress = 0.0f;
worker_status->do_update = true;
BKE_scene_graph_update_tagged(data->depsgraph, data->bmain);
SubdivModifierDisabler subdiv_disabler(data->depsgraph);
if (!data->params.apply_subdiv) {
subdiv_disabler.disable_modifiers();
BKE_scene_graph_update_tagged(data->depsgraph, data->bmain);
}
/* For restoring the current frame after exporting animation is done. */
Scene *scene = DEG_get_input_scene(data->depsgraph);
const int orig_frame = scene->r.cfra;
const bool export_animation = (data->params.frame_start != data->params.frame_end);
/* Create the Alembic archive. */
std::unique_ptr<ABCArchive> abc_archive;
try {
abc_archive = std::make_unique<ABCArchive>(
data->bmain, scene, data->params, std::string(data->filepath));
}
catch (const std::exception &ex) {
std::stringstream error_message_stream;
error_message_stream << "Error writing to " << data->filepath;
const std::string &error_message = error_message_stream.str();
/* The exception message can be very cryptic (just "iostream error" on Linux, for example),
* so better not to include it in the report. */
CLOG_ERROR(&LOG, "%s: %s", error_message.c_str(), ex.what());
WM_global_report(RPT_ERROR, error_message.c_str());
data->export_ok = false;
return;
}
catch (...) {
/* Unknown exception class, so we cannot include its message. */
std::stringstream error_message_stream;
error_message_stream << "Unknown error writing to " << data->filepath;
WM_global_report(RPT_ERROR, error_message_stream.str().c_str());
data->export_ok = false;
return;
}
ABCHierarchyIterator iter(data->bmain, data->depsgraph, abc_archive.get(), data->params);
if (export_animation) {
CLOG_STR_DEBUG(&LOG, "Exporting animation");
/* Writing the animated frames is not 100% of the work, but it's our best guess. */
const float progress_per_frame = 1.0f / std::max(size_t(1), abc_archive->total_frame_count());
ABCArchive::Frames::const_iterator frame_it = abc_archive->frames_begin();
const ABCArchive::Frames::const_iterator frames_end = abc_archive->frames_end();
for (; frame_it != frames_end; frame_it++) {
double frame = *frame_it;
if (G.is_break || worker_status->stop) {
break;
}
/* Update the scene for the next frame to render. */
scene->r.cfra = int(frame);
scene->r.subframe = float(frame - scene->r.cfra);
BKE_scene_graph_update_for_newframe(data->depsgraph);
CLOG_DEBUG(&LOG, "Exporting frame %.2f", frame);
ExportSubset export_subset = abc_archive->export_subset_for_frame(frame);
iter.set_export_subset(export_subset);
iter.iterate_and_write();
worker_status->progress += progress_per_frame;
worker_status->do_update = true;
}
}
else {
/* If we're not animating, a single iteration over all objects is enough. */
iter.iterate_and_write();
}
iter.release_writers();
/* Finish up by going back to the keyframe that was current before we started. */
if (scene->r.cfra != orig_frame) {
scene->r.cfra = orig_frame;
BKE_scene_graph_update_for_newframe(data->depsgraph);
}
data->export_ok = !data->was_canceled;
worker_status->progress = 1.0f;
worker_status->do_update = true;
}
static void export_endjob(void *customdata)
{
ExportJobData *data = static_cast<ExportJobData *>(customdata);
DEG_graph_free(data->depsgraph);
if (data->was_canceled && BLI_exists(data->filepath)) {
BLI_delete(data->filepath, false, false);
}
G.is_rendering = false;
WM_locked_interface_set(data->wm, false);
report_job_duration(data);
}
} // namespace io::alembic
bool ABC_export(Scene *scene,
bContext *C,
const char *filepath,
const AlembicExportParams *params,
bool as_background_job)
{
ViewLayer *view_layer = CTX_data_view_layer(C);
ExportJobData *job = MEM_new<ExportJobData>("ExportJobData");
job->bmain = CTX_data_main(C);
job->wm = CTX_wm_manager(C);
job->export_ok = false;
STRNCPY(job->filepath, filepath);
ED_editors_flush_edits(job->bmain);
job->depsgraph = DEG_graph_new(job->bmain, scene, view_layer, params->evaluation_mode);
job->params = *params;
/* Construct the depsgraph for exporting.
*
* Has to be done from main thread currently, as it may affect Main original data (e.g. when
* doing deferred update of the view-layers, see #112534 for details). */
if (!io::alembic::build_depsgraph(job)) {
return false;
}
bool export_ok = false;
if (as_background_job) {
wmJob *wm_job = WM_jobs_get(job->wm,
CTX_wm_window(C),
scene,
"Exporting Alembic...",
WM_JOB_PROGRESS,
WM_JOB_TYPE_ALEMBIC_EXPORT);
/* setup job */
WM_jobs_customdata_set(
wm_job, job, [](void *j) { MEM_delete(static_cast<ExportJobData *>(j)); });
WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_FRAME, NC_SCENE | ND_FRAME);
WM_jobs_callbacks(
wm_job, io::alembic::export_startjob, nullptr, nullptr, io::alembic::export_endjob);
WM_jobs_start(CTX_wm_manager(C), wm_job);
}
else {
wmJobWorkerStatus worker_status = {};
io::alembic::export_startjob(job, &worker_status);
io::alembic::export_endjob(job);
export_ok = job->export_ok;
MEM_delete(job);
}
return export_ok;
}
} // namespace blender

View File

@@ -0,0 +1,265 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "abc_hierarchy_iterator.h"
#include "abc_writer_abstract.h"
#include "abc_writer_camera.h"
#include "abc_writer_curves.h"
#include "abc_writer_hair.h"
#include "abc_writer_instance.h"
#include "abc_writer_mball.h"
#include "abc_writer_mesh.h"
#include "abc_writer_nurbs.h"
#include "abc_writer_points.h"
#include "abc_writer_transform.h"
#include "intern/abc_util.h"
#include <memory>
#include <string>
#include "BLI_assert.h"
#include "DNA_layer_types.h"
#include "DNA_object_types.h"
namespace blender::io::alembic {
ABCHierarchyIterator::ABCHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
ABCArchive *abc_archive,
const AlembicExportParams &params)
: AbstractHierarchyIterator(bmain, depsgraph), abc_archive_(abc_archive), params_(params)
{
}
void ABCHierarchyIterator::iterate_and_write()
{
AbstractHierarchyIterator::iterate_and_write();
update_archive_bounding_box();
}
void ABCHierarchyIterator::update_archive_bounding_box()
{
Imath::Box3d bounds;
update_bounding_box_recursive(bounds, HierarchyContext::root());
abc_archive_->update_bounding_box(bounds);
}
void ABCHierarchyIterator::update_bounding_box_recursive(Imath::Box3d &bounds,
const HierarchyContext *context)
{
if (context != nullptr) {
AbstractHierarchyWriter *abstract_writer = writers_.lookup(context->export_path);
ABCAbstractWriter *abc_writer = static_cast<ABCAbstractWriter *>(abstract_writer);
if (abc_writer != nullptr) {
bounds.extendBy(abc_writer->bounding_box());
}
}
ExportChildren *children = graph_children(context);
if (!children) {
return;
}
for (HierarchyContext *child_context : *children) {
update_bounding_box_recursive(bounds, child_context);
}
}
bool ABCHierarchyIterator::mark_as_weak_export(const Object *object) const
{
if (params_.selected_only && (object->base_flag & BASE_SELECTED) == 0) {
return true;
}
/* TODO(Sybren): handle other flags too? */
return false;
}
void ABCHierarchyIterator::release_writer(AbstractHierarchyWriter *writer)
{
delete writer;
}
std::string ABCHierarchyIterator::make_valid_name(const std::string &name) const
{
return get_valid_abc_name(name.c_str());
}
ObjectIdentifier ABCHierarchyIterator::determine_graph_index_object(
const HierarchyContext *context)
{
if (params_.flatten_hierarchy) {
return ObjectIdentifier::for_graph_root();
}
return AbstractHierarchyIterator::determine_graph_index_object(context);
}
ObjectIdentifier ABCHierarchyIterator::determine_graph_index_dupli(
const HierarchyContext *context,
const DupliObject *dupli_object,
const DupliParentFinder &dupli_parent_finder)
{
if (params_.flatten_hierarchy) {
return ObjectIdentifier::for_graph_root();
}
return AbstractHierarchyIterator::determine_graph_index_dupli(
context, dupli_object, dupli_parent_finder);
}
Alembic::Abc::OObject ABCHierarchyIterator::get_alembic_object(
const std::string &export_path) const
{
if (export_path.empty()) {
return Alembic::Abc::OObject();
}
AbstractHierarchyWriter *writer = get_writer(export_path);
if (writer == nullptr) {
return Alembic::Abc::OObject();
}
ABCAbstractWriter *abc_writer = static_cast<ABCAbstractWriter *>(writer);
return abc_writer->get_alembic_object();
}
Alembic::Abc::OObject ABCHierarchyIterator::get_alembic_parent(
const HierarchyContext *context) const
{
Alembic::Abc::OObject parent = get_alembic_object(context->higher_up_export_path);
if (!parent.valid()) {
/* An invalid parent object means "no parent", which should be translated to Alembic's top
* archive object. */
return abc_archive_->archive->getTop();
}
return parent;
}
ABCWriterConstructorArgs ABCHierarchyIterator::writer_constructor_args(
const HierarchyContext *context) const
{
ABCWriterConstructorArgs constructor_args;
constructor_args.depsgraph = depsgraph_;
constructor_args.abc_archive = abc_archive_;
constructor_args.abc_parent = get_alembic_parent(context);
constructor_args.abc_name = context->export_name;
constructor_args.abc_path = context->export_path;
constructor_args.hierarchy_iterator = this;
constructor_args.export_params = &params_;
return constructor_args;
}
AbstractHierarchyWriter *ABCHierarchyIterator::create_transform_writer(
const HierarchyContext *context)
{
ABCAbstractWriter *transform_writer = new ABCTransformWriter(writer_constructor_args(context));
transform_writer->create_alembic_objects(context);
return transform_writer;
}
AbstractHierarchyWriter *ABCHierarchyIterator::create_data_writer(const HierarchyContext *context)
{
const ABCWriterConstructorArgs writer_args = writer_constructor_args(context);
ABCAbstractWriter *data_writer = nullptr;
if (params_.use_instancing && context->is_instance()) {
data_writer = new ABCInstanceWriter(writer_args);
}
else {
data_writer = create_data_writer_for_object_type(context, writer_args);
}
if (data_writer == nullptr || !data_writer->is_supported(context)) {
delete data_writer;
return nullptr;
}
data_writer->create_alembic_objects(context);
return data_writer;
}
ABCAbstractWriter *ABCHierarchyIterator::create_data_writer_for_object_type(
const HierarchyContext *context, const ABCWriterConstructorArgs &writer_args)
{
switch (context->object->type) {
case OB_MESH:
return new ABCMeshWriter(writer_args);
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);
}
return new ABCCurveWriter(writer_args);
case OB_SURF:
if (params_.curves_as_mesh) {
return new ABCCurveMeshWriter(writer_args);
}
return new ABCNurbsWriter(writer_args);
case OB_MBALL:
return new ABCMetaballWriter(writer_args);
case OB_EMPTY:
case OB_LAMP:
case OB_FONT:
case OB_SPEAKER:
case OB_LIGHTPROBE:
case OB_LATTICE:
case OB_ARMATURE:
case OB_GPENCIL_LEGACY:
case OB_POINTCLOUD:
case OB_VOLUME:
case OB_GREASE_PENCIL:
return nullptr;
case OB_TYPE_MAX:
BLI_assert_msg(0, "OB_TYPE_MAX should not be used");
return nullptr;
}
/* Just to please the compiler, all cases should be handled by the above switch. */
return nullptr;
}
AbstractHierarchyWriter *ABCHierarchyIterator::create_hair_writer(const HierarchyContext *context)
{
if (!params_.export_hair) {
return nullptr;
}
const ABCWriterConstructorArgs writer_args = writer_constructor_args(context);
ABCAbstractWriter *hair_writer = new ABCHairWriter(writer_args);
if (!hair_writer->is_supported(context)) {
delete hair_writer;
return nullptr;
}
hair_writer->create_alembic_objects(context);
return hair_writer;
}
AbstractHierarchyWriter *ABCHierarchyIterator::create_particle_writer(
const HierarchyContext *context)
{
if (!params_.export_particles) {
return nullptr;
}
const ABCWriterConstructorArgs writer_args = writer_constructor_args(context);
std::unique_ptr<ABCPointsWriter> particle_writer(std::make_unique<ABCPointsWriter>(writer_args));
if (!particle_writer->is_supported(context)) {
return nullptr;
}
particle_writer->create_alembic_objects(context);
return particle_writer.release();
}
} // namespace blender::io::alembic

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "ABC_alembic.h"
#include "abc_archive.h"
#include "IO_abstract_hierarchy_iterator.h"
#include <string>
#include <Alembic/Abc/OObject.h>
namespace blender {
struct Depsgraph;
struct Main;
struct Object;
namespace io::alembic {
class ABCAbstractWriter;
class ABCHierarchyIterator;
struct ABCWriterConstructorArgs {
Depsgraph *depsgraph;
ABCArchive *abc_archive;
Alembic::Abc::OObject abc_parent;
std::string abc_name;
std::string abc_path;
const ABCHierarchyIterator *hierarchy_iterator;
const AlembicExportParams *export_params;
};
class ABCHierarchyIterator : public AbstractHierarchyIterator {
private:
ABCArchive *abc_archive_;
const AlembicExportParams &params_;
public:
ABCHierarchyIterator(Main *bmain,
Depsgraph *depsgraph,
ABCArchive *abc_archive,
const AlembicExportParams &params);
void iterate_and_write() override;
std::string make_valid_name(const std::string &name) const override;
Alembic::Abc::OObject get_alembic_object(const std::string &export_path) const;
protected:
bool mark_as_weak_export(const Object *object) const override;
ObjectIdentifier determine_graph_index_object(const HierarchyContext *context) override;
ObjectIdentifier determine_graph_index_dupli(
const HierarchyContext *context,
const DupliObject *dupli_object,
const DupliParentFinder &dupli_parent_finder) override;
AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) override;
AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) override;
void release_writer(AbstractHierarchyWriter *writer) override;
private:
Alembic::Abc::OObject get_alembic_parent(const HierarchyContext *context) const;
ABCWriterConstructorArgs writer_constructor_args(const HierarchyContext *context) const;
void update_archive_bounding_box();
void update_bounding_box_recursive(Imath::Box3d &bounds, const HierarchyContext *context);
ABCAbstractWriter *create_data_writer_for_object_type(
const HierarchyContext *context, const ABCWriterConstructorArgs &writer_args);
};
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,139 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_bounds.hh"
#include "abc_hierarchy_iterator.h"
#include "abc_writer_abstract.h"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include <Alembic/AbcGeom/Visibility.h>
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::Abc::OObject;
using Alembic::Abc::TimeSamplingPtr;
ABCAbstractWriter::ABCAbstractWriter(const ABCWriterConstructorArgs &args)
: args_(args),
frame_has_been_written_(false),
is_animated_(false),
timesample_index_(args_.abc_archive->time_sampling_index_shapes())
{
}
bool ABCAbstractWriter::is_supported(const HierarchyContext * /*context*/) const
{
return true;
}
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. */
return;
}
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 object->data->properties;
}
uint32_t ABCAbstractWriter::timesample_index() const
{
return timesample_index_;
}
const Imath::Box3d &ABCAbstractWriter::bounding_box() const
{
return bounding_box_;
}
void ABCAbstractWriter::update_bounding_box(Object *object)
{
const std::optional<Bounds<float3>> bounds = BKE_object_boundbox_get(object);
if (!bounds) {
if (object->type != OB_CAMERA) {
CLOG_WARN(&LOG, "Bounding box is null!");
}
bounding_box_.min.x = bounding_box_.min.y = bounding_box_.min.z = 0;
bounding_box_.max.x = bounding_box_.max.y = bounding_box_.max.z = 0;
return;
}
const std::array<float3, 8> corners = bounds::corners(*bounds);
/* Convert Z-up to Y-up. This also changes which vector goes into which min/max property. */
bounding_box_.min.x = corners[0][0];
bounding_box_.min.y = corners[0][2];
bounding_box_.min.z = -corners[6][1];
bounding_box_.max.x = corners[6][0];
bounding_box_.max.y = corners[6][2];
bounding_box_.max.z = -corners[0][1];
}
void ABCAbstractWriter::write_visibility(const HierarchyContext &context)
{
const bool is_visible = context.is_object_visible(args_.export_params->evaluation_mode);
Alembic::Abc::OObject abc_object = get_alembic_object();
if (!abc_visibility_.valid()) {
abc_visibility_ = Alembic::AbcGeom::CreateVisibilityProperty(abc_object, timesample_index_);
}
abc_visibility_.set(is_visible ? Alembic::AbcGeom::kVisibilityVisible :
Alembic::AbcGeom::kVisibilityHidden);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "IO_abstract_hierarchy_iterator.h"
#include "abc_custom_props.h"
#include "abc_hierarchy_iterator.h"
#include <Alembic/Abc/OObject.h>
#include <memory>
namespace blender {
struct IDProperty;
struct Object;
namespace io::alembic {
class ABCAbstractWriter : public AbstractHierarchyWriter {
protected:
const ABCWriterConstructorArgs args_;
bool frame_has_been_written_;
bool is_animated_;
uint32_t timesample_index_;
Imath::Box3d bounding_box_;
/* 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);
void write(HierarchyContext &context) override;
/* Returns true if the data to be written is actually supported. This would, for example, allow a
* hypothetical camera writer accept a perspective camera but reject an orthogonal one.
*
* Returning false from a transform writer will prevent the object and all its descendants from
* being exported. Returning false from a data writer (object data, hair, or particles) will
* only prevent that data from being written (and thus cause the object to be exported as an
* 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
* is_supported(). */
virtual void create_alembic_objects(const HierarchyContext *context) = 0;
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 io::alembic
} // namespace blender

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_camera.h"
#include "abc_hierarchy_iterator.h"
#include "BKE_scene.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_camera_types.h"
#include "DNA_scene_types.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::AbcGeom::CameraSample;
using Alembic::AbcGeom::OCamera;
using Alembic::AbcGeom::OFloatProperty;
ABCCameraWriter::ABCCameraWriter(const ABCWriterConstructorArgs &args) : ABCAbstractWriter(args) {}
bool ABCCameraWriter::is_supported(const HierarchyContext *context) const
{
const Camera *camera = id_cast<const Camera *>(context->object->data);
return camera->type == CAM_PERSP;
}
void ABCCameraWriter::create_alembic_objects(const HierarchyContext * /*context*/)
{
CLOG_DEBUG(&LOG, "exporting %s", args_.abc_path.c_str());
abc_camera_ = OCamera(args_.abc_parent, args_.abc_name, timesample_index_);
abc_camera_schema_ = abc_camera_.getSchema();
abc_custom_data_container_ = abc_camera_schema_.getUserProperties();
abc_stereo_distance_ = OFloatProperty(
abc_custom_data_container_, "stereoDistance", timesample_index_);
abc_eye_separation_ = OFloatProperty(
abc_custom_data_container_, "eyeSeparation", timesample_index_);
/* Export scene render resolution on cameras as userProperties, for other software (e.g.
* Houdini). */
OFloatProperty render_resx(abc_custom_data_container_, "resx");
OFloatProperty render_resy(abc_custom_data_container_, "resy");
Scene *scene = DEG_get_evaluated_scene(args_.depsgraph);
int width, height;
BKE_render_resolution(&scene->r, false, &width, &height);
render_resx.set(float(width));
render_resy.set(float(height));
}
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)
{
const Camera *cam = id_cast<const Camera *>(context.object->data);
abc_stereo_distance_.set(cam->stereo.convergence_distance);
abc_eye_separation_.set(cam->stereo.interocular_distance);
const double apperture_x = cam->sensor_x / 10.0;
const double apperture_y = cam->sensor_y / 10.0;
const double film_aspect = apperture_x / apperture_y;
CameraSample camera_sample;
camera_sample.setFocalLength(cam->lens);
camera_sample.setHorizontalAperture(apperture_x);
camera_sample.setVerticalAperture(apperture_y);
camera_sample.setHorizontalFilmOffset(apperture_x * cam->shiftx);
camera_sample.setVerticalFilmOffset(apperture_y * cam->shifty * film_aspect);
camera_sample.setNearClippingPlane(cam->clip_start);
camera_sample.setFarClippingPlane(cam->clip_end);
if (cam->dof.focus_object) {
Imath::V3f v(context.object->loc[0] - cam->dof.focus_object->loc[0],
context.object->loc[1] - cam->dof.focus_object->loc[1],
context.object->loc[2] - cam->dof.focus_object->loc[2]);
camera_sample.setFocusDistance(v.length());
}
else {
camera_sample.setFocusDistance(cam->dof.focus_distance);
}
/* Blender camera does not have an fstop param, so try to find a custom prop
* instead. */
camera_sample.setFStop(cam->dof.aperture_fstop);
camera_sample.setLensSqueezeRatio(1.0);
abc_camera_schema_.set(camera_sample);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,37 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include <Alembic/AbcGeom/OCamera.h>
namespace blender::io::alembic {
class ABCCameraWriter : public ABCAbstractWriter {
private:
Alembic::AbcGeom::OCamera abc_camera_;
Alembic::AbcGeom::OCameraSchema abc_camera_schema_;
Alembic::AbcGeom::OCompoundProperty abc_custom_data_container_;
Alembic::AbcGeom::OFloatProperty abc_stereo_distance_;
Alembic::AbcGeom::OFloatProperty abc_eye_separation_;
public:
explicit ABCCameraWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,293 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \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_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"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
using Alembic::AbcGeom::OCompoundProperty;
using Alembic::AbcGeom::OCurves;
using Alembic::AbcGeom::OCurvesSchema;
using Alembic::AbcGeom::OInt16Property;
using Alembic::AbcGeom::ON3fGeomParam;
using Alembic::AbcGeom::OV2fGeomParam;
namespace 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)
{
CLOG_DEBUG(&LOG, "exporting %s", args_.abc_path.c_str());
abc_curve_ = OCurves(args_.abc_parent, args_.abc_name, timesample_index_);
abc_curve_schema_ = abc_curve_.getSchema();
/* 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. */
int resolution_u = 1;
switch (context->object->type) {
case OB_CURVES_LEGACY: {
Curve *curves_id = id_cast<Curve *>(context->object->data);
resolution_u = curves_id->resolu;
break;
}
case OB_CURVES: {
Curves *curves_id = id_cast<Curves *>(context->object->data);
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
resolution_u = curves.resolution().first();
break;
}
default:
break;
}
OCompoundProperty user_props = abc_curve_schema_.getUserProperties();
OInt16Property user_prop_resolu(user_props, ABC_CURVE_RESOLUTION_U_PROPNAME);
user_prop_resolu.set(resolution_u);
}
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)
{
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 = id_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;
}
case OB_CURVES:
curves_id = id_cast<Curves *>(context.object->data);
break;
default:
BLI_assert_unreachable();
return;
}
const bke::CurvesGeometry &curves = curves_id->geometry.wrap();
if (curves.is_empty()) {
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(),
[](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");
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;
Alembic::AbcGeom::CurveType curve_type = Alembic::AbcGeom::kLinear;
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::kLinear;
break;
case CURVE_TYPE_CATMULL_ROM:
curve_basis = Alembic::AbcGeom::kCatmullromBasis;
curve_type = Alembic::AbcGeom::kLinear;
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;
std::vector<float> widths;
std::vector<float> weights;
std::vector<float> knots;
std::vector<uint8_t> orders;
const Span<float3> positions = curves.positions();
const std::optional<Span<float>> nurbs_weights = curves.nurbs_weights();
const VArray<int8_t> nurbs_orders = curves.nurbs_orders();
const VArray<float> radii = curves.radius();
vert_counts.resize(curves.curves_num());
const OffsetIndices points_by_curve = curves.points_by_curve();
const std::optional<Span<float3>> handles_l = curves.handle_positions_left();
const std::optional<Span<float3>> handles_r = curves.handle_positions_right();
if (blender_curve_type == CURVE_TYPE_BEZIER && handles_l && handles_r) {
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 int start_point_index = points.first();
const int last_point_index = points.last();
/* 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]));
widths.push_back(radii[i_point] * 2.0f);
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());
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;
}
if (blender_curve_type == CURVE_TYPE_NURBS) {
if (nurbs_weights) {
weights.resize(curves.points_num());
std::copy_n(nurbs_weights->data(), weights.size(), weights.data());
}
orders.resize(curves.curves_num());
for (const int i_curve : curves.curves_range()) {
orders[i_curve] = nurbs_orders[i_curve];
}
}
offset_indices::copy_group_sizes(points_by_curve, points_by_curve.index_range(), vert_counts);
}
Alembic::AbcGeom::OFloatGeomParam::Sample width_sample;
width_sample.setVals(widths);
OCurvesSchema::Sample sample(verts,
vert_counts,
curve_type,
periodicity,
width_sample,
OV2fGeomParam::Sample(), /* UVs */
ON3fGeomParam::Sample(), /* normals */
curve_basis,
weights,
orders,
knots);
update_bounding_box(context.object);
sample.setSelfBounds(bounding_box_);
abc_curve_schema_.set(sample);
}
ABCCurveMeshWriter::ABCCurveMeshWriter(const ABCWriterConstructorArgs &args)
: ABCGenericMeshWriter(args)
{
}
Mesh *ABCCurveMeshWriter::get_export_mesh(Object *object_eval, bool &r_needsfree)
{
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: {
Curves *curves = id_cast<Curves *>(object_eval->data);
r_needsfree = true;
return bke::curve_to_wire_mesh(curves->geometry.wrap());
}
default:
break;
}
return nullptr;
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include "abc_writer_mesh.h"
#include <Alembic/AbcGeom/OCurves.h>
namespace blender::io::alembic {
extern const std::string ABC_CURVE_RESOLUTION_U_PROPNAME;
class ABCCurveWriter : public ABCAbstractWriter {
private:
Alembic::AbcGeom::OCurves abc_curve_;
Alembic::AbcGeom::OCurvesSchema abc_curve_schema_;
public:
explicit ABCCurveWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
protected:
void do_write(HierarchyContext &context) override;
};
class ABCCurveMeshWriter : public ABCGenericMeshWriter {
public:
ABCCurveMeshWriter(const ABCWriterConstructorArgs &args);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,333 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_hair.h"
#include "intern/abc_axis_conversion.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_object_types.h"
#include "BLI_math_matrix.h"
#include "BLI_math_vector.h"
#include "BKE_customdata.hh"
#include "BKE_mesh_legacy_convert.hh"
#include "BKE_mesh_runtime.hh"
#include "BKE_object.hh"
#include "BKE_particle.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
using Alembic::Abc::P3fArraySamplePtr;
using Alembic::AbcGeom::OCurves;
using Alembic::AbcGeom::OCurvesSchema;
using Alembic::AbcGeom::ON3fGeomParam;
using Alembic::AbcGeom::OV2fGeomParam;
namespace io::alembic {
ABCHairWriter::ABCHairWriter(const ABCWriterConstructorArgs &args)
: ABCAbstractWriter(args), uv_warning_shown_(false)
{
}
void ABCHairWriter::create_alembic_objects(const HierarchyContext * /*context*/)
{
CLOG_DEBUG(&LOG, "exporting %s", args_.abc_path.c_str());
abc_curves_ = OCurves(args_.abc_parent, args_.abc_name, timesample_index_);
abc_curves_schema_ = abc_curves_.getSchema();
}
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. */
return true;
}
void ABCHairWriter::do_write(HierarchyContext &context)
{
const Mesh *mesh = BKE_object_get_evaluated_mesh(context.object);
if (!mesh) {
return;
}
BKE_mesh_tessface_ensure(const_cast<Mesh *>(mesh));
std::vector<Imath::V3f> verts;
std::vector<int32_t> hvertices;
std::vector<Imath::V2f> uv_values;
std::vector<Imath::V3f> norm_values;
ParticleSystem *psys = context.particle_system;
if (psys->pathcache) {
ParticleSettings *part = psys->part;
bool export_children = psys->childcache && part->childtype != 0;
if (!export_children || part->draw & PART_DRAW_PARENT) {
write_hair_sample(
context, const_cast<Mesh *>(mesh), verts, norm_values, uv_values, hvertices);
}
if (export_children) {
write_hair_child_sample(
context, const_cast<Mesh *>(mesh), verts, norm_values, uv_values, hvertices);
}
}
Alembic::Abc::P3fArraySample iPos(verts);
OCurvesSchema::Sample sample(iPos, hvertices);
sample.setBasis(Alembic::AbcGeom::kNoBasis);
sample.setType(Alembic::AbcGeom::kLinear);
sample.setWrap(Alembic::AbcGeom::kNonPeriodic);
if (!uv_values.empty()) {
OV2fGeomParam::Sample uv_smp;
uv_smp.setVals(uv_values);
sample.setUVs(uv_smp);
}
if (!norm_values.empty()) {
ON3fGeomParam::Sample norm_smp;
norm_smp.setVals(norm_values);
sample.setNormals(norm_smp);
}
update_bounding_box(context.object);
sample.setSelfBounds(bounding_box_);
abc_curves_schema_.set(sample);
}
void ABCHairWriter::write_hair_sample(const HierarchyContext &context,
Mesh *mesh,
std::vector<Imath::V3f> &verts,
std::vector<Imath::V3f> &norm_values,
std::vector<Imath::V2f> &uv_values,
std::vector<int32_t> &hvertices)
{
/* Get untransformed vertices, there's a xform under the hair. */
float inv_mat[4][4];
invert_m4_m4_safe(inv_mat, context.object->object_to_world().ptr());
MTFace *mtface = static_cast<MTFace *>(
CustomData_get_layer_for_write(&mesh->fdata_legacy, CD_MTFACE, mesh->totface_legacy));
const MFace *mface = static_cast<const MFace *>(
CustomData_get_layer(&mesh->fdata_legacy, CD_MFACE));
const Span<float3> positions = mesh->vert_positions();
const Span<float3> vert_normals = mesh->vert_normals();
if ((!mtface || !mface) && !uv_warning_shown_) {
CLOG_WARN(&LOG, "No UV set found for underlying geometry of %s", context.object->id.name + 2);
uv_warning_shown_ = true;
}
ParticleSystem *psys = context.particle_system;
ParticleSettings *part = psys->part;
ParticleData *pa = psys->particles;
int k;
ParticleCacheKey **cache = psys->pathcache;
ParticleCacheKey *path;
float normal[3];
Imath::V3f tmp_nor;
for (int p = 0; p < psys->totpart; p++, pa++) {
/* underlying info for faces-only emission */
path = cache[p];
/* Write UV and normal vectors */
if (part->from == PART_FROM_FACE && mtface) {
const int num = pa->num_dmcache >= 0 ? pa->num_dmcache : pa->num;
if (num < mesh->totface_legacy) {
/* TODO(Sybren): check whether the null check here and if(mface) are actually required
*/
const MFace *face = mface == nullptr ? nullptr : &mface[num];
MTFace *tface = mtface + num;
if (mface) {
float uv[2], mapfw[4], vec[3];
psys_interpolate_uvs(tface, face->v4, pa->fuv, uv);
uv_values.emplace_back(uv[0], uv[1]);
psys_interpolate_face(mesh,
reinterpret_cast<const float (*)[3]>(positions.data()),
reinterpret_cast<const float (*)[3]>(vert_normals.data()),
face,
tface,
nullptr,
mapfw,
vec,
normal,
nullptr,
nullptr,
nullptr);
copy_yup_from_zup(tmp_nor.getValue(), normal);
norm_values.push_back(tmp_nor);
}
}
else {
CLOG_WARN(&LOG, "Particle to faces overflow (%d/%d)", num, mesh->totface_legacy);
}
}
else if (part->from == PART_FROM_VERT && mtface) {
/* vertex id */
const int num = (pa->num_dmcache >= 0) ? pa->num_dmcache : pa->num;
/* iterate over all faces to find a corresponding underlying UV */
for (int n = 0; n < mesh->totface_legacy; n++) {
const MFace *face = &mface[n];
const MTFace *tface = mtface + n;
uint vtx[4];
vtx[0] = face->v1;
vtx[1] = face->v2;
vtx[2] = face->v3;
vtx[3] = face->v4;
bool found = false;
for (int o = 0; o < 4; o++) {
if (o > 2 && vtx[o] == 0) {
break;
}
if (vtx[o] == num) {
uv_values.emplace_back(tface->uv[o][0], tface->uv[o][1]);
copy_v3_v3(normal, vert_normals[vtx[o]]);
copy_yup_from_zup(tmp_nor.getValue(), normal);
norm_values.push_back(tmp_nor);
found = true;
break;
}
}
if (found) {
break;
}
}
}
int steps = path->segments + 1;
hvertices.push_back(steps);
for (k = 0; k < steps; k++, path++) {
float vert[3];
copy_v3_v3(vert, path->co);
mul_m4_v3(inv_mat, vert);
/* Convert Z-up to Y-up. */
verts.emplace_back(vert[0], vert[2], -vert[1]);
}
}
}
void ABCHairWriter::write_hair_child_sample(const HierarchyContext &context,
Mesh *mesh,
std::vector<Imath::V3f> &verts,
std::vector<Imath::V3f> &norm_values,
std::vector<Imath::V2f> &uv_values,
std::vector<int32_t> &hvertices)
{
/* Get untransformed vertices, there's a xform under the hair. */
float inv_mat[4][4];
invert_m4_m4_safe(inv_mat, context.object->object_to_world().ptr());
const MFace *mface = static_cast<const MFace *>(
CustomData_get_layer(&mesh->fdata_legacy, CD_MFACE));
MTFace *mtface = static_cast<MTFace *>(
CustomData_get_layer_for_write(&mesh->fdata_legacy, CD_MTFACE, mesh->totface_legacy));
const Span<float3> positions = mesh->vert_positions();
const Span<float3> vert_normals = mesh->vert_normals();
ParticleSystem *psys = context.particle_system;
ParticleSettings *part = psys->part;
ParticleCacheKey **cache = psys->childcache;
ParticleCacheKey *path;
ChildParticle *pc = psys->child;
for (int p = 0; p < psys->totchild; p++, pc++) {
path = cache[p];
if (part->from == PART_FROM_FACE && part->childtype != PART_CHILD_PARTICLES && mtface) {
const int num = pc->num;
if (num < 0) {
CLOG_WARN(
&LOG,
"Child particle of hair system %s has unknown face index of geometry of %s, skipping "
"child hair.",
psys->name,
context.object->id.name + 2);
continue;
}
const MFace *face = &mface[num];
MTFace *tface = mtface + num;
float uv[2], tmpnor[3], mapfw[4], vec[3];
psys_interpolate_uvs(tface, face->v4, pc->fuv, uv);
uv_values.emplace_back(uv[0], uv[1]);
psys_interpolate_face(mesh,
reinterpret_cast<const float (*)[3]>(positions.data()),
reinterpret_cast<const float (*)[3]>(vert_normals.data()),
face,
tface,
nullptr,
mapfw,
vec,
tmpnor,
nullptr,
nullptr,
nullptr);
/* Convert Z-up to Y-up. */
norm_values.emplace_back(tmpnor[0], tmpnor[2], -tmpnor[1]);
}
else {
if (!uv_values.empty()) {
uv_values.push_back(uv_values[pc->parent]);
}
if (!norm_values.empty()) {
norm_values.push_back(norm_values[pc->parent]);
}
}
int steps = path->segments + 1;
hvertices.push_back(steps);
for (int k = 0; k < steps; k++) {
float vert[3];
copy_v3_v3(vert, path->co);
mul_m4_v3(inv_mat, vert);
/* Convert Z-up to Y-up. */
verts.emplace_back(vert[0], vert[2], -vert[1]);
path++;
}
}
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include <Alembic/AbcGeom/OCurves.h>
#include <vector>
namespace blender::io::alembic {
class ABCHairWriter : public ABCAbstractWriter {
private:
Alembic::AbcGeom::OCurves abc_curves_;
Alembic::AbcGeom::OCurvesSchema abc_curves_schema_;
bool uv_warning_shown_;
public:
explicit ABCHairWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
protected:
void do_write(HierarchyContext &context) override;
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,
struct Mesh *mesh,
std::vector<Imath::V3f> &verts,
std::vector<Imath::V3f> &norm_values,
std::vector<Imath::V2f> &uv_values,
std::vector<int32_t> &hvertices);
void write_hair_child_sample(const HierarchyContext &context,
struct Mesh *mesh,
std::vector<Imath::V3f> &verts,
std::vector<Imath::V3f> &norm_values,
std::vector<Imath::V2f> &uv_values,
std::vector<int32_t> &hvertices);
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,68 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_instance.h"
#include "abc_hierarchy_iterator.h"
#include "BLI_assert.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::Abc::OObject;
ABCInstanceWriter::ABCInstanceWriter(const ABCWriterConstructorArgs &args)
: ABCAbstractWriter(args)
{
}
void ABCInstanceWriter::create_alembic_objects(const HierarchyContext *context)
{
OObject original = args_.hierarchy_iterator->get_alembic_object(context->original_export_path);
OObject abc_parent = args_.abc_parent;
if (!abc_parent.addChildInstance(original, args_.abc_name)) {
CLOG_WARN(&LOG, "unable to export %s as instance", args_.abc_path.c_str());
return;
}
CLOG_DEBUG(&LOG, "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. */
BLI_assert_msg(0, "ABCInstanceWriter cannot return its Alembic OObject");
return OObject();
}
bool ABCInstanceWriter::is_supported(const HierarchyContext *context) const
{
return context->is_instance();
}
void ABCInstanceWriter::do_write(HierarchyContext & /*context*/)
{
/* Instances don't have data to be written. Just creating them is enough. */
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
namespace blender::io::alembic {
/* Writer for Alembic instances, i.e. data that references another Alembic object.
*
* Note that the Alembic object created by this writer cannot be used as a
* parent, because it already instantiates the entire hierarchy of the
* referenced object. */
class ABCInstanceWriter : public ABCAbstractWriter {
public:
explicit ABCInstanceWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
protected:
bool is_supported(const HierarchyContext *context) const override;
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

@@ -0,0 +1,72 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_mball.h"
#include "abc_hierarchy_iterator.h"
#include "BKE_lib_id.hh"
#include "BKE_mball.hh"
#include "BKE_mesh.hh"
#include "BKE_object.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_mesh_types.h"
namespace blender::io::alembic {
ABCMetaballWriter::ABCMetaballWriter(const ABCWriterConstructorArgs &args)
: ABCGenericMeshWriter(args)
{
}
bool ABCMetaballWriter::is_supported(const HierarchyContext *context) const
{
Scene *scene = DEG_get_input_scene(args_.depsgraph);
bool supported = is_basis_ball(scene, context->object) &&
ABCGenericMeshWriter::is_supported(context);
return supported;
}
bool ABCMetaballWriter::check_is_animated(const HierarchyContext & /*context*/) const
{
/* We assume that meta-balls are always animated, as the current object may
* not be animated but another ball in the same group may be. */
return true;
}
bool ABCMetaballWriter::export_as_subdivision_surface(Object * /*ob_eval*/) const
{
/* Meta-balls should be exported to subdivision surfaces, if the export options allow. */
return true;
}
Mesh *ABCMetaballWriter::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;
}
r_needsfree = true;
return BKE_mesh_new_from_object(args_.depsgraph, object_eval, false, false, true);
}
void ABCMetaballWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
bool ABCMetaballWriter::is_basis_ball(Scene *scene, Object *ob) const
{
Object *basis_ob = BKE_mball_basis_find(*DEG_get_bmain(args_.depsgraph), scene, ob);
return ob == basis_ob;
}
} // namespace blender::io::alembic

View File

@@ -0,0 +1,29 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_mesh.h"
namespace blender::io::alembic {
class ABCMetaballWriter : public ABCGenericMeshWriter {
public:
explicit ABCMetaballWriter(const ABCWriterConstructorArgs &args);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
void free_export_mesh(Mesh *mesh) override;
bool is_supported(const HierarchyContext *context) const override;
bool check_is_animated(const HierarchyContext &context) const override;
bool export_as_subdivision_surface(Object *ob_eval) const override;
private:
bool is_basis_ball(Scene *scene, Object *ob) const;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,568 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_mesh.h"
#include "abc_hierarchy_iterator.h"
#include "intern/abc_axis_conversion.h"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_lib_id.hh"
#include "BKE_material.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_wrapper.hh"
#include "BKE_object.hh"
#include "BKE_subdiv.hh"
#include "bmesh.hh"
#include "bmesh_tools.hh"
#include "DNA_customdata_types.h"
#include "DNA_material_types.h"
#include "DNA_mesh_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
using Alembic::Abc::FloatArraySample;
using Alembic::Abc::Int32ArraySample;
using Alembic::Abc::OObject;
using Alembic::Abc::V2fArraySample;
using Alembic::Abc::V3fArraySample;
using Alembic::AbcGeom::kFacevaryingScope;
using Alembic::AbcGeom::OBoolProperty;
using Alembic::AbcGeom::OCompoundProperty;
using Alembic::AbcGeom::OFaceSet;
using Alembic::AbcGeom::OFaceSetSchema;
using Alembic::AbcGeom::ON3fGeomParam;
using Alembic::AbcGeom::OPolyMesh;
using Alembic::AbcGeom::OPolyMeshSchema;
using Alembic::AbcGeom::OSubD;
using Alembic::AbcGeom::OSubDSchema;
using Alembic::AbcGeom::OV2fGeomParam;
using Alembic::AbcGeom::UInt32ArraySample;
namespace io::alembic {
/* NOTE: Alembic's polygon winding order is clockwise, to match with Renderman. */
static void get_vertices(Mesh *mesh, std::vector<Imath::V3f> &points);
static void get_topology(Mesh *mesh,
std::vector<int32_t> &face_verts,
std::vector<int32_t> &loop_counts);
static void get_edge_creases(Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<int32_t> &lengths,
std::vector<float> &sharpnesses);
static void get_vert_creases(Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<float> &sharpnesses);
static void get_loop_normals(const Mesh *mesh, std::vector<Imath::V3f> &normals);
ABCGenericMeshWriter::ABCGenericMeshWriter(const ABCWriterConstructorArgs &args)
: ABCAbstractWriter(args), is_subd_(false)
{
}
void ABCGenericMeshWriter::create_alembic_objects(const HierarchyContext *context)
{
if (!args_.export_params->apply_subdiv && export_as_subdivision_surface(context->object)) {
is_subd_ = args_.export_params->use_subdiv_schema;
}
if (is_subd_) {
CLOG_DEBUG(&LOG, "exporting OSubD %s", args_.abc_path.c_str());
abc_subdiv_ = OSubD(args_.abc_parent, args_.abc_name, timesample_index_);
abc_subdiv_schema_ = abc_subdiv_.getSchema();
}
else {
CLOG_DEBUG(&LOG, "exporting OPolyMesh %s", args_.abc_path.c_str());
abc_poly_mesh_ = OPolyMesh(args_.abc_parent, args_.abc_name, timesample_index_);
abc_poly_mesh_schema_ = abc_poly_mesh_.getSchema();
OCompoundProperty typeContainer = abc_poly_mesh_.getSchema().getUserProperties();
OBoolProperty type(typeContainer, "meshtype");
type.set(subsurf_modifier_ == nullptr);
}
}
Alembic::Abc::OObject ABCGenericMeshWriter::get_alembic_object() const
{
if (is_subd_) {
return abc_subdiv_;
}
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);
for (; md; md = md->prev) {
/* This modifier has been temporarily disabled by SubdivModifierDisabler,
* so this indicates this is to be exported as subdivision surface. */
if (md->type == eModifierType_Subsurf && (md->mode & eModifierMode_DisableTemporary)) {
return true;
}
}
return false;
}
bool ABCGenericMeshWriter::is_supported(const HierarchyContext *context) const
{
return context->is_object_visible(args_.export_params->evaluation_mode);
}
void ABCGenericMeshWriter::do_write(HierarchyContext &context)
{
Object *object = context.object;
bool needsfree = false;
Mesh *mesh = get_export_mesh(object, needsfree);
if (mesh == nullptr) {
return;
}
/* Ensure data exists if currently in edit mode. */
BKE_mesh_wrapper_ensure_mdata(mesh);
if (args_.export_params->triangulate) {
const bool tag_only = false;
const int quad_method = args_.export_params->quad_method;
const int ngon_method = args_.export_params->ngon_method;
BMeshCreateParams bmesh_create_params{};
BMeshFromMeshParams bmesh_from_mesh_params{};
bmesh_from_mesh_params.calc_face_normal = true;
bmesh_from_mesh_params.calc_vert_normal = true;
BMesh *bm = BKE_mesh_to_bmesh_ex(mesh, &bmesh_create_params, &bmesh_from_mesh_params);
BM_mesh_triangulate(bm, quad_method, ngon_method, 4, tag_only, nullptr, nullptr, nullptr);
Mesh *triangulated_mesh = BKE_mesh_from_bmesh_for_eval_nomain(bm, nullptr, mesh);
BM_mesh_free(bm);
if (needsfree) {
free_export_mesh(mesh);
}
mesh = triangulated_mesh;
needsfree = true;
}
m_custom_data_config.pack_uvs = args_.export_params->packuv;
m_custom_data_config.mesh = mesh;
m_custom_data_config.face_offsets = mesh->face_offsets_for_write().data();
m_custom_data_config.corner_verts = mesh->corner_verts_for_write().data();
m_custom_data_config.faces_num = mesh->faces_num;
m_custom_data_config.totloop = mesh->corners_num;
m_custom_data_config.totvert = mesh->verts_num;
m_custom_data_config.timesample_index = timesample_index_;
try {
if (is_subd_) {
write_subd(context, mesh);
}
else {
write_mesh(context, mesh);
}
if (needsfree) {
free_export_mesh(mesh);
}
}
catch (...) {
if (needsfree) {
free_export_mesh(mesh);
}
throw;
}
}
void ABCGenericMeshWriter::free_export_mesh(Mesh *mesh)
{
BKE_id_free(nullptr, mesh);
}
void ABCGenericMeshWriter::write_mesh(HierarchyContext &context, Mesh *mesh)
{
std::vector<Imath::V3f> points, normals;
std::vector<int32_t> face_verts, loop_counts;
std::vector<Imath::V3f> velocities;
get_vertices(mesh, points);
get_topology(mesh, face_verts, loop_counts);
if (!frame_has_been_written_ && args_.export_params->face_sets) {
write_face_sets(context.object, mesh, abc_poly_mesh_schema_);
}
OPolyMeshSchema::Sample mesh_sample = OPolyMeshSchema::Sample(
V3fArraySample(points), Int32ArraySample(face_verts), Int32ArraySample(loop_counts));
UVSample uvs_and_indices;
if (args_.export_params->uvs) {
const char *name = get_uv_sample(uvs_and_indices, m_custom_data_config, *mesh);
if (!uvs_and_indices.indices.empty() && !uvs_and_indices.uvs.empty()) {
OV2fGeomParam::Sample uv_sample;
uv_sample.setVals(V2fArraySample(uvs_and_indices.uvs));
uv_sample.setIndices(UInt32ArraySample(uvs_and_indices.indices));
uv_sample.setScope(kFacevaryingScope);
abc_poly_mesh_schema_.setUVSourceName(name);
mesh_sample.setUVs(uv_sample);
}
write_custom_data(
abc_poly_mesh_schema_.getArbGeomParams(), m_custom_data_config, *mesh, CD_PROP_FLOAT2);
}
if (args_.export_params->normals) {
get_loop_normals(mesh, normals);
ON3fGeomParam::Sample normals_sample;
if (!normals.empty()) {
normals_sample.setScope(kFacevaryingScope);
normals_sample.setVals(V3fArraySample(normals));
}
mesh_sample.setNormals(normals_sample);
}
if (args_.export_params->orcos) {
write_generated_coordinates(abc_poly_mesh_schema_.getArbGeomParams(), m_custom_data_config);
}
if (get_velocities(mesh, velocities)) {
mesh_sample.setVelocities(V3fArraySample(velocities));
}
update_bounding_box(context.object);
mesh_sample.setSelfBounds(bounding_box_);
abc_poly_mesh_schema_.set(mesh_sample);
write_arb_geo_params(mesh);
}
void ABCGenericMeshWriter::write_subd(HierarchyContext &context, Mesh *mesh)
{
std::vector<float> edge_crease_sharpness, vert_crease_sharpness;
std::vector<Imath::V3f> points;
std::vector<int32_t> face_verts, loop_counts;
std::vector<int32_t> edge_crease_indices, edge_crease_lengths, vert_crease_indices;
get_vertices(mesh, points);
get_topology(mesh, face_verts, loop_counts);
get_edge_creases(mesh, edge_crease_indices, edge_crease_lengths, edge_crease_sharpness);
get_vert_creases(mesh, vert_crease_indices, vert_crease_sharpness);
if (!frame_has_been_written_ && args_.export_params->face_sets) {
write_face_sets(context.object, mesh, abc_subdiv_schema_);
}
OSubDSchema::Sample subdiv_sample = OSubDSchema::Sample(
V3fArraySample(points), Int32ArraySample(face_verts), Int32ArraySample(loop_counts));
UVSample sample;
if (args_.export_params->uvs) {
const char *name = get_uv_sample(sample, m_custom_data_config, *mesh);
if (!sample.indices.empty() && !sample.uvs.empty()) {
OV2fGeomParam::Sample uv_sample;
uv_sample.setVals(V2fArraySample(sample.uvs));
uv_sample.setIndices(UInt32ArraySample(sample.indices));
uv_sample.setScope(kFacevaryingScope);
abc_subdiv_schema_.setUVSourceName(name);
subdiv_sample.setUVs(uv_sample);
}
write_custom_data(
abc_subdiv_schema_.getArbGeomParams(), m_custom_data_config, *mesh, CD_PROP_FLOAT2);
}
if (args_.export_params->orcos) {
write_generated_coordinates(abc_subdiv_schema_.getArbGeomParams(), m_custom_data_config);
}
if (!edge_crease_indices.empty()) {
subdiv_sample.setCreaseIndices(Int32ArraySample(edge_crease_indices));
subdiv_sample.setCreaseLengths(Int32ArraySample(edge_crease_lengths));
subdiv_sample.setCreaseSharpnesses(FloatArraySample(edge_crease_sharpness));
}
if (!vert_crease_indices.empty()) {
subdiv_sample.setCornerIndices(Int32ArraySample(vert_crease_indices));
subdiv_sample.setCornerSharpnesses(FloatArraySample(vert_crease_sharpness));
}
update_bounding_box(context.object);
subdiv_sample.setSelfBounds(bounding_box_);
abc_subdiv_schema_.set(subdiv_sample);
write_arb_geo_params(mesh);
}
template<typename Schema>
void ABCGenericMeshWriter::write_face_sets(Object *object, Mesh *mesh, Schema &schema)
{
std::map<std::string, std::vector<int32_t>> geo_groups;
get_geo_groups(object, mesh, geo_groups);
std::map<std::string, std::vector<int32_t>>::iterator it;
for (it = geo_groups.begin(); it != geo_groups.end(); ++it) {
OFaceSet face_set = schema.createFaceSet(it->first);
OFaceSetSchema::Sample samp;
samp.setFaces(Int32ArraySample(it->second));
face_set.getSchema().set(samp);
}
}
void ABCGenericMeshWriter::write_arb_geo_params(Mesh *mesh)
{
if (!args_.export_params->vcolors) {
return;
}
OCompoundProperty arb_geom_params;
if (is_subd_) {
arb_geom_params = abc_subdiv_.getSchema().getArbGeomParams();
}
else {
arb_geom_params = abc_poly_mesh_.getSchema().getArbGeomParams();
}
write_custom_data(arb_geom_params, m_custom_data_config, *mesh, CD_PROP_BYTE_COLOR);
}
bool ABCGenericMeshWriter::get_velocities(Mesh *mesh, std::vector<Imath::V3f> &vels)
{
/* Export velocity attribute output by fluid sim, sequence cache modifier
* and geometry nodes. */
const bke::AttributeAccessor attributes = mesh->attributes();
const VArraySpan attr = *attributes.lookup<float3>("velocity", bke::AttrDomain::Point);
if (attr.is_empty()) {
return false;
}
const int totverts = mesh->verts_num;
vels.clear();
vels.resize(totverts);
for (int i = 0; i < totverts; i++) {
copy_yup_from_zup(vels[i].getValue(), attr[i]);
}
return true;
}
void ABCGenericMeshWriter::get_geo_groups(Object *object,
Mesh *mesh,
std::map<std::string, std::vector<int32_t>> &geo_groups)
{
const bke::AttributeAccessor attributes = mesh->attributes();
const VArraySpan<int> material_indices = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Face, 0);
for (const int i : material_indices.index_range()) {
short mnr = material_indices[i];
Material *mat = BKE_object_material_get(object, mnr + 1);
if (!mat) {
continue;
}
std::string name = args_.hierarchy_iterator->get_id_name(&mat->id);
if (!geo_groups.contains(name)) {
std::vector<int32_t> faceArray;
geo_groups[name] = faceArray;
}
geo_groups[name].push_back(i);
}
if (geo_groups.empty()) {
Material *mat = BKE_object_material_get(object, 1);
std::string name = (mat) ? args_.hierarchy_iterator->get_id_name(&mat->id) : "default";
std::vector<int32_t> faceArray;
for (int i = 0, e = mesh->totface_legacy; i < e; i++) {
faceArray.push_back(i);
}
geo_groups[name] = faceArray;
}
}
/* NOTE: Alembic's polygon winding order is clockwise, to match with Renderman. */
static void get_vertices(Mesh *mesh, std::vector<Imath::V3f> &points)
{
points.clear();
points.resize(mesh->verts_num);
const Span<float3> positions = mesh->vert_positions();
for (int i = 0, e = mesh->verts_num; i < e; i++) {
copy_yup_from_zup(points[i].getValue(), positions[i]);
}
}
static void get_topology(Mesh *mesh,
std::vector<int32_t> &face_verts,
std::vector<int32_t> &loop_counts)
{
const OffsetIndices faces = mesh->faces();
const Span<int> corner_verts = mesh->corner_verts();
face_verts.clear();
loop_counts.clear();
face_verts.reserve(corner_verts.size());
loop_counts.reserve(faces.size());
/* NOTE: data needs to be written in the reverse order. */
for (const int i : faces.index_range()) {
const IndexRange face = faces[i];
loop_counts.push_back(face.size());
int corner = face.start() + (face.size() - 1);
for (int j = 0; j < face.size(); j++, corner--) {
face_verts.push_back(corner_verts[corner]);
}
}
}
static void get_edge_creases(Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<int32_t> &lengths,
std::vector<float> &sharpnesses)
{
indices.clear();
lengths.clear();
sharpnesses.clear();
const bke::AttributeAccessor attributes = mesh->attributes();
const bke::AttributeReader attribute = attributes.lookup<float>("crease_edge",
bke::AttrDomain::Edge);
if (!attribute) {
return;
}
const VArraySpan creases(*attribute);
const Span<int2> edges = mesh->edges();
for (const int i : edges.index_range()) {
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
if (crease != 0.0f) {
indices.push_back(edges[i][0]);
indices.push_back(edges[i][1]);
sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
}
}
lengths.resize(sharpnesses.size(), 2);
}
static void get_vert_creases(Mesh *mesh,
std::vector<int32_t> &indices,
std::vector<float> &sharpnesses)
{
indices.clear();
sharpnesses.clear();
const bke::AttributeAccessor attributes = mesh->attributes();
const bke::AttributeReader attribute = attributes.lookup<float>("crease_vert",
bke::AttrDomain::Point);
if (!attribute) {
return;
}
const VArraySpan creases(*attribute);
for (const int i : creases.index_range()) {
const float crease = std::clamp(creases[i], 0.0f, 1.0f);
if (crease != 0.0f) {
indices.push_back(i);
sharpnesses.push_back(bke::subdiv::crease_to_sharpness(crease));
}
}
}
static void get_loop_normals(const Mesh *mesh, std::vector<Imath::V3f> &normals)
{
normals.clear();
switch (mesh->normals_domain()) {
case bke::MeshNormalDomain::Point: {
/* If all faces are smooth shaded, and there are no custom normals, we don't need to
* export normals at all. This is also done by other software, see #71246. */
break;
}
case bke::MeshNormalDomain::Face: {
normals.resize(mesh->corners_num);
MutableSpan dst_normals(reinterpret_cast<float3 *>(normals.data()), normals.size());
const OffsetIndices faces = mesh->faces();
const Span<float3> face_normals = mesh->face_normals();
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int i : range) {
float3 y_up;
copy_yup_from_zup(y_up, face_normals[i]);
dst_normals.slice(faces[i]).fill(y_up);
}
});
break;
}
case bke::MeshNormalDomain::Corner: {
normals.resize(mesh->corners_num);
MutableSpan dst_normals(reinterpret_cast<float3 *>(normals.data()), normals.size());
/* NOTE: data needs to be written in the reverse order. */
const OffsetIndices faces = mesh->faces();
const Span<float3> corner_normals = mesh->corner_normals();
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
for (const int i : range) {
const IndexRange face = faces[i];
for (const int i : face.index_range()) {
copy_yup_from_zup(dst_normals[face.last(i)], corner_normals[face[i]]);
}
}
});
break;
}
}
}
ABCMeshWriter::ABCMeshWriter(const ABCWriterConstructorArgs &args) : ABCGenericMeshWriter(args) {}
Mesh *ABCMeshWriter::get_export_mesh(Object *object_eval, bool & /*r_needsfree*/)
{
return BKE_object_get_evaluated_mesh(object_eval);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include "intern/abc_customdata.h"
#include <Alembic/AbcGeom/OPolyMesh.h>
#include <Alembic/AbcGeom/OSubD.h>
namespace blender {
struct ModifierData;
namespace io::alembic {
/* Writer for Alembic geometry. Does not assume the object is a mesh object. */
class ABCGenericMeshWriter : public ABCAbstractWriter {
private:
/* Either poly-mesh or subdivision-surface is used, depending on is_subd_.
* References to the schema must be kept, or Alembic will not properly write. */
Alembic::AbcGeom::OPolyMesh abc_poly_mesh_;
Alembic::AbcGeom::OPolyMeshSchema abc_poly_mesh_schema_;
Alembic::AbcGeom::OSubD abc_subdiv_;
Alembic::AbcGeom::OSubDSchema abc_subdiv_schema_;
/* Determines whether a poly mesh or a subdivision surface is exported.
* The value is set by an export option but only true if there is a subdivision modifier on the
* exported object. */
bool is_subd_;
ModifierData *subsurf_modifier_;
CDStreamConfig m_custom_data_config;
public:
explicit ABCGenericMeshWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
virtual Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) = 0;
virtual void free_export_mesh(Mesh *mesh);
virtual bool export_as_subdivision_surface(Object *ob_eval) const;
private:
void write_mesh(HierarchyContext &context, Mesh *mesh);
void write_subd(HierarchyContext &context, Mesh *mesh);
template<typename Schema> void write_face_sets(Object *object, Mesh *mesh, Schema &schema);
void write_arb_geo_params(Mesh *mesh);
bool get_velocities(Mesh *mesh, std::vector<Imath::V3f> &vels);
void get_geo_groups(Object *object,
Mesh *mesh,
std::map<std::string, std::vector<int32_t>> &geo_groups);
};
/* Writer for Alembic geometry of Blender Mesh objects. */
class ABCMeshWriter : public ABCGenericMeshWriter {
public:
ABCMeshWriter(const ABCWriterConstructorArgs &args);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
};
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,184 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_nurbs.h"
#include "intern/abc_axis_conversion.h"
#include "DNA_curve_types.h"
#include "DNA_object_types.h"
#include "BLI_listbase.h"
#include "BKE_curve.hh"
#include "BKE_object_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::Abc::OObject;
using Alembic::AbcGeom::FloatArraySample;
using Alembic::AbcGeom::OBoolProperty;
using Alembic::AbcGeom::OCompoundProperty;
using Alembic::AbcGeom::ONuPatch;
using Alembic::AbcGeom::ONuPatchSchema;
ABCNurbsWriter::ABCNurbsWriter(const ABCWriterConstructorArgs &args) : ABCAbstractWriter(args) {}
void ABCNurbsWriter::create_alembic_objects(const HierarchyContext *context)
{
Curve *curve = id_cast<Curve *>(context->object->data);
size_t num_nurbs = curve->nurb.count();
OObject abc_parent = args_.abc_parent;
const char *abc_parent_path = abc_parent.getFullName().c_str();
for (size_t i = 0; i < num_nurbs; i++) {
std::stringstream patch_name_stream;
patch_name_stream << args_.abc_name << '_' << i;
while (abc_parent.getChildHeader(patch_name_stream.str())) {
patch_name_stream << "_";
}
std::string patch_name = patch_name_stream.str();
CLOG_DEBUG(&LOG, "exporting %s/%s", abc_parent_path, patch_name.c_str());
ONuPatch nurbs(abc_parent, patch_name, timesample_index_);
abc_nurbs_.push_back(nurbs);
abc_nurbs_schemas_.push_back(nurbs.getSchema());
}
}
OObject ABCNurbsWriter::get_alembic_object() const
{
if (abc_nurbs_.empty()) {
return OObject();
}
/* For parenting purposes within the Alembic file, all NURBS patches are equal, so just use the
* first one. */
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. */
Curve *cu = id_cast<Curve *>(context.object->data);
return (cu->key != nullptr);
}
bool ABCNurbsWriter::is_supported(const HierarchyContext *context) const
{
return ELEM(context->object->type, OB_SURF, OB_CURVES_LEGACY);
}
static void get_knots(std::vector<float> &knots, const int num_knots, float *nu_knots)
{
if (num_knots <= 1) {
return;
}
/* Add an extra knot at the beginning and end of the array since most apps
* require/expect them. */
knots.reserve(num_knots + 2);
knots.push_back(0.0f);
for (int i = 0; i < num_knots; i++) {
knots.push_back(nu_knots[i]);
}
knots[0] = 2.0f * knots[1] - knots[2];
knots.push_back(2.0f * knots[num_knots] - knots[num_knots - 1]);
}
void ABCNurbsWriter::do_write(HierarchyContext &context)
{
Curve *curve = id_cast<Curve *>(context.object->data);
ListBaseT<Nurb> *nulb;
if (context.object->runtime->curve_cache->deformed_nurbs.first != nullptr) {
nulb = &context.object->runtime->curve_cache->deformed_nurbs;
}
else {
nulb = BKE_curve_nurbs_get(curve);
}
size_t count = 0;
for (Nurb *nu = static_cast<Nurb *>(nulb->first); nu; nu = nu->next, count++) {
std::vector<float> knotsU;
get_knots(knotsU, KNOTSU(nu), nu->knotsu);
std::vector<float> knotsV;
get_knots(knotsV, KNOTSV(nu), nu->knotsv);
const int size = nu->pntsu * nu->pntsv;
std::vector<Imath::V3f> positions(size);
std::vector<float> weights(size);
const BPoint *bp = nu->bp;
for (int i = 0; i < size; i++, bp++) {
copy_yup_from_zup(positions[i].getValue(), bp->vec);
weights[i] = bp->vec[3];
}
ONuPatchSchema::Sample sample;
sample.setUOrder(nu->orderu + 1);
sample.setVOrder(nu->orderv + 1);
sample.setPositions(positions);
sample.setPositionWeights(weights);
sample.setUKnot(FloatArraySample(knotsU));
sample.setVKnot(FloatArraySample(knotsV));
sample.setNu(nu->pntsu);
sample.setNv(nu->pntsv);
/* TODO(kevin): to accommodate other software we should duplicate control
* points to indicate that a NURBS is cyclic. */
OCompoundProperty user_props = abc_nurbs_schemas_[count].getUserProperties();
if ((nu->flagu & CU_NURB_ENDPOINT) != 0) {
OBoolProperty prop(user_props, "endpoint_u");
prop.set(true);
}
if ((nu->flagv & CU_NURB_ENDPOINT) != 0) {
OBoolProperty prop(user_props, "endpoint_v");
prop.set(true);
}
if ((nu->flagu & CU_NURB_CYCLIC) != 0) {
OBoolProperty prop(user_props, "cyclic_u");
prop.set(true);
}
if ((nu->flagv & CU_NURB_CYCLIC) != 0) {
OBoolProperty prop(user_props, "cyclic_v");
prop.set(true);
}
abc_nurbs_schemas_[count].set(sample);
}
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include "abc_writer_mesh.h"
#include <Alembic/AbcGeom/ONuPatch.h>
#include <vector>
namespace blender::io::alembic {
class ABCNurbsWriter : public ABCAbstractWriter {
private:
std::vector<Alembic::AbcGeom::ONuPatch> abc_nurbs_;
std::vector<Alembic::AbcGeom::ONuPatchSchema> abc_nurbs_schemas_;
public:
explicit ABCNurbsWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
protected:
bool is_supported(const HierarchyContext *context) const override;
void do_write(HierarchyContext &context) override;
bool check_is_animated(const HierarchyContext &context) const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
};
class ABCNurbsMeshWriter : public ABCGenericMeshWriter {
public:
explicit ABCNurbsMeshWriter(const ABCWriterConstructorArgs &args);
protected:
Mesh *get_export_mesh(Object *object_eval, bool &r_needsfree) override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,133 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_points.h"
#include "DNA_object_types.h"
#include "DNA_particle_types.h"
#include "BLI_math_matrix.h"
#include "BLI_math_vector.h"
#include "BKE_particle.h"
#include "DEG_depsgraph_query.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::AbcGeom::kVertexScope;
using Alembic::AbcGeom::OPoints;
using Alembic::AbcGeom::OPointsSchema;
ABCPointsWriter::ABCPointsWriter(const ABCWriterConstructorArgs &args) : ABCAbstractWriter(args) {}
void ABCPointsWriter::create_alembic_objects(const HierarchyContext * /*context*/)
{
CLOG_DEBUG(&LOG, "exporting OPoints %s", args_.abc_path.c_str());
abc_points_ = OPoints(args_.abc_parent, args_.abc_name, timesample_index_);
abc_points_schema_ = abc_points_.getSchema();
}
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,
PART_EMITTER,
PART_FLUID_FLIP,
PART_FLUID_SPRAY,
PART_FLUID_BUBBLE,
PART_FLUID_FOAM,
PART_FLUID_TRACER,
PART_FLUID_SPRAYFOAM,
PART_FLUID_SPRAYBUBBLE,
PART_FLUID_FOAMBUBBLE,
PART_FLUID_SPRAYFOAMBUBBLE);
}
bool ABCPointsWriter::check_is_animated(const HierarchyContext & /*context*/) const
{
/* We assume that particles are always animated. */
return true;
}
void ABCPointsWriter::do_write(HierarchyContext &context)
{
BLI_assert(context.particle_system != nullptr);
std::vector<Imath::V3f> points;
std::vector<Imath::V3f> velocities;
std::vector<float> widths;
std::vector<uint64_t> ids;
ParticleSystem *psys = context.particle_system;
ParticleKey state;
ParticleSimulationData sim;
sim.depsgraph = args_.depsgraph;
sim.scene = DEG_get_evaluated_scene(args_.depsgraph);
sim.ob = context.object;
sim.psys = psys;
psys_sim_data_init(&sim);
uint64_t index = 0;
for (int p = 0; p < psys->totpart; p++) {
float pos[3], vel[3];
if (psys->particles[p].flag & (PARS_NO_DISP | PARS_UNEXIST)) {
continue;
}
state.time = DEG_get_ctime(args_.depsgraph);
if (psys_get_particle_state(&sim, p, &state, false) == 0) {
continue;
}
/* location */
mul_v3_m4v3(pos, context.object->world_to_object().ptr(), state.co);
/* velocity */
sub_v3_v3v3(vel, state.co, psys->particles[p].prev_state.co);
/* Convert Z-up to Y-up. */
points.emplace_back(pos[0], pos[2], -pos[1]);
velocities.emplace_back(vel[0], vel[2], -vel[1]);
widths.push_back(psys->particles[p].size);
ids.push_back(index++);
}
psys_sim_data_free(&sim);
Alembic::Abc::P3fArraySample psample(points);
Alembic::Abc::UInt64ArraySample idsample(ids);
Alembic::Abc::V3fArraySample vsample(velocities);
Alembic::Abc::FloatArraySample wsample_array(widths);
Alembic::AbcGeom::OFloatGeomParam::Sample wsample(wsample_array, kVertexScope);
OPointsSchema::Sample sample(psample, idsample, vsample, wsample);
update_bounding_box(context.object);
sample.setSelfBounds(bounding_box_);
abc_points_schema_.set(sample);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include <Alembic/AbcGeom/OPoints.h>
namespace blender::io::alembic {
class ABCPointsWriter : public ABCAbstractWriter {
Alembic::AbcGeom::OPoints abc_points_;
Alembic::AbcGeom::OPointsSchema abc_points_schema_;
public:
explicit ABCPointsWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
Alembic::Abc::OObject get_alembic_object() const override;
Alembic::Abc::OCompoundProperty abc_prop_for_custom_props() override;
bool is_supported(const HierarchyContext *context) const override;
protected:
bool check_is_animated(const HierarchyContext &context) const override;
void do_write(HierarchyContext &context) override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_writer_transform.h"
#include "abc_hierarchy_iterator.h"
#include "intern/abc_axis_conversion.h"
#include "intern/abc_util.h"
#include "BKE_object.hh"
#include "BLI_math_euler_types.hh"
#include "BLI_math_matrix.hh"
#include "DNA_object_types.h"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
namespace io::alembic {
using Alembic::Abc::OObject;
using Alembic::AbcGeom::OXform;
using Alembic::AbcGeom::OXformSchema;
using Alembic::AbcGeom::XformSample;
ABCTransformWriter::ABCTransformWriter(const ABCWriterConstructorArgs &args)
: ABCAbstractWriter(args)
{
timesample_index_ = args_.abc_archive->time_sampling_index_transforms();
}
void ABCTransformWriter::create_alembic_objects(const HierarchyContext * /*context*/)
{
CLOG_DEBUG(&LOG, "exporting %s", args_.abc_path.c_str());
abc_xform_ = OXform(args_.abc_parent, args_.abc_name, timesample_index_);
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)
{
/* The object matrix relative to the parent. */
float4x4 parent_relative_matrix = context.parent_matrix_inv_world * context.matrix_world;
/* After this, parent_relative_matrix uses Y=up. */
copy_m44_axis_swap(parent_relative_matrix.ptr(), parent_relative_matrix.ptr(), ABC_YUP_FROM_ZUP);
/* If the parent is a camera, undo its to-Maya rotation (see below). */
bool is_root_object = context.export_parent == nullptr;
if (!is_root_object && context.export_parent->type == OB_CAMERA) {
float4x4 rot_mat = math::from_rotation<float4x4>(math::EulerXYZ(M_PI_2, 0.0f, 0.0f));
parent_relative_matrix = rot_mat * parent_relative_matrix;
}
/* If the object is a camera, apply an extra rotation to Maya camera orientation. */
if (context.object->type == OB_CAMERA) {
float4x4 rot_mat = math::from_rotation<float4x4>(math::EulerXYZ(-M_PI_2, 0.0f, 0.0f));
parent_relative_matrix = parent_relative_matrix * rot_mat;
}
/* Only apply scaling to root objects, parenting will propagate it. */
if (is_root_object) {
/* A float4 so we also scale translation */
const float4 scale(args_.export_params->global_scale);
parent_relative_matrix = math::scale(parent_relative_matrix, scale);
parent_relative_matrix[3][3] /=
args_.export_params->global_scale; /* Normalize the homogeneous component. */
}
XformSample xform_sample;
xform_sample.setMatrix(convert_matrix_datatype(parent_relative_matrix.ptr()));
xform_sample.setInheritsXforms(true);
abc_xform_schema_.set(xform_sample);
write_visibility(context);
}
OObject ABCTransformWriter::get_alembic_object() const
{
return abc_xform_;
}
bool ABCTransformWriter::check_is_animated(const HierarchyContext &context) const
{
if (context.duplicator != nullptr) {
/* This object is being duplicated, so could be emitted by a particle system and thus
* influenced by forces. TODO(Sybren): Make this more strict. Probably better to get from the
* depsgraph whether this object instance has a time source. */
return true;
}
if (check_has_physics(context)) {
return true;
}
return BKE_object_moves_in_time(context.object, context.animation_check_include_parent);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_writer_abstract.h"
#include <Alembic/AbcGeom/OXform.h>
namespace blender::io::alembic {
class ABCTransformWriter : public ABCAbstractWriter {
private:
Alembic::AbcGeom::OXform abc_xform_;
Alembic::AbcGeom::OXformSchema abc_xform_schema_;
public:
explicit ABCTransformWriter(const ABCWriterConstructorArgs &args);
void create_alembic_objects(const HierarchyContext *context) override;
protected:
void do_write(HierarchyContext &context) override;
bool check_is_animated(const HierarchyContext &context) const override;
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

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup Alembic
*/
#include "abc_axis_conversion.h"
#include "BLI_assert.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BKE_object_types.hh"
#include "DNA_object_types.h"
namespace blender::io::alembic {
void create_swapped_rotation_matrix(float rot_x_mat[3][3],
float rot_y_mat[3][3],
float rot_z_mat[3][3],
const float euler[3],
AbcAxisSwapMode mode)
{
const float rx = euler[0];
float ry;
float rz;
/* Apply transformation */
switch (mode) {
case ABC_ZUP_FROM_YUP:
ry = -euler[2];
rz = euler[1];
break;
case ABC_YUP_FROM_ZUP:
ry = euler[2];
rz = -euler[1];
break;
default:
ry = 0.0f;
rz = 0.0f;
BLI_assert(false);
break;
}
unit_m3(rot_x_mat);
unit_m3(rot_y_mat);
unit_m3(rot_z_mat);
rot_x_mat[1][1] = cos(rx);
rot_x_mat[2][1] = -sin(rx);
rot_x_mat[1][2] = sin(rx);
rot_x_mat[2][2] = cos(rx);
rot_y_mat[2][2] = cos(ry);
rot_y_mat[0][2] = -sin(ry);
rot_y_mat[2][0] = sin(ry);
rot_y_mat[0][0] = cos(ry);
rot_z_mat[0][0] = cos(rz);
rot_z_mat[1][0] = -sin(rz);
rot_z_mat[0][1] = sin(rz);
rot_z_mat[1][1] = cos(rz);
} // namespace
// alembicvoidcreate_swapped_rotation_matrix(floatrot_x_mat[3][3],floatrot_y_mat[3][3],floatrot_z_mat[3][3],constfloateuler[3],AbcAxisSwapModemode)
void copy_m44_axis_swap(float dst_mat[4][4], float src_mat[4][4], AbcAxisSwapMode mode)
{
float dst_rot[3][3], src_rot[3][3], dst_scale_mat[4][4];
float rot_x_mat[3][3], rot_y_mat[3][3], rot_z_mat[3][3];
float src_trans[3], dst_scale[3], src_scale[3], euler[3];
zero_v3(src_trans);
zero_v3(dst_scale);
zero_v3(src_scale);
zero_v3(euler);
unit_m3(src_rot);
unit_m3(dst_rot);
unit_m4(dst_scale_mat);
/* TODO(Sybren): This code assumes there is no sheer component and no
* homogeneous scaling component, which is not always true when writing
* non-hierarchical (e.g. flat) objects (e.g. when parent has non-uniform
* scale and the child rotates). This is currently not taken into account
* when axis-swapping. */
/* Extract translation, rotation, and scale form matrix. */
mat4_to_loc_rot_size(src_trans, src_rot, src_scale, src_mat);
/* Get euler angles from rotation matrix. */
mat3_to_eulO(euler, ROT_MODE_XZY, src_rot);
/* Create X, Y, Z rotation matrices from euler angles. */
create_swapped_rotation_matrix(rot_x_mat, rot_y_mat, rot_z_mat, euler, mode);
/* Concatenate rotation matrices. */
mul_m3_m3m3(dst_rot, dst_rot, rot_z_mat);
mul_m3_m3m3(dst_rot, dst_rot, rot_y_mat);
mul_m3_m3m3(dst_rot, dst_rot, rot_x_mat);
mat3_to_eulO(euler, ROT_MODE_XZY, dst_rot);
/* Start construction of dst_mat from rotation matrix */
unit_m4(dst_mat);
copy_m4_m3(dst_mat, dst_rot);
/* Apply translation */
switch (mode) {
case ABC_ZUP_FROM_YUP:
copy_zup_from_yup(dst_mat[3], src_trans);
break;
case ABC_YUP_FROM_ZUP:
copy_yup_from_zup(dst_mat[3], src_trans);
break;
default:
BLI_assert(false);
}
/* Apply scale matrix. Swaps y and z, but does not
* negate like translation does. */
dst_scale[0] = src_scale[0];
dst_scale[1] = src_scale[2];
dst_scale[2] = src_scale[1];
size_to_mat4(dst_scale_mat, dst_scale);
mul_m4_m4m4(dst_mat, dst_mat, dst_scale_mat);
}
void create_transform_matrix(Object *obj,
float r_yup_mat[4][4],
AbcMatrixMode mode,
Object *proxy_from)
{
float zup_mat[4][4];
/* get local or world matrix. */
if (mode == ABC_MATRIX_LOCAL && obj->parent) {
/* Note that this produces another matrix than the local matrix, due to
* constraints and modifiers as well as the obj->parentinv matrix. */
invert_m4_m4(obj->parent->runtime->world_to_object.ptr(),
obj->parent->object_to_world().ptr());
mul_m4_m4m4(zup_mat, obj->parent->world_to_object().ptr(), obj->object_to_world().ptr());
}
else {
copy_m4_m4(zup_mat, obj->object_to_world().ptr());
}
if (proxy_from) {
mul_m4_m4m4(zup_mat, proxy_from->object_to_world().ptr(), zup_mat);
}
copy_m44_axis_swap(r_yup_mat, zup_mat, ABC_YUP_FROM_ZUP);
}
} // namespace blender::io::alembic

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich & Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup Alembic
*/
#include "BLI_compiler_compat.h"
namespace blender {
struct Object;
namespace io::alembic {
/* TODO(kevin): for now keeping these transformations hardcoded to make sure
* everything works properly, and also because Alembic is almost exclusively
* used in Y-up software, but eventually they'll be set by the user in the UI
* like other importers/exporters do, to support other axis. */
/* Copy from Y-up to Z-up. */
BLI_INLINE void copy_zup_from_yup(float zup[3], const float yup[3])
{
const float old_yup1 = yup[1]; /* in case zup == yup */
zup[0] = yup[0];
zup[1] = -yup[2];
zup[2] = old_yup1;
}
BLI_INLINE void copy_zup_from_yup(short zup[3], const short yup[3])
{
const short old_yup1 = yup[1]; /* in case zup == yup */
zup[0] = yup[0];
zup[1] = -yup[2];
zup[2] = old_yup1;
}
/* Copy from Z-up to Y-up. */
BLI_INLINE void copy_yup_from_zup(float yup[3], const float zup[3])
{
const float old_zup1 = zup[1]; /* in case yup == zup */
yup[0] = zup[0];
yup[1] = zup[2];
yup[2] = -old_zup1;
}
BLI_INLINE void copy_yup_from_zup(short yup[3], const short zup[3])
{
const short old_zup1 = zup[1]; /* in case yup == zup */
yup[0] = zup[0];
yup[1] = zup[2];
yup[2] = -old_zup1;
}
/* Names are given in (dst, src) order, just like
* the parameters of copy_m44_axis_swap(). */
enum AbcAxisSwapMode {
ABC_ZUP_FROM_YUP = 1,
ABC_YUP_FROM_ZUP = 2,
};
/**
* Create a rotation matrix for each axis from euler angles.
* Euler angles are swapped to change coordinate system.
*/
void create_swapped_rotation_matrix(float rot_x_mat[3][3],
float rot_y_mat[3][3],
float rot_z_mat[3][3],
const float euler[3],
AbcAxisSwapMode mode);
/**
* Convert matrix from Z=up to Y=up or vice versa.
* Use yup_mat = zup_mat for in-place conversion.
*/
void copy_m44_axis_swap(float dst_mat[4][4], float src_mat[4][4], AbcAxisSwapMode mode);
enum AbcMatrixMode {
ABC_MATRIX_WORLD = 1,
ABC_MATRIX_LOCAL = 2,
};
/**
* Recompute transform matrix of object in new coordinate system
* (from Z-Up to Y-Up).
*/
void create_transform_matrix(Object *obj,
float r_yup_mat[4][4],
AbcMatrixMode mode,
Object *proxy_from);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,647 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_customdata.h"
#include "BLI_color_types.hh"
#include "abc_axis_conversion.h"
#include "abc_util.h"
#include <Alembic/Abc/ICompoundProperty.h>
#include <Alembic/Abc/ISampleSelector.h>
#include <Alembic/Abc/OCompoundProperty.h>
#include <Alembic/Abc/TypedArraySample.h>
#include <Alembic/AbcCoreAbstract/PropertyHeader.h>
#include <Alembic/AbcGeom/GeometryScope.h>
#include <Alembic/AbcGeom/IGeomParam.h>
#include <Alembic/AbcGeom/OGeomParam.h>
#include "DNA_customdata_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "BLI_math_base.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_utildefines.h"
#include "BKE_attribute.h"
#include "BKE_attribute.hh"
#include "BKE_customdata.hh"
#include "BKE_mesh.hh"
#include "IO_validate.hh"
namespace blender {
/* NOTE: for now only UVs and Vertex Colors are supported for streaming.
* Although Alembic only allows for a single UV layer per {I|O}Schema, and does
* not have a vertex color concept, there is a convention between DCCs to write
* such data in a way that lets other DCC know what they are for. See comments
* in the write code for the conventions. */
using Alembic::AbcGeom::kFacevaryingScope;
using Alembic::AbcGeom::kVaryingScope;
using Alembic::AbcGeom::kVertexScope;
using Alembic::Abc::C4fArraySample;
using Alembic::Abc::UInt32ArraySample;
using Alembic::Abc::V2fArraySample;
using Alembic::AbcGeom::OC4fGeomParam;
using Alembic::AbcGeom::OV2fGeomParam;
using Alembic::AbcGeom::OV3fGeomParam;
namespace io::alembic {
/* ORCO, Generated Coordinates, and Reference Points ("Pref") are all terms for the same thing.
* Other applications (Maya, Houdini) write these to a property called "Pref". */
static const std::string propNameOriginalCoordinates("Pref");
static void get_uvs(const CDStreamConfig &config,
std::vector<Imath::V2f> &uvs,
std::vector<uint32_t> &uvidx,
const Span<float2> uv_map_array)
{
const OffsetIndices faces = config.mesh->faces();
int *corner_verts = config.corner_verts;
if (!config.pack_uvs) {
int count = 0;
uvidx.resize(config.totloop);
uvs.resize(config.totloop);
/* Iterate in reverse order to match exported polygons. */
for (const int i : faces.index_range()) {
const IndexRange face = faces[i];
const float2 *loopuv = uv_map_array.data() + face.start() + face.size();
for (int j = 0; j < face.size(); j++, count++) {
loopuv--;
uvidx[count] = count;
uvs[count][0] = (*loopuv)[0];
uvs[count][1] = (*loopuv)[1];
}
}
}
else {
/* Mapping for indexed UVs, deduplicating UV coordinates at vertices. */
std::vector<std::vector<uint32_t>> idx_map(config.totvert);
int idx_count = 0;
for (const int i : faces.index_range()) {
const IndexRange face = faces[i];
int *face_verts = corner_verts + face.start() + face.size();
const float2 *loopuv = uv_map_array.data() + face.start() + face.size();
for (int j = 0; j < face.size(); j++) {
face_verts--;
loopuv--;
Imath::V2f uv((*loopuv)[0], (*loopuv)[1]);
bool found_same = false;
/* Find UV already in uvs array. */
for (uint32_t uv_idx : idx_map[*face_verts]) {
if (uvs[uv_idx] == uv) {
found_same = true;
uvidx.push_back(uv_idx);
break;
}
}
/* UV doesn't exists for this vertex, add it. */
if (!found_same) {
uint32_t uv_idx = idx_count++;
idx_map[*face_verts].push_back(uv_idx);
uvidx.push_back(uv_idx);
uvs.push_back(uv);
}
}
}
}
}
const char *get_uv_sample(UVSample &sample, const CDStreamConfig &config, const Mesh &mesh)
{
const StringRefNull name = mesh.active_uv_map_name();
if (name.is_empty()) {
return "";
}
const VArraySpan uv_map = *mesh.attributes().lookup<float2>(name, bke::AttrDomain::Corner);
if (uv_map.is_empty()) {
return "";
}
get_uvs(config, sample.uvs, sample.indices, uv_map);
return name.c_str();
}
/* Convention to write UVs:
* - V2fGeomParam on the arbGeomParam
* - set scope as face varying
* - (optional due to its behavior) tag as UV using Alembic::AbcGeom::SetIsUV
*/
static void write_uv(const OCompoundProperty &prop,
CDStreamConfig &config,
const Span<float2> data,
const std::string &uv_map_name)
{
std::vector<uint32_t> indices;
std::vector<Imath::V2f> uvs;
get_uvs(config, uvs, indices, data);
if (indices.empty() || uvs.empty()) {
return;
}
OV2fGeomParam param = config.abc_uv_maps[uv_map_name];
if (!param.valid()) {
param = OV2fGeomParam(prop, uv_map_name, true, kFacevaryingScope, 1);
}
OV2fGeomParam::Sample sample(V2fArraySample(&uvs.front(), uvs.size()),
UInt32ArraySample(&indices.front(), indices.size()),
kFacevaryingScope);
param.set(sample);
param.setTimeSampling(config.timesample_index);
config.abc_uv_maps[uv_map_name] = param;
}
static void get_cols(const CDStreamConfig &config,
std::vector<Imath::C4f> &buffer,
std::vector<uint32_t> &uvidx,
const void *cd_data)
{
const float cscale = 1.0f / 255.0f;
const OffsetIndices faces = config.mesh->faces();
const MCol *cfaces = static_cast<const MCol *>(cd_data);
buffer.reserve(config.totvert);
uvidx.reserve(config.totvert);
Imath::C4f col;
for (const int i : faces.index_range()) {
const IndexRange face = faces[i];
const MCol *cface = &cfaces[face.start() + face.size()];
for (int j = 0; j < face.size(); j++) {
cface--;
col[0] = cface->a * cscale;
col[1] = cface->r * cscale;
col[2] = cface->g * cscale;
col[3] = cface->b * cscale;
buffer.push_back(col);
uvidx.push_back(buffer.size() - 1);
}
}
}
/* Convention to write Vertex Colors:
* - C3fGeomParam/C4fGeomParam on the arbGeomParam
* - set scope as vertex varying
*/
static void write_mcol(const OCompoundProperty &prop,
CDStreamConfig &config,
const void *data,
const std::string &vcol_name)
{
std::vector<uint32_t> indices;
std::vector<Imath::C4f> buffer;
get_cols(config, buffer, indices, data);
if (indices.empty() || buffer.empty()) {
return;
}
OC4fGeomParam param = config.abc_vertex_colors[vcol_name];
if (!param.valid()) {
param = OC4fGeomParam(prop, vcol_name, true, kFacevaryingScope, 1);
}
OC4fGeomParam::Sample sample(C4fArraySample(&buffer.front(), buffer.size()),
UInt32ArraySample(&indices.front(), indices.size()),
kVertexScope);
param.set(sample);
param.setTimeSampling(config.timesample_index);
config.abc_vertex_colors[vcol_name] = param;
}
void write_generated_coordinates(const OCompoundProperty &prop, CDStreamConfig &config)
{
Mesh *mesh = config.mesh;
const void *customdata = CustomData_get_layer(&mesh->vert_data, CD_ORCO);
if (customdata == nullptr) {
/* Data not available, so don't even bother creating an Alembic property for it. */
return;
}
const float (*orcodata)[3] = static_cast<const float (*)[3]>(customdata);
/* Convert 3D vertices from float[3] z=up to V3f y=up. */
std::vector<Imath::V3f> coords(config.totvert);
float orco_yup[3];
for (int vertex_idx = 0; vertex_idx < config.totvert; vertex_idx++) {
copy_yup_from_zup(orco_yup, orcodata[vertex_idx]);
coords[vertex_idx].setValue(orco_yup[0], orco_yup[1], orco_yup[2]);
}
/* ORCOs are always stored in the normalized 0..1 range in Blender, but Alembic stores them
* unnormalized, so we need to unnormalize (invert transform) them. */
BKE_mesh_orco_verts_transform(
mesh, reinterpret_cast<float (*)[3]>(coords.data()), mesh->verts_num, true);
if (!config.abc_orco.valid()) {
/* Create the Alembic property and keep a reference so future frames can reuse it. */
config.abc_orco = OV3fGeomParam(prop, propNameOriginalCoordinates, false, kVertexScope, 1);
}
OV3fGeomParam::Sample sample(coords, kVertexScope);
config.abc_orco.set(sample);
}
void write_custom_data(const OCompoundProperty &prop,
CDStreamConfig &config,
const Mesh &mesh,
int data_type)
{
const bke::AttributeAccessor attributes = mesh.attributes();
if (data_type == CD_PROP_FLOAT2) {
const StringRef active_uv_name = mesh.active_uv_map_name();
for (const StringRefNull name : mesh.uv_map_names()) {
if (name == active_uv_name) {
/* Already exported. */
continue;
}
const VArraySpan uv_map = *attributes.lookup<float2>(name, bke::AttrDomain::Corner);
write_uv(prop, config, uv_map, get_valid_abc_name(name.c_str()));
}
}
else if (data_type == CD_PROP_BYTE_COLOR) {
mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.data_type != bke::AttrType::ColorByte) {
return;
}
if (iter.domain != bke::AttrDomain::Corner) {
return;
}
const VArraySpan attr = *attributes.lookup<ColorGeometry4b>(iter.name,
bke::AttrDomain::Corner);
write_mcol(prop, config, attr.data(), get_valid_abc_name(iter.name.c_str()));
});
}
}
/* ************************************************************************** */
using Alembic::Abc::C3fArraySamplePtr;
using Alembic::Abc::C4fArraySamplePtr;
using Alembic::Abc::PropertyHeader;
using Alembic::Abc::UInt32ArraySamplePtr;
using Alembic::AbcGeom::IC3fGeomParam;
using Alembic::AbcGeom::IC4fGeomParam;
using Alembic::AbcGeom::IV2fGeomParam;
using Alembic::AbcGeom::IV3fGeomParam;
static void read_uvs(const CDStreamConfig &config,
MutableSpan<float2> uv_map,
const AbcUvScope uv_scope,
const Alembic::AbcGeom::V2fArraySamplePtr &uvs,
const UInt32ArraySamplePtr &indices)
{
const OffsetIndices faces = config.mesh->faces();
const int *corner_verts = config.corner_verts;
const int64_t indices_size = int64_t(indices->size());
const int64_t uvs_size = int64_t(uvs->size());
BLI_assert(uv_scope != ABC_UV_SCOPE_NONE);
const bool do_uvs_per_loop = (uv_scope == ABC_UV_SCOPE_LOOP);
for (const int64_t i : faces.index_range()) {
const IndexRange face = faces[i];
const int64_t rev_loop_offset = face.start() + face.size() - 1;
for (int64_t f = 0; f < face.size(); f++) {
const int64_t rev_loop_index = rev_loop_offset - f;
const int64_t loop_index = do_uvs_per_loop ? face.start() + f : corner_verts[rev_loop_index];
if (!validate::index_in_range(loop_index, indices_size)) {
continue;
}
const int64_t uv_index = (*indices)[loop_index];
if (!validate::index_in_range(uv_index, uvs_size)) {
continue;
}
const Imath::V2f &uv = (*uvs)[uv_index];
float2 &loopuv = uv_map[rev_loop_index];
loopuv[0] = uv[0];
loopuv[1] = uv[1];
}
}
}
static int64_t mcols_out_of_bounds_check(const int64_t color_index,
const int64_t array_size,
const std::string &iobject_full_name,
const PropertyHeader &prop_header,
bool &r_is_out_of_bounds,
bool &r_bounds_warning_given)
{
if (validate::index_in_range(color_index, array_size)) {
return color_index;
}
if (!r_bounds_warning_given) {
std::cerr << "Alembic: color index out of bounds "
"reading face colors for object "
<< iobject_full_name << ", property " << prop_header.getName() << std::endl;
r_bounds_warning_given = true;
}
r_is_out_of_bounds = true;
return 0;
}
static void read_custom_data_mcols(const std::string &iobject_full_name,
const ICompoundProperty &arbGeomParams,
const PropertyHeader &prop_header,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss)
{
C3fArraySamplePtr c3f_ptr = C3fArraySamplePtr();
C4fArraySamplePtr c4f_ptr = C4fArraySamplePtr();
Alembic::Abc::UInt32ArraySamplePtr indices;
bool use_c3f_ptr;
bool is_facevarying;
/* Find the correct interpretation of the data */
if (IC3fGeomParam::matches(prop_header)) {
IC3fGeomParam color_param(arbGeomParams, prop_header.getName());
IC3fGeomParam::Sample sample;
BLI_assert(STREQ("rgb", color_param.getInterpretation()));
color_param.getIndexed(sample, iss);
is_facevarying = sample.getScope() == kFacevaryingScope &&
config.totloop == sample.getIndices()->size();
c3f_ptr = sample.getVals();
indices = sample.getIndices();
use_c3f_ptr = true;
}
else if (IC4fGeomParam::matches(prop_header)) {
IC4fGeomParam color_param(arbGeomParams, prop_header.getName());
IC4fGeomParam::Sample sample;
BLI_assert(STREQ("rgba", color_param.getInterpretation()));
color_param.getIndexed(sample, iss);
is_facevarying = sample.getScope() == kFacevaryingScope &&
config.totloop == sample.getIndices()->size();
c4f_ptr = sample.getVals();
indices = sample.getIndices();
use_c3f_ptr = false;
}
else {
/* this won't happen due to the checks in read_custom_data() */
return;
}
BLI_assert(c3f_ptr || c4f_ptr);
/* Read the vertex colors */
bke::MutableAttributeAccessor attributes = config.mesh->attributes_for_write();
bke::SpanAttributeWriter attr = attributes.lookup_or_add_for_write_span<ColorGeometry4b>(
prop_header.getName(), bke::AttrDomain::Corner);
const OffsetIndices faces = config.mesh->faces();
const int *corner_verts = config.corner_verts;
int64_t face_index = 0;
int64_t color_index;
bool bounds_warning_given = false;
/* The colors can go through two layers of indexing. Often the 'indices'
* array doesn't do anything (i.e. indices[n] = n), but when it does, it's
* important. Blender 2.79 writes indices incorrectly (see #53745), which
* is why we have to check for indices->size() > 0 */
bool use_dual_indexing = is_facevarying && indices->size() > 0;
for (const int64_t i : faces.index_range()) {
const IndexRange face = faces[i];
int64_t corner = face.start() + face.size();
for (int64_t j = 0; j < face.size(); j++, face_index++) {
corner--;
color_index = is_facevarying ? face_index : corner_verts[corner];
if (use_dual_indexing) {
color_index = (*indices)[color_index];
}
if (use_c3f_ptr) {
bool is_mcols_out_of_bounds = false;
color_index = mcols_out_of_bounds_check(color_index,
c3f_ptr->size(),
iobject_full_name,
prop_header,
is_mcols_out_of_bounds,
bounds_warning_given);
if (is_mcols_out_of_bounds) {
continue;
}
const Imath::C3f &color = (*c3f_ptr)[color_index];
attr.span[corner].r = unit_float_to_uchar_clamp(color[0]);
attr.span[corner].g = unit_float_to_uchar_clamp(color[1]);
attr.span[corner].b = unit_float_to_uchar_clamp(color[2]);
attr.span[corner].a = 255;
}
else {
bool is_mcols_out_of_bounds = false;
color_index = mcols_out_of_bounds_check(color_index,
c4f_ptr->size(),
iobject_full_name,
prop_header,
is_mcols_out_of_bounds,
bounds_warning_given);
if (is_mcols_out_of_bounds) {
continue;
}
const Imath::C4f &color = (*c4f_ptr)[color_index];
attr.span[corner].r = unit_float_to_uchar_clamp(color[0]);
attr.span[corner].g = unit_float_to_uchar_clamp(color[1]);
attr.span[corner].b = unit_float_to_uchar_clamp(color[2]);
attr.span[corner].a = unit_float_to_uchar_clamp(color[3]);
}
}
}
attr.finish();
}
static void read_custom_data_uvs(const ICompoundProperty &prop,
const PropertyHeader &prop_header,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss)
{
IV2fGeomParam uv_param(prop, prop_header.getName());
if (!uv_param.isIndexed()) {
return;
}
IV2fGeomParam::Sample sample;
uv_param.getIndexed(sample, iss);
UInt32ArraySamplePtr uvs_indices = sample.getIndices();
const AbcUvScope uv_scope = get_uv_scope(uv_param.getScope(), config, uvs_indices);
if (uv_scope == ABC_UV_SCOPE_NONE) {
return;
}
bke::MutableAttributeAccessor attributes = config.mesh->attributes_for_write();
bke::SpanAttributeWriter uv_map = attributes.lookup_or_add_for_write_span<float2>(
prop_header.getName(), bke::AttrDomain::Corner);
read_uvs(config, uv_map.span, uv_scope, sample.getVals(), uvs_indices);
uv_map.finish();
}
void read_velocity(const V3fArraySamplePtr &velocities,
const CDStreamConfig &config,
const float velocity_scale)
{
if (velocities->size() != config.mesh->verts_num) {
/* Files containing videogrammetry data may be malformed and export velocity data on missing
* frames (most likely by copying the last valid data). */
return;
}
const int64_t num_velocity_vectors = config.mesh->verts_num;
bke::MutableAttributeAccessor attributes = config.mesh->attributes_for_write();
bke::SpanAttributeWriter attr = attributes.lookup_or_add_for_write_span<float3>(
"velocity", bke::AttrDomain::Point);
MutableSpan<float3> velocity = attr.span;
for (int64_t i = 0; i < num_velocity_vectors; i++) {
const Imath::V3f &vel_in = (*velocities)[i];
copy_zup_from_yup(velocity[i], vel_in.getValue());
mul_v3_fl(velocity[i], velocity_scale);
}
attr.finish();
}
void read_generated_coordinates(const ICompoundProperty &prop,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss)
{
if (!prop.valid() || prop.getPropertyHeader(propNameOriginalCoordinates) == nullptr) {
/* The ORCO property isn't there, so don't bother trying to process it. */
return;
}
IV3fGeomParam param(prop, propNameOriginalCoordinates);
if (!param.valid() || param.isIndexed()) {
/* Invalid or indexed coordinates aren't supported. */
return;
}
if (param.getScope() != kVertexScope) {
/* These are original vertex coordinates, so must be vertex-scoped. */
return;
}
IV3fGeomParam::Sample sample = param.getExpandedValue(iss);
Alembic::AbcGeom::V3fArraySamplePtr abc_orco = sample.getVals();
const size_t totvert = abc_orco.get()->size();
Mesh *mesh = config.mesh;
if (totvert != mesh->verts_num) {
/* Either the data is somehow corrupted, or we have a dynamic simulation where only the ORCOs
* for the first frame were exported. */
return;
}
void *cd_data;
if (CustomData_has_layer(&mesh->vert_data, CD_ORCO)) {
cd_data = CustomData_get_layer_for_write(&mesh->vert_data, CD_ORCO, mesh->verts_num);
}
else {
cd_data = CustomData_add_layer(&mesh->vert_data, CD_ORCO, CD_CONSTRUCT, totvert);
}
float (*orcodata)[3] = static_cast<float (*)[3]>(cd_data);
for (int vertex_idx = 0; vertex_idx < totvert; ++vertex_idx) {
const Imath::V3f &abc_coords = (*abc_orco)[vertex_idx];
copy_zup_from_yup(orcodata[vertex_idx], abc_coords.getValue());
}
/* ORCOs are always stored in the normalized 0..1 range in Blender, but Alembic stores them
* unnormalized, so we need to normalize them. */
BKE_mesh_orco_verts_transform(mesh, orcodata, mesh->verts_num, false);
}
void read_custom_data(const std::string &iobject_full_name,
const ICompoundProperty &prop,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss)
{
if (!prop.valid()) {
return;
}
int num_uvs = 0;
const size_t num_props = prop.getNumProperties();
for (size_t i = 0; i < num_props; i++) {
const Alembic::Abc::PropertyHeader &prop_header = prop.getPropertyHeader(i);
/* Read UVs according to convention. */
if (IV2fGeomParam::matches(prop_header) && Alembic::AbcGeom::isUV(prop_header)) {
if (++num_uvs > MAX_MTFACE) {
continue;
}
read_custom_data_uvs(prop, prop_header, config, iss);
continue;
}
/* Read vertex colors according to convention. */
if (IC3fGeomParam::matches(prop_header) || IC4fGeomParam::matches(prop_header)) {
read_custom_data_mcols(iobject_full_name, prop, prop_header, config, iss);
continue;
}
}
}
AbcUvScope get_uv_scope(const Alembic::AbcGeom::GeometryScope scope,
const CDStreamConfig &config,
const Alembic::AbcGeom::UInt32ArraySamplePtr &indices)
{
if (scope == kFacevaryingScope && indices->size() == config.totloop) {
return ABC_UV_SCOPE_LOOP;
}
/* kVaryingScope is sometimes used for vertex scopes as the values vary across the vertices. To
* be sure, one has to check the size of the data against the number of vertices, as it could
* also be a varying attribute across the faces (i.e. one value per face). */
if (ELEM(scope, kVaryingScope, kVertexScope) && indices->size() == config.totvert) {
return ABC_UV_SCOPE_VERTEX;
}
return ABC_UV_SCOPE_NONE;
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,133 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "BLI_math_vector_types.hh"
#include <Alembic/Abc/ICompoundProperty.h>
#include <Alembic/Abc/ISampleSelector.h>
#include <Alembic/Abc/OCompoundProperty.h>
#include <Alembic/Abc/TypedArraySample.h>
#include <Alembic/AbcCoreAbstract/Foundation.h>
#include <Alembic/AbcGeom/GeometryScope.h>
#include <Alembic/AbcGeom/OGeomParam.h>
#include "BKE_attribute.hh"
#include <cstdint>
#include <map>
#include <string>
#include <vector>
namespace blender {
struct CustomData;
struct Mesh;
using Alembic::Abc::ICompoundProperty;
using Alembic::Abc::OCompoundProperty;
using Alembic::Abc::UInt32ArraySamplePtr;
using Alembic::Abc::V2fArraySamplePtr;
using Alembic::Abc::V3fArraySamplePtr;
namespace io::alembic {
struct UVSample {
std::vector<Imath::V2f> uvs;
std::vector<uint32_t> indices;
};
enum AbcUvScope {
ABC_UV_SCOPE_NONE,
ABC_UV_SCOPE_LOOP,
ABC_UV_SCOPE_VERTEX,
};
struct CDStreamConfig {
int *corner_verts = nullptr;
int totloop = 0;
int *face_offsets = nullptr;
int faces_num = 0;
float3 *positions = nullptr;
int totvert = 0;
bke::SpanAttributeWriter<float2> uv_map;
bool pack_uvs = false;
/* TODO(kevin): might need a better way to handle adding and/or updating
* custom data such that it updates the custom data holder and its pointers properly. */
Mesh *mesh = nullptr;
Alembic::Abc::chrono_t time = 0.0;
int timesample_index = 0;
const char **modifier_error_message = nullptr;
/* Alembic needs Blender to keep references to C++ objects (the destructors finalize the writing
* to ABC). The following fields are all used to keep these references. */
/* Mapping from UV map name to its ABC property, for the 2nd and subsequent UV maps; the primary
* UV map is kept alive by the Alembic mesh sample itself. */
std::map<std::string, Alembic::AbcGeom::OV2fGeomParam> abc_uv_maps;
/* ORCO coordinates, aka Generated Coordinates. */
Alembic::AbcGeom::OV3fGeomParam abc_orco;
/* Mapping from vertex color layer name to its Alembic color data. */
std::map<std::string, Alembic::AbcGeom::OC4fGeomParam> abc_vertex_colors;
AbcUvScope uv_scope;
V2fArraySamplePtr uvs;
UInt32ArraySamplePtr uvs_indices;
CDStreamConfig() = default;
};
/* Get the UVs for the main UV property on a OSchema.
* Returns the name of the UV layer.
*
* For now the active layer is used, maybe needs a better way to choose this. */
const char *get_uv_sample(UVSample &sample, const CDStreamConfig &config, const Mesh &mesh);
void write_generated_coordinates(const OCompoundProperty &prop, CDStreamConfig &config);
void read_velocity(const V3fArraySamplePtr &velocities,
const CDStreamConfig &config,
const float velocity_scale);
void read_generated_coordinates(const ICompoundProperty &prop,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss);
void write_custom_data(const OCompoundProperty &prop,
CDStreamConfig &config,
const Mesh &mesh,
int data_type);
void read_custom_data(const std::string &iobject_full_name,
const ICompoundProperty &prop,
const CDStreamConfig &config,
const Alembic::Abc::ISampleSelector &iss);
/**
* UVs can be defined per-loop (one value per vertex per face), or per-vertex (one value per
* vertex). The first case is the most common, as this is the standard way of storing this data
* given that some vertices might be on UV seams and have multiple possible UV coordinates; the
* second case can happen when the mesh is split according to the UV islands, in which case storing
* a single UV value per vertex allows to de-duplicate data and thus to reduce the file size since
* vertices are guaranteed to only have a single UV coordinate.
*/
AbcUvScope get_uv_scope(const Alembic::AbcGeom::GeometryScope scope,
const CDStreamConfig &config,
const Alembic::AbcGeom::UInt32ArraySamplePtr &indices);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,111 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "abc_keyframing.h"
#include "abc_reader_archive.h"
#include "DNA_scene_types.h"
#include "ANIM_action.hh"
#include "ANIM_animdata.hh"
#include "BKE_fcurve.hh"
using Alembic::Abc::ISampleSelector;
namespace blender {
namespace io::alembic {
/* Utility: create new fcurve and add it as a channel to a group. */
static FCurve *create_fcurve(animrig::Channelbag &channelbag,
const animrig::FCurveDescriptor &fcurve_descriptor,
const int sample_count)
{
FCurve *fcurve = channelbag.fcurve_create_unique(nullptr, fcurve_descriptor);
BLI_assert_msg(fcurve, "The same F-Curve is being created twice, this is unexpected.");
if (fcurve) {
BKE_fcurve_bezt_resize(*fcurve, sample_count);
}
return fcurve;
}
/* Utility: fill in a single fcurve sample at the provided index. */
void set_fcurve_sample(FCurve *fcu, int64_t sample_index, const float frame, const float value)
{
BLI_assert(sample_index >= 0 && sample_index < fcu->totvert);
BezTriple &bez = fcu->bezt[sample_index];
bez.vec[1][0] = frame;
bez.vec[1][1] = value;
bez.ipo = BEZT_IPO_LIN;
bez.f1 = bez.f2 = bez.f3 = BEZT_FLAG_SELECT;
bez.h1 = bez.h2 = HD_AUTO;
}
FCurveCreationHelper::~FCurveCreationHelper() = default;
void FCurveCreationHelper::ensure_action_data(Main *bmain, const int sample_count)
{
action_ = animrig::id_action_ensure(bmain, id_);
channelbag = &animrig::action_channelbag_ensure(*action_, *id_);
create_fcurves(sample_count);
}
FCurve *FCurveCreationHelper::create_fcurve(const animrig::FCurveDescriptor &fcurve_descriptor,
const int sample_count)
{
return alembic::create_fcurve(*channelbag, fcurve_descriptor, sample_count);
}
void FCurveCreationHelper::finish()
{
remove_unnecessary_fcurves();
for (FCurve *fcu : channelbag->fcurves()) {
if (fcu) {
BKE_fcurve_handles_recalc(*fcu);
}
}
}
void create_keyframes(Main *bmain,
Scene *scene,
Span<std::unique_ptr<FCurveCreationHelper>> helpers,
const TimeInfo time_info)
{
if (helpers.is_empty()) {
return;
}
const double fps = scene->frames_per_second();
const int start_frame = int(round(time_info.min_time * fps));
const int end_frame = int(round(time_info.max_time * fps));
const int sample_count = end_frame - start_frame + 1;
for (const std::unique_ptr<FCurveCreationHelper> &helper : helpers) {
helper->ensure_action_data(bmain, sample_count);
}
int64_t sample_index = 0;
for (int i = start_frame; i <= end_frame; i++) {
const double frame_time = (double(i) / fps);
const ISampleSelector selector = ISampleSelector(frame_time);
FrameSampleInfo sample_info;
sample_info.frame = float(i);
sample_info.sample_index = sample_index++;
sample_info.selector = selector;
for (const std::unique_ptr<FCurveCreationHelper> &helper : helpers) {
helper->set_fcurves_sample(sample_info);
}
}
for (const std::unique_ptr<FCurveCreationHelper> &helper : helpers) {
helper->finish();
}
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,82 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <functional>
#include <Alembic/Abc/ISampleSelector.h>
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "RNA_path.hh"
namespace blender {
namespace animrig {
class Channelbag;
struct FCurveDescriptor;
} // namespace animrig
struct bAction;
struct FCurve;
struct ID;
struct Main;
struct Scene;
namespace io::alembic {
struct TimeInfo;
struct FrameSampleInfo {
Alembic::Abc::ISampleSelector selector{};
int64_t sample_index = 0;
float frame = 0.0f;
};
/* Base class for creating FCurves and setting their samples for each frame.
* The actual FCurve creation is delegated to derived classes. */
class FCurveCreationHelper {
protected:
ID *id_ = nullptr;
bAction *action_ = nullptr;
animrig::Channelbag *channelbag = nullptr;
public:
FCurveCreationHelper(ID *id) : id_(id) {}
virtual ~FCurveCreationHelper();
void ensure_action_data(Main *bmain, const int sample_count);
/* Called every frame. Derived classes should set the sample for every FCurve that they have
* created. */
virtual void set_fcurves_sample(const FrameSampleInfo &sample_info) = 0;
void finish();
protected:
FCurve *create_fcurve(const animrig::FCurveDescriptor &fcurve_descriptor,
const int sample_count);
/* This is where derived classes should create FCurves for every property that they want to see
* key-framed. FCurves should be created using the #create_fcurve method above. */
virtual void create_fcurves(const int sample_count) = 0;
/* Derived classes can implement this to remove any FCurve for any property which was not
* actually animated. */
virtual void remove_unnecessary_fcurves() {}
};
/* Create keyframes for the entire range of the supplied #TimeInfo. */
void create_keyframes(Main *bmain,
Scene *scene,
Span<std::unique_ptr<FCurveCreationHelper>> helpers,
const TimeInfo time_info);
void set_fcurve_sample(FCurve *fcu, int64_t sample_index, const float frame, const float value);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,174 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_archive.h"
#include "Alembic/Abc/ArchiveInfo.h"
#include "Alembic/AbcCoreAbstract/MetaData.h"
#include "Alembic/AbcCoreLayer/Read.h"
#include "Alembic/AbcCoreOgawa/ReadWrite.h"
#include "BKE_main.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#ifdef WIN32
# include "utfconv.hh"
#endif
#include <fstream>
#include <vector>
namespace blender {
using Alembic::Abc::chrono_t;
using Alembic::Abc::ErrorHandler;
using Alembic::Abc::Exception;
using Alembic::Abc::IArchive;
using Alembic::Abc::kWrapExisting;
using Alembic::Abc::MetaData;
namespace io::alembic {
static IArchive open_archive(const std::string &filename,
const std::vector<std::istream *> &input_streams)
{
try {
Alembic::AbcCoreOgawa::ReadArchive archive_reader(input_streams);
return IArchive(archive_reader(filename), kWrapExisting, ErrorHandler::kThrowPolicy);
}
catch (const Exception &e) {
std::cerr << e.what() << '\n';
/* Inspect the file to see whether it's actually a HDF5 file. */
char header[4]; /* char(0x89) + "HDF" */
std::ifstream the_file(filename.c_str(), std::ios::in | std::ios::binary);
if (!the_file) {
std::cerr << "Unable to open " << filename << std::endl;
}
else if (!the_file.read(header, sizeof(header))) {
std::cerr << "Unable to read from " << filename << std::endl;
}
else if (strncmp(header + 1, "HDF", 3) != 0) {
std::cerr << filename << " has an unknown file format, unable to read." << std::endl;
}
else {
std::cerr << filename << " is in the obsolete HDF5 format, unable to read." << std::endl;
}
if (the_file.is_open()) {
the_file.close();
}
}
return IArchive();
}
ArchiveReader *ArchiveReader::get(const Main *bmain, const std::vector<const char *> &filenames)
{
std::vector<ArchiveReader *> readers;
for (const char *filename : filenames) {
ArchiveReader *reader = new ArchiveReader(bmain, filename);
if (!reader->valid()) {
delete reader;
continue;
}
readers.push_back(reader);
}
if (readers.empty()) {
return nullptr;
}
if (readers.size() == 1) {
return readers[0];
}
return new ArchiveReader(readers);
}
ArchiveReader::ArchiveReader(const std::vector<ArchiveReader *> &readers) : m_readers(readers)
{
Alembic::AbcCoreLayer::ArchiveReaderPtrs archives;
for (ArchiveReader *reader : readers) {
archives.push_back(reader->m_archive.getPtr());
}
Alembic::AbcCoreLayer::ReadArchive layer;
Alembic::AbcCoreAbstract::ArchiveReaderPtr arPtr = layer(archives);
m_archive = IArchive(arPtr, kWrapExisting, ErrorHandler::kThrowPolicy);
}
ArchiveReader::ArchiveReader(const Main *bmain, const char *filename)
{
char abs_filepath[FILE_MAX];
STRNCPY(abs_filepath, filename);
BLI_path_abs(abs_filepath, BKE_main_blendfile_path(bmain));
#ifdef WIN32
UTF16_ENCODE(abs_filepath);
std::wstring wstr(abs_filepath_16);
m_infile.open(wstr.c_str(), std::ios::in | std::ios::binary);
UTF16_UN_ENCODE(abs_filepath);
#else
m_infile.open(abs_filepath, std::ios::in | std::ios::binary);
#endif
m_streams.push_back(&m_infile);
m_archive = open_archive(abs_filepath, m_streams);
}
ArchiveReader::~ArchiveReader()
{
for (ArchiveReader *reader : m_readers) {
delete reader;
}
}
bool ArchiveReader::valid() const
{
return m_archive.valid();
}
Alembic::Abc::IObject ArchiveReader::getTop()
{
return m_archive.getTop();
}
bool ArchiveReader::is_blender_archive_version_prior_44()
{
const MetaData &abc_metadata = m_archive.getPtr()->getMetaData();
/* Was the incoming Archive written by Blender? If so, make the version check. */
if (abc_metadata.get(Alembic::Abc::kApplicationNameKey) == "Blender") {
return abc_metadata.get("blender_version") < "v4.4";
}
return false;
}
TimeInfo ArchiveReader::getTimeInfo()
{
chrono_t min_time = std::numeric_limits<chrono_t>::max();
chrono_t max_time = -std::numeric_limits<chrono_t>::max();
Alembic::Abc::GetArchiveStartAndEndTime(m_archive, min_time, max_time);
return {min_time, max_time};
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,68 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include <Alembic/Abc/IArchive.h>
#include <Alembic/Abc/IObject.h>
#include <fstream>
#include <vector>
namespace blender {
struct Main;
namespace io::alembic {
/* Represents the time range in seconds for animated data inside of an Alembic archive. The time
* range is [min, max]. */
struct TimeInfo {
Alembic::Abc::chrono_t min_time = std::numeric_limits<Alembic::Abc::chrono_t>::max();
Alembic::Abc::chrono_t max_time = -std::numeric_limits<Alembic::Abc::chrono_t>::max();
bool is_valid() const
{
return min_time <= max_time &&
min_time != std::numeric_limits<Alembic::Abc::chrono_t>::max() &&
max_time != -std::numeric_limits<Alembic::Abc::chrono_t>::max();
}
};
/**
* Wrappers around input and output archives. The goal is to be able to use
* streams so that unicode paths work on Windows (#49112), and to make sure that
* the stream objects remain valid as long as the archives are open.
*/
class ArchiveReader {
Alembic::Abc::IArchive m_archive;
std::ifstream m_infile;
std::vector<std::istream *> m_streams;
std::vector<ArchiveReader *> m_readers;
ArchiveReader(const std::vector<ArchiveReader *> &readers);
ArchiveReader(const struct Main *bmain, const char *filename);
public:
static ArchiveReader *get(const struct Main *bmain, const std::vector<const char *> &filenames);
~ArchiveReader();
bool valid() const;
Alembic::Abc::IObject getTop();
/* Detect if the Archive was written by Blender prior to 4.4. */
bool is_blender_archive_version_prior_44();
TimeInfo getTimeInfo();
};
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,202 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_camera.h"
#include "abc_keyframing.h"
#include "abc_util.h"
/* Silence warnings from copying deprecated fields. */
#define DNA_DEPRECATED_ALLOW
#include "DNA_camera_types.h"
#include "DNA_object_types.h"
#include "ANIM_action.hh"
#include "ANIM_fcurve.hh"
#include "BLI_math_base.h"
#include "BKE_camera.h"
#include "BKE_object.hh"
#include "BLT_translation.hh"
namespace blender {
using Alembic::AbcGeom::CameraSample;
using Alembic::AbcGeom::ICamera;
using Alembic::AbcGeom::ICompoundProperty;
using Alembic::AbcGeom::IFloatProperty;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::kWrapExisting;
namespace io::alembic {
AbcCameraReader::AbcCameraReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
ICamera abc_cam(m_iobject, kWrapExisting);
m_schema = abc_cam.getSchema();
}
bool AbcCameraReader::valid() const
{
return m_schema.valid();
}
bool AbcCameraReader::accepts_object_type(
const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const
{
if (!Alembic::AbcGeom::ICamera::matches(alembic_header)) {
*r_err_str = RPT_(
"Object type mismatch, Alembic object path pointed to Camera when importing, but not any "
"more");
return false;
}
if (ob->type != OB_CAMERA) {
*r_err_str = RPT_("Object type mismatch, Alembic object path points to Camera");
return false;
}
return true;
}
static void read_camera_sample(Camera *bcam,
const ICamera::schema_type &schema,
const ISampleSelector &sample_sel)
{
CameraSample cam_sample;
schema.get(cam_sample, sample_sel);
ICompoundProperty customDataContainer = schema.getUserProperties();
if (customDataContainer.valid() && customDataContainer.getPropertyHeader("stereoDistance") &&
customDataContainer.getPropertyHeader("eyeSeparation"))
{
IFloatProperty convergence_plane(customDataContainer, "stereoDistance");
IFloatProperty eye_separation(customDataContainer, "eyeSeparation");
bcam->stereo.interocular_distance = eye_separation.getValue(sample_sel);
bcam->stereo.convergence_distance = convergence_plane.getValue(sample_sel);
}
const float lens = float(cam_sample.getFocalLength());
const float apperture_x = float(cam_sample.getHorizontalAperture());
const float apperture_y = float(cam_sample.getVerticalAperture());
const float h_film_offset = float(cam_sample.getHorizontalFilmOffset());
const float v_film_offset = float(cam_sample.getVerticalFilmOffset());
const float film_aspect = apperture_x / apperture_y;
bcam->lens = lens;
bcam->sensor_x = apperture_x * 10;
bcam->sensor_y = apperture_y * 10;
bcam->shiftx = h_film_offset / apperture_x;
bcam->shifty = v_film_offset / apperture_y / film_aspect;
bcam->clip_start = max_ff(0.1f, float(cam_sample.getNearClippingPlane()));
bcam->clip_end = float(cam_sample.getFarClippingPlane());
bcam->dof.focus_distance = float(cam_sample.getFocusDistance());
bcam->dof.aperture_fstop = float(cam_sample.getFStop());
}
void AbcCameraReader::readObjectData(Main *bmain, const ISampleSelector &sample_sel)
{
Camera *bcam = BKE_camera_add(bmain, m_data_name.c_str());
read_camera_sample(bcam, m_schema, sample_sel);
m_object = BKE_object_add_only_object(bmain, OB_CAMERA, m_object_name.c_str());
m_object->data = id_cast<ID *>(bcam);
}
/* The macro that needs to be passed should have arguments :
* (short_name, rna_path, member_accessor) */
#define ENUMERATE_CAMERA_PROPERTIES(X) \
X(lens, lens, lens) \
X(sensor_width, sensor_width, sensor_x) \
X(sensor_height, sensor_height, sensor_y) \
X(clip_start, clip_start, clip_start) \
X(clip_end, clip_end, clip_end) \
X(shift_x, shift_x, shiftx) \
X(shift_y, shift_y, shifty) \
X(focus_distance, dof.focus_distance, dof.focus_distance) \
X(aperture_fstop, dof.aperture_fstop, dof.aperture_fstop) \
X(interocular_distance, stereo.interocular_distance, stereo.interocular_distance) \
X(convergence_distance, stereo.convergence_distance, stereo.convergence_distance)
class CameraFCurveCreationHelper : public FCurveCreationHelper {
Camera *camera_ = nullptr;
const Alembic::AbcGeom::ICameraSchema &schema_{};
/* Keep track of what has been modified to remove unnecessary fcurves at the end as Alembic
* seemingly does not have per property information. */
struct MemberModified {
#define DECLARE_MEMBER(short_name, rna_path, member_accessor) bool short_name = false;
ENUMERATE_CAMERA_PROPERTIES(DECLARE_MEMBER)
#undef DECLARE_MEMBER
};
MemberModified member_modified_{};
#define DECLARE_FCURVES(short_name, rna_path, member_accessor) \
FCurve *short_name##_fcurve = nullptr;
ENUMERATE_CAMERA_PROPERTIES(DECLARE_FCURVES)
#undef DECLARE_FCURVES
public:
CameraFCurveCreationHelper(Camera *camera, const Alembic::AbcGeom::ICameraSchema &schema)
: FCurveCreationHelper(&camera->id), camera_(camera), schema_(schema)
{
}
void create_fcurves(const int sample_count) override
{
#define CREATE_FCURVE(short_name, rna_path, member_accessor) \
short_name##_fcurve = create_fcurve({#rna_path, 0}, sample_count);
ENUMERATE_CAMERA_PROPERTIES(CREATE_FCURVE)
#undef CREATE_FCURVE
}
void set_fcurves_sample(const FrameSampleInfo &sample_info) override
{
/* To detect what has been modified. */
Camera last_camera = *camera_;
read_camera_sample(camera_, schema_, sample_info.selector);
#define SET_FCURVE_SAMPLE(short_name, rna_path, member_accessor) \
set_fcurve_sample(short_name##_fcurve, \
sample_info.sample_index, \
sample_info.frame, \
camera_->member_accessor); \
member_modified_.short_name |= last_camera.member_accessor != camera_->member_accessor;
ENUMERATE_CAMERA_PROPERTIES(SET_FCURVE_SAMPLE)
#undef SET_FCURVE_SAMPLE
}
void remove_unnecessary_fcurves() override
{
#define REMOVE_UNNECESSARY_FCURVE(short_name, rna_path, member_accessor) \
if (member_modified_.short_name == false) { \
channelbag->fcurve_remove(*short_name##_fcurve); \
}
ENUMERATE_CAMERA_PROPERTIES(REMOVE_UNNECESSARY_FCURVE)
#undef REMOVE_UNNECESSARY_FCURVE
}
};
std::unique_ptr<FCurveCreationHelper> AbcCameraReader::getKeyFramingHelper()
{
if (m_schema.isConstant()) {
return nullptr;
}
Camera *camera = id_cast<Camera *>(m_object->data);
return std::make_unique<CameraFCurveCreationHelper>(camera, m_schema);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,32 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/AbcGeom/ICamera.h>
namespace blender::io::alembic {
class AbcCameraReader final : public AbcObjectReader {
Alembic::AbcGeom::ICameraSchema m_schema;
public:
AbcCameraReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
std::unique_ptr<FCurveCreationHelper> getKeyFramingHelper() override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,593 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_curves.h"
#include "abc_axis_conversion.h"
#include "abc_util.h"
#include "DNA_curves_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_geometry_set.hh"
#include "BKE_object.hh"
#include "BLI_vector.hh"
#include "BLT_translation.hh"
#include "CLG_log.h"
#include "IO_validate.hh"
namespace blender {
using Alembic::Abc::FloatArraySamplePtr;
using Alembic::Abc::Int32ArraySamplePtr;
using Alembic::Abc::P3fArraySamplePtr;
using Alembic::Abc::PropertyHeader;
using Alembic::Abc::UcharArraySamplePtr;
using Alembic::AbcGeom::CurvePeriodicity;
using Alembic::AbcGeom::ICompoundProperty;
using Alembic::AbcGeom::ICurves;
using Alembic::AbcGeom::ICurvesSchema;
using Alembic::AbcGeom::IFloatGeomParam;
using Alembic::AbcGeom::IInt16Property;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::kWrapExisting;
namespace io::alembic {
static CLG_LogRef LOG = {"io.alembic"};
static int16_t get_curve_resolution(const ICurvesSchema &schema,
const Alembic::Abc::ISampleSelector &sample_sel)
{
ICompoundProperty user_props = schema.getUserProperties();
if (!user_props) {
return 0;
}
const PropertyHeader *header = user_props.getPropertyHeader(ABC_CURVE_RESOLUTION_U_PROPNAME);
if (!header || !header->isScalar() || !IInt16Property::matches(*header)) {
return 0;
}
IInt16Property resolu(user_props, header->getName());
return resolu.getValue(sample_sel);
}
static int16_t get_curve_order(const Alembic::AbcGeom::CurveType abc_curve_type,
const UcharArraySamplePtr orders,
const size_t curve_index)
{
switch (abc_curve_type) {
case Alembic::AbcGeom::kCubic:
return 4;
case Alembic::AbcGeom::kVariableOrder:
if (orders && orders->size() > curve_index) {
return int16_t((*orders)[curve_index]);
}
ATTR_FALLTHROUGH;
case Alembic::AbcGeom::kLinear:
default:
return 2;
}
}
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)
{
/* 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.
*/
const int start = idx;
const int end = idx + num_verts;
int overlap = 0;
const int safe_order = order <= num_verts ? order : num_verts;
for (int j = start, k = end - safe_order; j < (start + safe_order); j++, k++) {
const Imath::V3f &p1 = (*positions)[j];
const Imath::V3f &p2 = (*positions)[k];
if (p1 != p2) {
break;
}
overlap++;
}
/* TODO: Special case, need to figure out how it coincides with knots. */
if (overlap == 0 && num_verts > 2 && (*positions)[start] == (*positions)[end - 1]) {
overlap = 1;
}
return overlap;
}
static CurveType get_curve_type(const Alembic::AbcGeom::BasisType basis)
{
switch (basis) {
case Alembic::AbcGeom::kNoBasis:
return CURVE_TYPE_POLY;
case Alembic::AbcGeom::kBezierBasis:
return CURVE_TYPE_BEZIER;
case Alembic::AbcGeom::kBsplineBasis:
return CURVE_TYPE_NURBS;
case Alembic::AbcGeom::kCatmullromBasis:
return CURVE_TYPE_CATMULL_ROM;
case Alembic::AbcGeom::kHermiteBasis:
case Alembic::AbcGeom::kPowerBasis:
/* Those types are unknown to Blender, use a default poly type. */
return CURVE_TYPE_POLY;
}
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)
{
if (curves.offsets() != preprocessed_offsets) {
return true;
}
return false;
}
template<typename SampleType>
static bool samples_have_same_topology(const SampleType &sample, const SampleType &ceil_sample)
{
const P3fArraySamplePtr positions = sample.getPositions();
const Int32ArraySamplePtr per_curve_vertices_count = sample.getCurvesNumVertices();
const P3fArraySamplePtr ceil_positions = ceil_sample.getPositions();
const Int32ArraySamplePtr ceil_per_curve_vertices_count = ceil_sample.getCurvesNumVertices();
/* It the counters are different, we can be sure the topology is different. */
const bool different_counters = positions->size() != ceil_positions->size() ||
per_curve_vertices_count->size() !=
ceil_per_curve_vertices_count->size();
if (different_counters) {
return false;
}
/* Otherwise check the curve vertex counts. */
if (memcmp(per_curve_vertices_count->get(),
ceil_per_curve_vertices_count->get(),
per_curve_vertices_count->size() * sizeof(int)))
{
return false;
}
return true;
}
/* Preprocessed data to help and simplify converting curve data from Alembic to Blender.
* As some operations may require to look up the Alembic sample multiple times, we just
* do it once and cache the results in this.
*/
struct PreprocessedSampleData {
/* This holds one value for each spline. This will be used to lookup the data at the right
* indices, and will also be used to set #CurveGeometry.offsets. */
Vector<int> offset_in_blender;
/* This holds one value for each spline, and tells where in the Alembic curve sample the spline
* actually starts, accounting for duplicate points indicating cyclicity. */
Vector<int> offset_in_alembic;
/* This holds one value for each spline to tell whether it is cyclic. */
Vector<bool> curves_cyclic;
/* This holds one value for each spline which define its order. */
Vector<int8_t> curves_orders;
/* True if any values of `curves_overlaps` is true. If so, we will need to copy the
* `curves_overlaps` to an attribute on the Blender curves. */
bool do_cyclic = false;
/* Only one curve type for the whole objects. */
CurveType curve_type = CURVE_TYPE_POLY;
int8_t knot_mode = 0;
/* Optional settings for reading interpolated vertices. If present, `ceil_positions` has to be
* valid. */
std::optional<SampleInterpolationSettings> interpolation_settings;
/* Store the pointers during preprocess so we do not have to look up the sample twice. */
P3fArraySamplePtr positions = nullptr;
P3fArraySamplePtr ceil_positions = nullptr;
FloatArraySamplePtr weights = nullptr;
FloatArraySamplePtr radii = nullptr;
};
/* Compute topological information about the curves. We do this step mainly to properly account
* for curves overlaps which imply different offsets between Blender and Alembic, but also to
* validate the data and cache some values. */
static std::optional<PreprocessedSampleData> preprocess_sample(StringRefNull iobject_name,
bool use_interpolation,
const ICurvesSchema &schema,
const ISampleSelector sample_sel)
{
ICurvesSchema::Sample smp;
try {
smp = schema.getValue(sample_sel);
}
catch (Alembic::Util::Exception &ex) {
CLOG_WARN(&LOG,
"Error reading curve sample for '%s/%s' at time %f: %s",
iobject_name.c_str(),
schema.getName().c_str(),
sample_sel.getRequestedTime(),
ex.what());
return {};
}
/* NOTE: although Alembic can store knots, we do not read them as the functionality is not
* exposed by the Blender's Curves API yet. */
const Int32ArraySamplePtr per_curve_vertices_count = smp.getCurvesNumVertices();
const P3fArraySamplePtr positions = smp.getPositions();
const FloatArraySamplePtr weights = smp.getPositionWeights();
const CurvePeriodicity periodicity = smp.getWrap();
const UcharArraySamplePtr orders = smp.getOrders();
if (positions->size() == 0) {
return {};
}
if (!validate::size_fits_in_int(positions->size()) ||
!validate::size_fits_in_int(per_curve_vertices_count->size()))
{
CLOG_WARN(&LOG,
"Curves too large to import for '%s/%s' at time %f, exceeds max int size",
iobject_name.c_str(),
schema.getName().c_str(),
sample_sel.getRequestedTime());
return {};
}
const IFloatGeomParam widths_param = schema.getWidthsParam();
FloatArraySamplePtr radii;
if (widths_param.valid()) {
IFloatGeomParam::Sample wsample = widths_param.getExpandedValue(sample_sel);
radii = wsample.getVals();
}
const int curve_count = per_curve_vertices_count->size();
PreprocessedSampleData data;
/* Add 1 as these store offsets with the actual value being `offset[i + 1] - offset[i]`. */
data.offset_in_blender.resize(curve_count + 1);
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 #kVariableOrder is set then we must have order data. If not, this sample is suspect.
* Interpret the data as linear as a fallback. See #126324 for one such example.
* See also: Alembic source code in `ICurves.h`, #ICurvesSchema::Sample::valid() */
if (smp.getType() == Alembic::AbcGeom::kVariableOrder && !orders) {
data.curve_type = CURVE_TYPE_POLY;
data.knot_mode = NURBS_KNOT_MODE_NORMAL;
data.do_cyclic = false;
}
if (data.curve_type == CURVE_TYPE_NURBS) {
data.curves_orders.resize(curve_count);
}
/* Compute topological information. */
const int positions_size = positions->size();
int blender_offset = 0;
int alembic_offset = 0;
for (size_t i = 0; i < curve_count; i++) {
int vertices_count = (*per_curve_vertices_count)[i];
/* Guard against invalid vertex counts. */
if (vertices_count < 0 || vertices_count > positions_size - alembic_offset) {
vertices_count = std::max(0, positions_size - alembic_offset);
}
const int curve_order = get_curve_order(smp.getType(), orders, i);
data.offset_in_blender[i] = blender_offset;
data.offset_in_alembic[i] = alembic_offset;
data.curves_cyclic[i] = data.do_cyclic;
if (data.curve_type == CURVE_TYPE_NURBS) {
data.curves_orders[i] = curve_order;
}
/* 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;
data.offset_in_alembic[curve_count] = alembic_offset;
/* Store relevant pointers. */
data.positions = positions;
if (weights && weights->size() > 1) {
data.weights = weights;
}
if (radii && radii->size() > 1) {
data.radii = radii;
}
const std::optional<SampleInterpolationSettings> interpolation_settings =
get_sample_interpolation_settings(
sample_sel, schema.getTimeSampling(), schema.getNumSamples());
if (use_interpolation && interpolation_settings.has_value()) {
Alembic::AbcGeom::ICurvesSchema::Sample ceil_smp;
schema.get(ceil_smp, Alembic::Abc::ISampleSelector(interpolation_settings->ceil_index));
if (samples_have_same_topology(smp, ceil_smp)) {
data.ceil_positions = ceil_smp.getPositions();
data.interpolation_settings = interpolation_settings;
}
}
return data;
}
AbcCurveReader::AbcCurveReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
ICurves abc_curves(m_iobject, kWrapExisting);
m_curves_schema = abc_curves.getSchema();
}
bool AbcCurveReader::valid() const
{
return m_curves_schema.valid();
}
bool AbcCurveReader::accepts_object_type(
const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const
{
if (!Alembic::AbcGeom::ICurves::matches(alembic_header)) {
*r_err_str = RPT_(
"Object type mismatch, Alembic object path pointed to Curves when importing, but not "
"anymore.");
return false;
}
if (ob->type != OB_CURVES) {
*r_err_str = RPT_("Object type mismatch, Alembic object path points to Curves.");
return false;
}
return true;
}
void AbcCurveReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel)
{
Curves *curves = BKE_curves_add(bmain, m_data_name.c_str());
m_object = BKE_object_add_only_object(bmain, OB_CURVES, m_object_name.c_str());
m_object->data = id_cast<ID *>(curves);
read_curves_sample(curves, false, m_curves_schema, sample_sel);
if (m_settings->always_add_cache_reader || has_animations(m_curves_schema, m_settings)) {
addCacheModifier();
}
}
BLI_INLINE float3 interpolate_to_zup(const Span<Imath::V3f> &floor_positions,
const Span<Imath::V3f> &ceil_positions,
int i,
float weight)
{
float3 p;
const Imath::V3f &floor_pos = floor_positions[i];
const Imath::V3f &ceil_pos = ceil_positions[i];
interp_v3_v3v3(p, floor_pos.getValue(), ceil_pos.getValue(), weight);
copy_zup_from_yup(p, p);
return p;
}
static void add_bezier_control_point(int cp,
int offset,
const Span<Imath::V3f> floor_positions,
const Span<Imath::V3f> ceil_positions,
MutableSpan<float3> positions,
MutableSpan<float3> handles_left,
MutableSpan<float3> handles_right,
float weight)
{
positions[cp] = interpolate_to_zup(floor_positions, ceil_positions, offset, weight);
if (offset == 0) {
handles_right[cp] = interpolate_to_zup(floor_positions, ceil_positions, offset + 1, weight);
handles_left[cp] = 2.0f * positions[cp] - handles_right[cp];
}
else if (offset == floor_positions.size() - 1) {
handles_left[cp] = interpolate_to_zup(floor_positions, ceil_positions, offset - 1, weight);
handles_right[cp] = 2.0f * positions[cp] - handles_left[cp];
}
else {
handles_left[cp] = interpolate_to_zup(floor_positions, ceil_positions, offset - 1, weight);
handles_right[cp] = interpolate_to_zup(floor_positions, ceil_positions, offset + 1, weight);
}
}
void AbcCurveReader::read_curves_sample(Curves *curves_id,
bool use_interpolation,
const ICurvesSchema &schema,
const ISampleSelector &sample_sel)
{
std::optional<PreprocessedSampleData> opt_preprocess = preprocess_sample(
m_iobject.getFullName(), use_interpolation, schema, sample_sel);
if (!opt_preprocess) {
return;
}
const PreprocessedSampleData &data = opt_preprocess.value();
const int point_count = data.offset_in_blender.last();
const int curve_count = data.offset_in_blender.size() - 1;
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
if (curves_topology_changed(curves, data.offset_in_blender)) {
curves.resize(point_count, curve_count);
curves.offsets_for_write().copy_from(data.offset_in_blender);
}
curves.fill_curve_types(data.curve_type);
if (data.curve_type != CURVE_TYPE_POLY) {
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();
Span<Imath::V3f> alembic_points{&(*data.positions)[0], int64_t((*data.positions).size())};
Span<Imath::V3f> alembic_points_ceil;
float interp_weight = 0.0f;
if (data.interpolation_settings.has_value()) {
alembic_points_ceil = {&(*data.ceil_positions)[0], int64_t((*data.ceil_positions).size())};
interp_weight = data.interpolation_settings->weight;
}
else {
alembic_points_ceil = alembic_points;
}
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),
alembic_points_ceil.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),
interp_weight);
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]) {
if (data.interpolation_settings.has_value()) {
curves_positions[i_point] = interpolate_to_zup(
alembic_points, alembic_points_ceil, position_offset++, interp_weight);
}
else {
curves_positions[i_point] = to_zup_float3(alembic_points[position_offset++]);
}
}
}
}
if (data.do_cyclic) {
curves.cyclic_for_write().copy_from(data.curves_cyclic);
}
if (data.radii) {
MutableSpan<float> radii = curves.radius_for_write();
Alembic::Abc::FloatArraySample alembic_widths = *data.radii;
for (const int i_point : curves.points_range()) {
radii[i_point] = alembic_widths[i_point] / 2.0f;
}
}
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();
Span<float> data_weights_span = {data.weights->get(), int64_t(data.weights->size())};
for (const int i_curve : curves.curves_range()) {
const int alembic_offset = data.offset_in_alembic[i_curve];
const IndexRange points = curves.points_by_curve()[i_curve];
curves_weights.slice(points).copy_from(
data_weights_span.slice(alembic_offset, points.size()));
}
}
}
}
void AbcCurveReader::read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char ** /*r_err_str*/)
{
Curves *curves = geometry_set.get_curves_for_write();
bool use_interpolation = read_params.read_flag & MOD_MESHSEQ_INTERPOLATE_VERTICES;
read_curves_sample(curves, use_interpolation, m_curves_schema, sample_sel);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,47 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/AbcGeom/ICurves.h>
namespace blender {
struct Curves;
#define ABC_CURVE_RESOLUTION_U_PROPNAME "blender:resolution"
namespace io::alembic {
class AbcCurveReader final : public AbcObjectReader {
Alembic::AbcGeom::ICurvesSchema m_curves_schema;
public:
AbcCurveReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
void read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str) override;
void read_curves_sample(Curves *curves_id,
bool use_interpolation,
const Alembic::AbcGeom::ICurvesSchema &schema,
const Alembic::Abc::ISampleSelector &sample_selector);
};
} // namespace io::alembic
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "BLI_span.hh"
#include "abc_reader_object.h"
#include <Alembic/AbcGeom/IPolyMesh.h>
#include <Alembic/AbcGeom/ISubD.h>
namespace blender {
struct Mesh;
namespace io::alembic {
class AbcMeshReader final : public AbcObjectReader {
Alembic::AbcGeom::IPolyMeshSchema m_schema;
public:
AbcMeshReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
struct Mesh *read_mesh(struct Mesh *existing_mesh,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str);
void read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str) override;
bool topology_changed(const Mesh *existing_mesh,
const Alembic::Abc::ISampleSelector &sample_sel) override;
private:
void readFaceSetsSample(Main *bmain,
Mesh *mesh,
const Alembic::AbcGeom::ISampleSelector &sample_sel);
void assign_facesets_to_material_indices(const Alembic::Abc::ISampleSelector &sample_sel,
MutableSpan<int> material_indices,
std::map<std::string, int> &r_mat_map);
};
class AbcSubDReader final : public AbcObjectReader {
Alembic::AbcGeom::ISubDSchema m_schema;
public:
AbcSubDReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
void read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str) override;
bool topology_changed(const Mesh *existing_mesh,
const Alembic::Abc::ISampleSelector &sample_sel) override;
private:
struct Mesh *read_mesh(struct Mesh *existing_mesh,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str);
};
void read_mverts(Mesh &mesh,
const Alembic::AbcGeom::P3fArraySamplePtr positions,
const Alembic::AbcGeom::N3fArraySamplePtr normals);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,241 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_nurbs.h"
#include "abc_axis_conversion.h"
#include "abc_util.h"
#include "MEM_guardedalloc.h"
#include "DNA_curve_types.h"
#include "DNA_object_types.h"
#include "BLI_listbase.h"
#include "BLT_translation.hh"
#include "BKE_curve.hh"
#include "BKE_object.hh"
#include "CLG_log.h"
namespace blender {
using Alembic::AbcGeom::FloatArraySamplePtr;
using Alembic::AbcGeom::kWrapExisting;
using Alembic::AbcGeom::MetaData;
using Alembic::AbcGeom::P3fArraySamplePtr;
using Alembic::AbcGeom::ICompoundProperty;
using Alembic::AbcGeom::INuPatch;
using Alembic::AbcGeom::INuPatchSchema;
using Alembic::AbcGeom::IObject;
namespace io::alembic {
static CLG_LogRef LOG = {"io.alembic"};
AbcNurbsReader::AbcNurbsReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
getNurbsPatches(m_iobject);
}
bool AbcNurbsReader::valid() const
{
if (m_schemas.empty()) {
return false;
}
std::vector<std::pair<INuPatchSchema, IObject>>::const_iterator it;
for (it = m_schemas.begin(); it != m_schemas.end(); ++it) {
const INuPatchSchema &schema = it->first;
if (!schema.valid()) {
return false;
}
}
return true;
}
bool AbcNurbsReader::accepts_object_type(
const Alembic::AbcCoreAbstract::v12::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const
{
if (!Alembic::AbcGeom::INuPatch::matches(alembic_header)) {
*r_err_str = RPT_(
"Object type mismatch, Alembic object path pointed to NURBS when importing, but not any "
"more");
return false;
}
if (ob->type != OB_CURVES_LEGACY) {
*r_err_str = RPT_("Object type mismatch, Alembic object path points to NURBS");
return false;
}
return true;
}
static bool set_knots(const FloatArraySamplePtr &knots, float *&nu_knots)
{
if (!knots || knots->size() < 2) {
return false;
}
/* Skip first and last knots, as they are used for padding. */
const size_t num_knots = knots->size() - 2;
nu_knots = MEM_new_array_zeroed<float>(num_knots, "abc_setsplineknotsu");
for (size_t i = 0; i < num_knots; i++) {
nu_knots[i] = (*knots)[i + 1];
}
return true;
}
void AbcNurbsReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel)
{
Curve *cu = BKE_curve_add(bmain, m_data_name.c_str(), OB_SURF);
cu->actvert = CU_ACT_NONE;
std::vector<std::pair<INuPatchSchema, IObject>>::iterator it;
for (it = m_schemas.begin(); it != m_schemas.end(); ++it) {
Nurb *nu = MEM_new<Nurb>("abc_getnurb");
nu->flag = CU_SMOOTH;
nu->type = CU_NURBS;
nu->resolu = cu->resolu;
nu->resolv = cu->resolv;
const INuPatchSchema &schema = it->first;
INuPatchSchema::Sample smp;
try {
smp = schema.getValue(sample_sel);
}
catch (Alembic::Util::Exception &ex) {
CLOG_WARN(&LOG,
"Error reading nurbs sample for '%s/%s' at time %f: %s",
m_iobject.getFullName().c_str(),
schema.getName().c_str(),
sample_sel.getRequestedTime(),
ex.what());
return;
}
nu->orderu = smp.getUOrder() - 1;
nu->orderv = smp.getVOrder() - 1;
nu->pntsu = smp.getNumU();
nu->pntsv = smp.getNumV();
/* Read positions and weights. */
const P3fArraySamplePtr positions = smp.getPositions();
const FloatArraySamplePtr weights = smp.getPositionWeights();
const size_t num_points = positions->size();
const bool has_weights = weights && weights->size() >= num_points;
nu->bp = MEM_new_array_zeroed<BPoint>(num_points, "abc_setsplinetype");
BPoint *bp = nu->bp;
float posw_in = 1.0f;
for (size_t i = 0; i < num_points; i++, bp++) {
const Imath::V3f &pos_in = (*positions)[i];
if (has_weights) {
posw_in = (*weights)[i];
}
copy_zup_from_yup(bp->vec, pos_in.getValue());
bp->vec[3] = posw_in;
bp->f1 = SELECT;
bp->radius = 1.0f;
bp->weight = 1.0f;
}
/* Read knots. */
if (!set_knots(smp.getUKnot(), nu->knotsu)) {
BKE_nurb_knot_calc_u(nu);
}
if (!set_knots(smp.getVKnot(), nu->knotsv)) {
BKE_nurb_knot_calc_v(nu);
}
/* Read flags. */
ICompoundProperty user_props = schema.getUserProperties();
if (has_property(user_props, "enpoint_u")) {
nu->flagu |= CU_NURB_ENDPOINT;
}
if (has_property(user_props, "enpoint_v")) {
nu->flagv |= CU_NURB_ENDPOINT;
}
if (has_property(user_props, "cyclic_u")) {
nu->flagu |= CU_NURB_CYCLIC;
}
if (has_property(user_props, "cyclic_v")) {
nu->flagv |= CU_NURB_CYCLIC;
}
BLI_addtail(BKE_curve_nurbs_get(cu), nu);
}
m_object = BKE_object_add_only_object(bmain, OB_SURF, m_object_name.c_str());
m_object->data = id_cast<ID *>(cu);
}
void AbcNurbsReader::getNurbsPatches(const IObject &obj)
{
if (!obj.valid()) {
return;
}
const int num_children = obj.getNumChildren();
if (num_children == 0) {
INuPatch abc_nurb(obj, kWrapExisting);
INuPatchSchema schem = abc_nurb.getSchema();
m_schemas.emplace_back(schem, obj);
return;
}
for (int i = 0; i < num_children; i++) {
bool ok = true;
IObject child(obj, obj.getChildHeader(i).getName());
if (!m_name.empty() && child.valid() && !begins_with(child.getFullName(), m_name)) {
ok = false;
}
if (!child.valid()) {
continue;
}
const MetaData &md = child.getMetaData();
if (INuPatch::matches(md) && ok) {
INuPatch abc_nurb(child, kWrapExisting);
INuPatchSchema schem = abc_nurb.getSchema();
m_schemas.emplace_back(schem, child);
}
getNurbsPatches(child);
}
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/AbcGeom/INuPatch.h>
namespace blender::io::alembic {
class AbcNurbsReader final : public AbcObjectReader {
std::vector<std::pair<Alembic::AbcGeom::INuPatchSchema, Alembic::Abc::IObject>> m_schemas;
public:
AbcNurbsReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
private:
void getNurbsPatches(const Alembic::Abc::IObject &obj);
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,425 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include "abc_axis_conversion.h"
#include "abc_keyframing.h"
#include "abc_util.h"
#include "DNA_cachefile_types.h"
#include "DNA_constraint_types.h"
#include "DNA_modifier_types.h"
#include "DNA_object_types.h"
#include "ANIM_fcurve.hh"
#include "BKE_constraint.h"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BKE_object.hh"
#include "BKE_object_types.hh"
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_math_rotation.h"
#include "BLI_string.h"
#include "Alembic/AbcGeom/Visibility.h"
#include "CLG_log.h"
namespace blender {
using Alembic::AbcGeom::IObject;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::IVisibilityProperty;
using Alembic::AbcGeom::IXform;
using Alembic::AbcGeom::IXformSchema;
using Alembic::AbcGeom::ObjectVisibility;
namespace io::alembic {
static CLG_LogRef LOG = {"io.alembic"};
AbcReaderConstructorArgs create_reader_constructor_args(const IObject &object,
ImportSettings &settings)
{
return AbcReaderConstructorArgs{.object = object, .settings = settings};
}
AbcObjectReader::AbcObjectReader(const AbcReaderConstructorArgs &args)
: m_object(nullptr),
m_iobject(args.object),
m_settings(&args.settings),
m_is_reading_a_file_sequence(args.settings.is_sequence),
m_refcount(0),
parent_reader(nullptr)
{
m_name = m_iobject.getFullName();
std::vector<std::string> parts;
split(m_name, '/', parts);
if (parts.size() >= 2) {
m_object_name = parts[parts.size() - 2];
m_data_name = parts[parts.size() - 1];
}
else {
m_object_name = m_data_name = parts[parts.size() - 1];
}
determine_inherits_xform();
}
void AbcObjectReader::determine_inherits_xform()
{
m_inherits_xform = false;
IXform ixform = xform();
if (!ixform) {
return;
}
const IXformSchema &schema(ixform.getSchema());
if (!schema.valid()) {
std::cerr << "Alembic object " << ixform.getFullName() << " has an invalid schema."
<< std::endl;
return;
}
m_inherits_xform = schema.getInheritsXforms();
IObject ixform_parent = ixform.getParent();
if (!ixform_parent.getParent()) {
/* The archive top object certainly is not a transform itself, so handle
* it as "no parent". */
m_inherits_xform = false;
}
else {
m_inherits_xform = ixform_parent && m_inherits_xform;
}
}
const IObject &AbcObjectReader::iobject() const
{
return m_iobject;
}
Object *AbcObjectReader::object() const
{
return m_object;
}
void AbcObjectReader::object(Object *ob)
{
m_object = ob;
}
static Imath::M44d blend_matrices(const Imath::M44d &m0,
const Imath::M44d &m1,
const double weight)
{
float mat0[4][4], mat1[4][4], ret[4][4];
/* Cannot use Imath::M44d::getValue() since this returns a pointer to
* doubles and interp_m4_m4m4 expects pointers to floats. So need to convert
* the matrices manually.
*/
convert_matrix_datatype(m0, mat0);
convert_matrix_datatype(m1, mat1);
interp_m4_m4m4(ret, mat0, mat1, float(weight));
return convert_matrix_datatype(ret);
}
Imath::M44d get_matrix(const IXformSchema &schema, const chrono_t time)
{
Alembic::AbcGeom::ISampleSelector selector(time);
const std::optional<SampleInterpolationSettings> interpolation_settings =
get_sample_interpolation_settings(
selector, schema.getTimeSampling(), schema.getNumSamples());
if (!interpolation_settings.has_value()) {
/* No interpolation, just read the current time. */
Alembic::AbcGeom::XformSample s0;
schema.get(s0, selector);
return s0.getMatrix();
}
Alembic::AbcGeom::XformSample s0, s1;
schema.get(s0, Alembic::AbcGeom::ISampleSelector(interpolation_settings->index));
schema.get(s1, Alembic::AbcGeom::ISampleSelector(interpolation_settings->ceil_index));
return blend_matrices(s0.getMatrix(), s1.getMatrix(), interpolation_settings->weight);
}
void AbcObjectReader::read_geometry(bke::GeometrySet & /*geometry_set*/,
const Alembic::Abc::ISampleSelector & /*sample_sel*/,
const AbcReadGeometryParams & /*read_params*/,
const char ** /*r_err_str*/)
{
}
bool AbcObjectReader::topology_changed(const Mesh * /*existing_mesh*/,
const Alembic::Abc::ISampleSelector & /*sample_sel*/)
{
/* The default implementation of read_mesh() just returns the original mesh, so never changes the
* topology. */
return false;
}
class VisibilityFCurveCreationHelper : public FCurveCreationHelper {
IObject vis_object_{};
IVisibilityProperty vis_prop_{};
FCurve *viewport_fcurve = nullptr;
FCurve *render_fcurve = nullptr;
public:
VisibilityFCurveCreationHelper(Object *object,
const IObject &vis_object,
const IVisibilityProperty &vis_prop)
: FCurveCreationHelper(&object->id), vis_object_(vis_object), vis_prop_(vis_prop)
{
}
void create_fcurves(const int sample_count) override
{
viewport_fcurve = create_fcurve({"hide_viewport", 0}, sample_count);
render_fcurve = create_fcurve({"hide_render", 0}, sample_count);
}
void set_fcurves_sample(const FrameSampleInfo &sample_info) override
{
ObjectVisibility vis = ObjectVisibility(vis_prop_.getValue(sample_info.selector));
if (vis == Alembic::AbcGeom::kVisibilityDeferred) {
IObject parent = vis_object_.getParent();
while (parent) {
const IVisibilityProperty &parent_vis_prop(
Alembic::AbcGeom::GetVisibilityProperty(parent));
if (parent_vis_prop) {
vis = ObjectVisibility(parent_vis_prop.getValue(sample_info.selector));
if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
break;
}
}
parent = parent.getParent();
}
}
const float hidden = (vis == ObjectVisibility::kVisibilityHidden) ? 1.0f : 0.0f;
set_fcurve_sample(viewport_fcurve, sample_info.sample_index, sample_info.frame, hidden);
set_fcurve_sample(render_fcurve, sample_info.sample_index, sample_info.frame, hidden);
}
};
void AbcObjectReader::getKeyFramingHelpers(
Vector<std::unique_ptr<FCurveCreationHelper>> &keyframing_helpers)
{
/* Check if we have animated visibility. */
IObject vis_object = m_iobject;
ObjectVisibility vis = Alembic::AbcGeom::kVisibilityDeferred;
while (vis_object) {
IVisibilityProperty vis_prop = Alembic::AbcGeom::GetVisibilityProperty(vis_object);
if (vis_prop) {
if (!vis_prop.isConstant()) {
std::unique_ptr<FCurveCreationHelper> helper =
std::make_unique<VisibilityFCurveCreationHelper>(m_object, vis_object, vis_prop);
keyframing_helpers.append(std::move(helper));
m_has_visibility_keyframes = true;
break;
}
vis = ObjectVisibility(vis_prop.getValue(ISampleSelector()));
if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
break;
}
}
vis_object = vis_object.getParent();
}
/* Helper for the object data. */
std::unique_ptr<FCurveCreationHelper> specific_helper = getKeyFramingHelper();
if (specific_helper) {
keyframing_helpers.append(std::move(specific_helper));
}
}
std::unique_ptr<FCurveCreationHelper> AbcObjectReader::getKeyFramingHelper()
{
return nullptr;
}
void AbcObjectReader::setupObjectTransform(const chrono_t time)
{
bool is_constant = false;
float transform_from_alembic[4][4];
/* If the parent is a camera, apply the inverse rotation to make up for the from-Maya rotation.
* This assumes that the parent object also was imported from Alembic. */
if (m_object->parent != nullptr && m_object->parent->type == OB_CAMERA) {
axis_angle_to_mat4_single(m_object->parentinv, 'X', -M_PI_2);
}
this->read_matrix(transform_from_alembic, time, m_settings->scale, is_constant);
/* Apply the matrix to the object. */
BKE_object_apply_mat4(m_object, transform_from_alembic, true, false);
BKE_object_to_mat4(m_object, m_object->runtime->object_to_world.ptr());
if (!is_constant || m_settings->always_add_cache_reader) {
bConstraint *con = BKE_constraint_add_for_object(
m_object, nullptr, CONSTRAINT_TYPE_TRANSFORM_CACHE);
bTransformCacheConstraint *data = static_cast<bTransformCacheConstraint *>(con->data);
STRNCPY(data->object_path, m_iobject.getFullName().c_str());
data->cache_file = m_settings->cache_file;
id_us_plus(&data->cache_file->id);
}
}
Alembic::AbcGeom::IXform AbcObjectReader::xform()
{
/* Check that we have an empty object (locator, bone head/tail...). */
if (IXform::matches(m_iobject.getMetaData())) {
try {
return IXform(m_iobject, Alembic::AbcGeom::kWrapExisting);
}
catch (Alembic::Util::Exception &ex) {
CLOG_WARN(&LOG,
"Error reading object transform for '%s': %s",
m_iobject.getFullName().c_str(),
ex.what());
return IXform();
}
}
/* Check that we have an object with actual data, in which case the
* parent Alembic object should contain the transform. */
IObject abc_parent = m_iobject.getParent();
/* The archive's top object can be recognized by not having a parent. */
if (abc_parent.getParent() && IXform::matches(abc_parent.getMetaData())) {
try {
return IXform(abc_parent, Alembic::AbcGeom::kWrapExisting);
}
catch (Alembic::Util::Exception &ex) {
CLOG_WARN(&LOG,
"Error reading object transform for '%s': %s",
abc_parent.getFullName().c_str(),
ex.what());
return IXform();
}
}
/* This can happen in certain cases. For example, MeshLab exports
* point clouds without parent XForm. */
return IXform();
}
void AbcObjectReader::read_matrix(float r_mat[4][4] /* local matrix */,
const chrono_t time,
const float scale,
bool &r_is_constant)
{
IXform ixform = xform();
if (!ixform) {
unit_m4(r_mat);
r_is_constant = true;
return;
}
const IXformSchema &schema(ixform.getSchema());
if (!schema.valid()) {
std::cerr << "Alembic object " << ixform.getFullName() << " has an invalid schema."
<< std::endl;
return;
}
const Imath::M44d matrix = get_matrix(schema, time);
convert_matrix_datatype(matrix, r_mat);
copy_m44_axis_swap(r_mat, r_mat, ABC_ZUP_FROM_YUP);
/* Convert from Maya to Blender camera orientation. Children of this camera
* will have the opposite transform as their Parent Inverse matrix.
* See AbcObjectReader::setupObjectTransform(). */
if (m_object->type == OB_CAMERA) {
float camera_rotation[4][4];
axis_angle_to_mat4_single(camera_rotation, 'X', M_PI_2);
mul_m4_m4m4(r_mat, r_mat, camera_rotation);
}
if (!m_inherits_xform) {
/* Only apply scaling to root objects, parenting will propagate it. */
float scale_mat[4][4];
scale_m4_fl(scale_mat, scale);
mul_m4_m4m4(r_mat, scale_mat, r_mat);
}
r_is_constant = schema.isConstant();
}
void AbcObjectReader::addCacheModifier()
{
ModifierData *md = BKE_modifier_new(eModifierType_MeshSequenceCache);
BLI_addtail(&m_object->modifiers, md);
BKE_modifiers_persistent_uid_init(*m_object, *md);
MeshSeqCacheModifierData *mcmd = reinterpret_cast<MeshSeqCacheModifierData *>(md);
mcmd->cache_file = m_settings->cache_file;
id_us_plus(&mcmd->cache_file->id);
STRNCPY(mcmd->object_path, m_iobject.getFullName().c_str());
}
void AbcObjectReader::readVisibility()
{
IObject vis_object = m_iobject;
ObjectVisibility vis = Alembic::AbcGeom::kVisibilityDeferred;
while (vis_object) {
IVisibilityProperty vis_prop = Alembic::AbcGeom::GetVisibilityProperty(vis_object);
if (vis_prop) {
if (!vis_prop.isConstant()) {
return;
}
vis = ObjectVisibility(vis_prop.getValue(ISampleSelector()));
if (vis != Alembic::AbcGeom::kVisibilityDeferred) {
break;
}
}
vis_object = vis_object.getParent();
}
if (vis == Alembic::AbcGeom::kVisibilityHidden) {
m_object->visibility_flag |= (OB_HIDE_RENDER | OB_HIDE_VIEWPORT);
}
}
int AbcObjectReader::refcount() const
{
return m_refcount;
}
void AbcObjectReader::incref()
{
m_refcount++;
}
void AbcObjectReader::decref()
{
m_refcount--;
BLI_assert(m_refcount >= 0);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,195 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "BLI_vector.hh"
#include <Alembic/Abc/IObject.h>
#include <Alembic/Abc/ISampleSelector.h>
#include <Alembic/AbcCoreAbstract/Foundation.h>
#include <Alembic/AbcCoreAbstract/ObjectHeader.h>
#include <Alembic/AbcGeom/IXform.h>
#include <string>
#include <vector>
namespace blender {
struct CacheFile;
struct Main;
struct Mesh;
struct Object;
namespace bke {
struct GeometrySet;
}
using Alembic::AbcCoreAbstract::chrono_t;
namespace io::alembic {
class FCurveCreationHelper;
struct TimeInfo;
struct ImportSettings {
bool blender_archive_version_prior_44 = false;
bool do_convert_mat = false;
float conversion_mat[4][4];
int from_up = 0;
int from_forward = 0;
float scale = 1.0f;
bool is_sequence = false;
bool set_frame_range = false;
/* Min and max frame detected from file sequences. */
int sequence_min_frame = 0;
int sequence_max_frame = 1;
/* From MeshSeqCacheModifierData.read_flag */
int read_flag = 0;
/* From CacheFile and MeshSeqCacheModifierData */
std::string velocity_name;
float velocity_scale = 1.0f;
bool validate_meshes = false;
bool always_add_cache_reader = false;
CacheFile *cache_file = nullptr;
ImportSettings() = default;
};
template<typename Schema> static bool has_animations(Schema &schema, ImportSettings *settings)
{
return settings->is_sequence || !schema.isConstant();
}
struct AbcReadGeometryParams {
std::string velocity_name;
int read_flag = 0;
float velocity_scale = 1.0f;
};
struct AbcReaderConstructorArgs {
const Alembic::Abc::IObject &object;
ImportSettings &settings;
};
AbcReaderConstructorArgs create_reader_constructor_args(const Alembic::Abc::IObject &object,
ImportSettings &settings);
class AbcObjectReader {
protected:
std::string m_name;
std::string m_object_name;
std::string m_data_name;
Object *m_object;
Alembic::Abc::IObject m_iobject;
/* XXX - This used to reference stack memory for MeshSequenceCache scenarios. That has been
* addressed but ownership of these settings should be made more apparent to prevent similar
* issues in the future. */
ImportSettings *m_settings;
/* This is initialized from the ImportSettings above on construction. It will need to be removed
* once we fix the stack memory reference situation. */
bool m_is_reading_a_file_sequence = false;
/* Use reference counting since the same reader may be used by multiple
* modifiers and/or constraints. */
int m_refcount;
bool m_inherits_xform;
bool m_has_visibility_keyframes = false;
public:
AbcObjectReader *parent_reader;
public:
explicit AbcObjectReader(const AbcReaderConstructorArgs &args);
virtual ~AbcObjectReader() = default;
const Alembic::Abc::IObject &iobject() const;
using ptr_vector = std::vector<AbcObjectReader *>;
/**
* Returns the transform of this object. This can be the Alembic object
* itself (in case of an Empty) or it can be the parent Alembic object.
*/
virtual Alembic::AbcGeom::IXform xform();
Object *object() const;
void object(Object *ob);
const std::string &name() const
{
return m_name;
}
const std::string &object_name() const
{
return m_object_name;
}
const std::string &data_name() const
{
return m_data_name;
}
bool inherits_xform() const
{
return m_inherits_xform;
}
bool has_visibility_keyframes() const
{
return m_has_visibility_keyframes;
}
virtual bool valid() const = 0;
virtual bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const = 0;
virtual void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) = 0;
virtual void read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str);
virtual bool topology_changed(const Mesh *existing_mesh,
const Alembic::Abc::ISampleSelector &sample_sel);
void getKeyFramingHelpers(Vector<std::unique_ptr<FCurveCreationHelper>> &keyframing_helpers);
virtual std::unique_ptr<FCurveCreationHelper> getKeyFramingHelper();
/** Reads the object matrix and sets up an object transform if animated. */
void setupObjectTransform(chrono_t time);
void addCacheModifier();
void readVisibility();
int refcount() const;
void incref();
void decref();
void read_matrix(float r_mat[4][4], chrono_t time, float scale, bool &is_constant);
protected:
/** Determine whether we can inherit our parent's XForm. */
void determine_inherits_xform();
};
Imath::M44d get_matrix(const Alembic::AbcGeom::IXformSchema &schema, chrono_t time);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,294 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_points.h"
#include "abc_axis_conversion.h"
#include "abc_util.h"
#include "DNA_object_types.h"
#include "DNA_pointcloud_types.h"
#include "BLT_translation.hh"
#include "BKE_geometry_set.hh"
#include "BKE_object.hh"
#include "BKE_pointcloud.hh"
#include "BLI_color_types.hh"
#include "IO_validate.hh"
#include "CLG_log.h"
#include <algorithm>
namespace blender {
using namespace Alembic::AbcGeom;
namespace io::alembic {
static CLG_LogRef LOG = {"io.alembic"};
AbcPointsReader::AbcPointsReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
IPoints ipoints(m_iobject, kWrapExisting);
m_schema = ipoints.getSchema();
}
bool AbcPointsReader::valid() const
{
return m_schema.valid();
}
bool AbcPointsReader::accepts_object_type(
const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const
{
if (!Alembic::AbcGeom::IPoints::matches(alembic_header)) {
*r_err_str = RPT_(
"Object type mismatch, Alembic object path pointed to Points when importing, but not any "
"more");
return false;
}
if (ob->type != OB_POINTCLOUD) {
*r_err_str = RPT_("Object type mismatch, Alembic object path points to Points.");
return false;
}
return true;
}
void AbcPointsReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel)
{
PointCloud *pointcloud = BKE_pointcloud_add(bmain, m_data_name.c_str());
bke::GeometrySet geometry_set = bke::GeometrySet::from_pointcloud(
pointcloud, bke::GeometryOwnershipType::Editable);
AbcReadGeometryParams read_params{};
read_geometry(geometry_set, sample_sel, read_params, nullptr);
PointCloud *read_pointcloud =
geometry_set.get_component_for_write<bke::PointCloudComponent>().release();
if (read_pointcloud != pointcloud) {
BKE_pointcloud_nomain_to_pointcloud(read_pointcloud, pointcloud);
}
m_object = BKE_object_add_only_object(bmain, OB_POINTCLOUD, m_object_name.c_str());
m_object->data = id_cast<ID *>(pointcloud);
if (m_settings->always_add_cache_reader || has_animations(m_schema, m_settings)) {
addCacheModifier();
}
}
static void read_points(const P3fArraySamplePtr positions, MutableSpan<float3> r_points)
{
for (size_t i = 0; i < positions->size(); i++) {
copy_zup_from_yup(r_points[i], (*positions)[i].getValue());
}
}
static void read_points_sample(const IPointsSchema &schema,
const ISampleSelector &selector,
MutableSpan<float3> r_points)
{
Alembic::AbcGeom::IPointsSchema::Sample sample = schema.getValue(selector);
const P3fArraySamplePtr &positions = sample.getPositions();
read_points(positions, r_points);
}
template<typename TOut, typename TIn> static TOut convert_abc_value(const TIn &in)
{
static_assert(std::is_same_v<TIn, TOut>,
"convert_abc_value needs to be explicitly specialized for each pair of types");
return in;
}
template<> float3 convert_abc_value(const V3f &in)
{
float3 out;
copy_zup_from_yup(out, in.getValue());
return out;
}
template<> ColorGeometry4f convert_abc_value(const C3f &in)
{
return ColorGeometry4f(in[0], in[1], in[2], 1.0f);
}
template<> float2 convert_abc_value(const V2f &in)
{
return in.getValue();
}
template<typename TArrayProperty, typename TWriteValue>
static void read_typed_property_sample(const ICompoundProperty &parent,
const ISampleSelector &selector,
const std::string &name,
bke::MutableAttributeAccessor &attribute_accessor)
{
const TArrayProperty &array_prop = TArrayProperty(parent, name);
if (array_prop) {
using SamplePtr = typename TArrayProperty::sample_ptr_type;
using ValueType = typename TArrayProperty::value_type;
const SamplePtr sample_ptr = array_prop.getValue(selector);
bke::SpanAttributeWriter<TWriteValue> writer =
attribute_accessor.lookup_or_add_for_write_span<TWriteValue>(name, bke::AttrDomain::Point);
MutableSpan<TWriteValue> span = writer.span;
for (const int64_t i : IndexRange(std::min(span.size(), int64_t(sample_ptr->size())))) {
ValueType value = (*sample_ptr)[i];
span[i] = convert_abc_value<TWriteValue>(value);
}
writer.finish();
}
}
static void read_point_arb_geom_params(const IPointsSchema &schema,
const ISampleSelector &selector,
bke::MutableAttributeAccessor &attribute_accessor)
{
const ICompoundProperty prop = schema.getArbGeomParams();
if (!prop.valid()) {
return;
}
for (size_t i = 0; i < prop.getNumProperties(); i++) {
const PropertyHeader header = prop.getPropertyHeader(i);
const PropertyType property_type = header.getPropertyType();
if (property_type != kArrayProperty) {
// currently unsupported
continue;
}
const DataType data_type = header.getDataType();
const MetaData metadata = header.getMetaData();
const std::string interpretation = metadata.get("interpretation");
const std::string name = header.getName();
if (data_type == DataType(kFloat32POD, 3)) {
if (interpretation == C3fTPTraits::interpretation()) {
read_typed_property_sample<IC3fArrayProperty, ColorGeometry4f>(
prop, selector, name, attribute_accessor);
}
else if (interpretation == N3fTPTraits::interpretation()) {
read_typed_property_sample<IN3fArrayProperty, float3>(
prop, selector, name, attribute_accessor);
}
else {
read_typed_property_sample<IV3fArrayProperty, float3>(
prop, selector, name, attribute_accessor);
}
}
else if (data_type == DataType(kFloat32POD, 2)) {
read_typed_property_sample<IV2fArrayProperty, float2>(
prop, selector, name, attribute_accessor);
}
else if (data_type == DataType(kFloat32POD, 1)) {
read_typed_property_sample<IFloatArrayProperty, float>(
prop, selector, name, attribute_accessor);
}
}
}
void AbcPointsReader::read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str)
{
BLI_assert(geometry_set.has_pointcloud());
IPointsSchema::Sample sample;
try {
sample = m_schema.getValue(sample_sel);
}
catch (Alembic::Util::Exception &ex) {
*r_err_str = RPT_("Error reading points sample; more detail on the console");
CLOG_WARN(&LOG,
"Error reading points sample for '%s/%s' at time %f: %s",
m_iobject.getFullName().c_str(),
m_schema.getName().c_str(),
sample_sel.getRequestedTime(),
ex.what());
return;
}
PointCloud *existing_pointcloud = geometry_set.get_pointcloud_for_write();
PointCloud *pointcloud = existing_pointcloud;
const P3fArraySamplePtr &positions = sample.getPositions();
const IFloatGeomParam widths_param = m_schema.getWidthsParam();
FloatArraySamplePtr widths;
if (widths_param.valid()) {
IFloatGeomParam::Sample wsample = widths_param.getExpandedValue(sample_sel);
widths = wsample.getVals();
}
if (!validate::size_fits_in_int(positions->size())) {
CLOG_WARN(&LOG,
"Point cloud too large to import for '%s/%s' at time %f, exceeds max int size",
m_iobject.getFullName().c_str(),
m_schema.getName().c_str(),
sample_sel.getRequestedTime());
return;
}
if (pointcloud->totpoint != positions->size()) {
pointcloud = BKE_pointcloud_new_nomain(positions->size());
}
bke::MutableAttributeAccessor attribute_accessor = pointcloud->attributes_for_write();
MutableSpan<float3> point_positions = pointcloud->positions_for_write();
read_points_sample(m_schema, sample_sel, point_positions);
if (widths) {
MutableSpan<float> point_radii = pointcloud->radius_for_write();
for (const int64_t i : IndexRange(std::min(point_radii.size(), int64_t(widths->size())))) {
point_radii[i] = (*widths)[i] / 2.0f;
}
}
else {
attribute_accessor.remove("radius");
attribute_accessor.add<float>(
"radius", bke::AttrDomain::Point, bke::AttributeInitValue(0.01f));
}
read_point_arb_geom_params(m_schema, sample_sel, attribute_accessor);
if (read_params.velocity_name != "" && read_params.velocity_scale != 0.0f) {
V3fArraySamplePtr velocities = get_velocity_prop(
m_schema, sample_sel, read_params.velocity_name);
if (velocities && pointcloud->totpoint == int(velocities->size())) {
bke::SpanAttributeWriter<float3> velocity_writer =
attribute_accessor.lookup_or_add_for_write_span<float3>("velocity",
bke::AttrDomain::Point);
MutableSpan<float3> point_velocity = velocity_writer.span;
for (const int64_t i :
IndexRange(std::min(point_velocity.size(), int64_t(velocities->size()))))
{
const Imath::V3f &vel_in = (*velocities)[i];
copy_zup_from_yup(point_velocity[i], vel_in.getValue());
point_velocity[i] *= read_params.velocity_scale;
}
velocity_writer.finish();
}
}
geometry_set.replace_pointcloud(pointcloud);
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,36 @@
/* SPDX-FileCopyrightText: 2016 Kévin Dietrich. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/AbcGeom/IPoints.h>
namespace blender::io::alembic {
class AbcPointsReader final : public AbcObjectReader {
Alembic::AbcGeom::IPointsSchema m_schema;
Alembic::AbcGeom::IPointsSchema::Sample m_sample;
public:
AbcPointsReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
void read_geometry(bke::GeometrySet &geometry_set,
const Alembic::Abc::ISampleSelector &sample_sel,
const AbcReadGeometryParams &read_params,
const char **r_err_str) override;
};
} // namespace blender::io::alembic

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_reader_transform.h"
#include "abc_util.h"
#include "DNA_object_types.h"
#include "BLT_translation.hh"
#include "BKE_object.hh"
namespace blender {
using Alembic::Abc::ISampleSelector;
namespace io::alembic {
AbcEmptyReader::AbcEmptyReader(const AbcReaderConstructorArgs &args) : AbcObjectReader(args)
{
/* Empties have no data. It makes the import of Alembic files easier to
* understand when we name the empty after its name in Alembic. */
m_object_name = m_iobject.getName();
Alembic::AbcGeom::IXform xform(m_iobject, Alembic::AbcGeom::kWrapExisting);
m_schema = xform.getSchema();
}
bool AbcEmptyReader::valid() const
{
return m_schema.valid();
}
bool AbcEmptyReader::accepts_object_type(
const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const
{
if (!Alembic::AbcGeom::IXform::matches(alembic_header)) {
*r_err_str = RPT_(
"Object type mismatch, Alembic object path pointed to XForm when importing, but not any "
"more");
return false;
}
if (ob->type != OB_EMPTY) {
*r_err_str = RPT_("Object type mismatch, Alembic object path points to XForm");
return false;
}
return true;
}
void AbcEmptyReader::readObjectData(Main *bmain, const ISampleSelector & /*sample_sel*/)
{
m_object = BKE_object_add_only_object(bmain, OB_EMPTY, m_object_name.c_str());
m_object->data = nullptr;
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/Abc/IObject.h>
#include <Alembic/Abc/ISampleSelector.h>
#include <Alembic/AbcCoreAbstract/ObjectHeader.h>
#include <Alembic/AbcGeom/IXform.h>
namespace blender {
struct Object;
namespace io::alembic {
class AbcEmptyReader final : public AbcObjectReader {
Alembic::AbcGeom::IXformSchema m_schema;
public:
AbcEmptyReader(const AbcReaderConstructorArgs &args);
bool valid() const override;
bool accepts_object_type(const Alembic::AbcCoreAbstract::ObjectHeader &alembic_header,
const Object *const ob,
const char **r_err_str) const override;
void readObjectData(Main *bmain, const Alembic::Abc::ISampleSelector &sample_sel) override;
};
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,210 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "abc_util.h"
#include "abc_reader_camera.h"
#include "abc_reader_curves.h"
#include "abc_reader_mesh.h"
#include "abc_reader_points.h"
#include "abc_reader_transform.h"
#include <Alembic/AbcGeom/ILight.h>
#include <Alembic/AbcGeom/INuPatch.h>
#include <Alembic/AbcMaterial/IMaterial.h>
#include <algorithm>
namespace blender {
using Alembic::Abc::IV3fArrayProperty;
using Alembic::Abc::PropertyHeader;
using Alembic::Abc::V3fArraySamplePtr;
namespace io::alembic {
std::string get_valid_abc_name(const char *name)
{
std::string abc_name(name);
std::replace(abc_name.begin(), abc_name.end(), ' ', '_');
std::replace(abc_name.begin(), abc_name.end(), '.', '_');
std::replace(abc_name.begin(), abc_name.end(), ':', '_');
std::replace(abc_name.begin(), abc_name.end(), '/', '_');
return abc_name;
}
Imath::M44d convert_matrix_datatype(const float mat[4][4])
{
Imath::M44d m;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
m[i][j] = double(mat[i][j]);
}
}
return m;
}
void convert_matrix_datatype(const Imath::M44d &xform, float r_mat[4][4])
{
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
r_mat[i][j] = float(xform[i][j]);
}
}
}
void split(const std::string &s, const char delim, std::vector<std::string> &tokens)
{
tokens.clear();
std::stringstream ss(s);
std::string item;
while (std::getline(ss, item, delim)) {
if (!item.empty()) {
tokens.push_back(item);
}
}
}
bool has_property(const Alembic::Abc::ICompoundProperty &prop, const std::string &name)
{
if (!prop.valid()) {
return false;
}
return prop.getPropertyHeader(name) != nullptr;
}
V3fArraySamplePtr get_velocity_prop(const Alembic::Abc::ICompoundProperty &schema,
const Alembic::AbcGeom::ISampleSelector &selector,
const std::string &name)
{
for (size_t i = 0; i < schema.getNumProperties(); i++) {
const PropertyHeader &header = schema.getPropertyHeader(i);
if (header.isCompound()) {
const Alembic::Abc::ICompoundProperty &prop = Alembic::Abc::ICompoundProperty(
schema, header.getName());
if (has_property(prop, name)) {
/* Header cannot be null here, as its presence is checked via has_property, so it is safe
* to dereference. */
const PropertyHeader *header = prop.getPropertyHeader(name);
if (!IV3fArrayProperty::matches(*header)) {
continue;
}
const IV3fArrayProperty &velocity_prop = IV3fArrayProperty(prop, name, 0);
if (velocity_prop) {
return velocity_prop.getValue(selector);
}
}
}
else if (header.isArray()) {
if (header.getName() == name && IV3fArrayProperty::matches(header)) {
const IV3fArrayProperty &velocity_prop = IV3fArrayProperty(schema, name, 0);
return velocity_prop.getValue(selector);
}
}
}
return V3fArraySamplePtr();
}
using index_time_pair_t = std::pair<Alembic::AbcCoreAbstract::index_t, Alembic::AbcGeom::chrono_t>;
std::optional<SampleInterpolationSettings> get_sample_interpolation_settings(
const Alembic::AbcGeom::ISampleSelector &selector,
const Alembic::AbcCoreAbstract::TimeSamplingPtr &time_sampling,
size_t samples_number)
{
const chrono_t time = selector.getRequestedTime();
samples_number = std::max(samples_number, size_t(1));
index_time_pair_t t0 = time_sampling->getFloorIndex(time, samples_number);
Alembic::AbcCoreAbstract::index_t i0 = t0.first;
if (samples_number == 1 || (fabs(time - t0.second) < 0.0001)) {
return {};
}
index_time_pair_t t1 = time_sampling->getCeilIndex(time, samples_number);
Alembic::AbcCoreAbstract::index_t i1 = t1.first;
if (i0 == i1) {
return {};
}
const double bias = (time - t0.second) / (t1.second - t0.second);
if (fabs(1.0 - bias) < 0.0001) {
return {};
}
return SampleInterpolationSettings{i0, i1, bias};
}
// #define USE_NURBS
AbcObjectReader *create_reader(const AbcReaderConstructorArgs &args)
{
AbcObjectReader *reader = nullptr;
const Alembic::AbcGeom::MetaData &md = args.object.getMetaData();
if (Alembic::AbcGeom::IXform::matches(md)) {
reader = new AbcEmptyReader(args);
}
else if (Alembic::AbcGeom::IPolyMesh::matches(md)) {
reader = new AbcMeshReader(args);
}
else if (Alembic::AbcGeom::ISubD::matches(md)) {
reader = new AbcSubDReader(args);
}
else if (Alembic::AbcGeom::INuPatch::matches(md)) {
#ifdef USE_NURBS
/* TODO(kevin): importing cyclic NURBS from other software crashes
* at the moment. This is due to the fact that NURBS in other
* software have duplicated points which causes buffer overflows in
* Blender. Need to figure out exactly how these points are
* duplicated, in all cases (cyclic U, cyclic V, and cyclic UV).
* Until this is fixed, disabling NURBS reading. */
reader = new AbcNurbsReader(args);
#endif
}
else if (Alembic::AbcGeom::ICamera::matches(md)) {
reader = new AbcCameraReader(args);
}
else if (Alembic::AbcGeom::IPoints::matches(md)) {
reader = new AbcPointsReader(args);
}
else if (Alembic::AbcMaterial::IMaterial::matches(md)) {
/* Pass for now. */
}
else if (Alembic::AbcGeom::ILight::matches(md)) {
/* Pass for now. */
}
else if (Alembic::AbcGeom::IFaceSet::matches(md)) {
/* Pass, those are handled in the mesh reader. */
}
else if (Alembic::AbcGeom::ICurves::matches(md)) {
reader = new AbcCurveReader(args);
}
else {
std::cerr << "Alembic: unknown how to handle objects of schema '" << md.get("schemaObjTitle")
<< "', skipping object '" << args.object.getFullName() << "'" << std::endl;
}
return reader;
}
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,84 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup balembic
*/
#include "abc_reader_object.h"
#include <Alembic/Abc/Foundation.h>
#include <Alembic/Abc/ICompoundProperty.h>
#include <Alembic/Abc/IObject.h>
#include <Alembic/Abc/ISampleSelector.h>
#include <Alembic/Abc/TypedArraySample.h>
#include <Alembic/AbcCoreAbstract/Foundation.h>
#include <Alembic/AbcCoreAbstract/TimeSampling.h>
#include <Alembic/AbcGeom/IXform.h>
#include <optional>
#include <string>
#include <vector>
namespace blender {
using Alembic::Abc::chrono_t;
using Alembic::Abc::V3fArraySamplePtr;
struct ID;
struct Object;
namespace io::alembic {
class AbcObjectReader;
struct AbcReaderConstructorArgs;
std::string get_valid_abc_name(const char *name);
/* Convert from float to Alembic matrix representations. Does NOT convert from Z-up to Y-up. */
Imath::M44d convert_matrix_datatype(const float mat[4][4]);
/* Convert from Alembic to float matrix representations. Does NOT convert from Y-up to Z-up. */
void convert_matrix_datatype(const Imath::M44d &xform, float r_mat[4][4]);
void split(const std::string &s, char delim, std::vector<std::string> &tokens);
template<class TContainer> bool begins_with(const TContainer &input, const TContainer &match)
{
return input.size() >= match.size() && std::equal(match.begin(), match.end(), input.begin());
}
bool has_property(const Alembic::Abc::ICompoundProperty &prop, const std::string &name);
V3fArraySamplePtr get_velocity_prop(const Alembic::Abc::ICompoundProperty &schema,
const Alembic::AbcGeom::ISampleSelector &selector,
const std::string &name);
/**
* The SampleInterpolationSettings struct holds information for interpolating data between two
* samples.
*/
struct SampleInterpolationSettings {
/* Index of the first ("floor") sample. */
Alembic::AbcGeom::index_t index;
/* Index of the second ("ceil") sample. */
Alembic::AbcGeom::index_t ceil_index;
/* Factor to interpolate between the `index` and `ceil_index`. */
double weight;
};
/**
* Check whether the requested time from the \a selector falls between two sampling time from the
* \a time_sampling. If so, returns a #SampleInterpolationSettings with the required data to
* interpolate. If not, returns nothing and we can assume that the requested time falls on a
* specific sampling time of \a time_sampling and no interpolation is necessary.
*/
std::optional<SampleInterpolationSettings> get_sample_interpolation_settings(
const Alembic::AbcGeom::ISampleSelector &selector,
const Alembic::AbcCoreAbstract::TimeSamplingPtr &time_sampling,
size_t samples_number);
AbcObjectReader *create_reader(const AbcReaderConstructorArgs &args);
} // namespace io::alembic
} // namespace blender

View File

@@ -0,0 +1,997 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup balembic
*/
#include "../ABC_alembic.h"
#include "IO_types.hh"
#include <Alembic/AbcGeom/ILight.h>
#include <Alembic/AbcGeom/INuPatch.h>
#include <Alembic/AbcMaterial/IMaterial.h>
#include "abc_keyframing.h"
#include "abc_reader_archive.h"
#include "abc_reader_camera.h"
#include "abc_reader_curves.h"
#include "abc_reader_mesh.h"
#ifdef USE_NURBS
# include "abc_reader_nurbs.h"
#endif
#include "abc_reader_points.h"
#include "abc_reader_transform.h"
#include "abc_util.h"
#include "MEM_guardedalloc.h"
#include "DNA_cachefile_types.h"
#include "DNA_collection_types.h"
#include "DNA_listBase.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "BKE_cachefile.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_object.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "ED_undo.hh"
#include "BLI_compiler_compat.h"
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
#include "BLI_path_utils.hh"
#include "BLI_sort.hh"
#include "BLI_span.hh"
#include "BLI_string.h"
#include "BLI_timeit.hh"
#include "BLT_translation.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"io.alembic"};
using Alembic::Abc::IV3fArrayProperty;
using Alembic::Abc::ObjectHeader;
using Alembic::Abc::PropertyHeader;
using Alembic::Abc::V3fArraySamplePtr;
using Alembic::AbcGeom::ICamera;
using Alembic::AbcGeom::ICurves;
using Alembic::AbcGeom::IFaceSet;
using Alembic::AbcGeom::ILight;
using Alembic::AbcGeom::INuPatch;
using Alembic::AbcGeom::IObject;
using Alembic::AbcGeom::IPoints;
using Alembic::AbcGeom::IPolyMesh;
using Alembic::AbcGeom::IPolyMeshSchema;
using Alembic::AbcGeom::ISampleSelector;
using Alembic::AbcGeom::ISubD;
using Alembic::AbcGeom::IXform;
using Alembic::AbcGeom::kWrapExisting;
using Alembic::AbcGeom::MetaData;
using Alembic::AbcMaterial::IMaterial;
using namespace blender::io::alembic;
struct AlembicArchiveData {
ArchiveReader *archive_reader = nullptr;
ImportSettings *settings = nullptr;
AlembicArchiveData() = default;
~AlembicArchiveData()
{
delete archive_reader;
delete settings;
}
AlembicArchiveData(const AlembicArchiveData &) = delete;
AlembicArchiveData &operator==(const AlembicArchiveData &) = delete;
};
BLI_INLINE AlembicArchiveData *archive_from_handle(CacheArchiveHandle *handle)
{
return reinterpret_cast<AlembicArchiveData *>(handle);
}
BLI_INLINE CacheArchiveHandle *handle_from_archive(AlembicArchiveData *archive)
{
return reinterpret_cast<CacheArchiveHandle *>(archive);
}
/* Add the object's path to list of object paths. No duplication is done, callers are
* responsible for ensuring that only unique paths are added to the list.
*/
static void add_object_path(ListBaseT<CacheObjectPath> *object_paths, const IObject &object)
{
CacheObjectPath *abc_path = MEM_new<CacheObjectPath>("CacheObjectPath");
STRNCPY(abc_path->path, object.getFullName().c_str());
BLI_addtail(object_paths, abc_path);
}
// #define USE_NURBS
/* NOTE: this function is similar to visit_objects below, need to keep them in
* sync. */
static bool gather_objects_paths(const IObject &object, ListBaseT<CacheObjectPath> *object_paths)
{
if (!object.valid()) {
return false;
}
size_t children_claiming_this_object = 0;
size_t num_children = object.getNumChildren();
for (size_t i = 0; i < num_children; i++) {
bool child_claims_this_object = gather_objects_paths(object.getChild(i), object_paths);
children_claiming_this_object += child_claims_this_object ? 1 : 0;
}
const MetaData &md = object.getMetaData();
bool get_path = false;
bool parent_is_part_of_this_object = false;
if (!object.getParent()) {
/* The root itself is not an object we should import. */
}
else if (IXform::matches(md)) {
if (has_property(object.getProperties(), "locator")) {
get_path = true;
}
else {
get_path = children_claiming_this_object == 0;
}
/* Transforms are never "data" for their parent. */
parent_is_part_of_this_object = false;
}
else {
/* These types are "data" for their parent. */
get_path = IPolyMesh::matches(md) || ISubD::matches(md) ||
#ifdef USE_NURBS
INuPatch::matches(md) ||
#endif
ICamera::matches(md) || IPoints::matches(md) || ICurves::matches(md);
parent_is_part_of_this_object = get_path;
}
if (get_path) {
add_object_path(object_paths, object);
}
return parent_is_part_of_this_object;
}
CacheArchiveHandle *ABC_create_handle(const Main *bmain,
const char *filepath,
const CacheFileLayer *layers,
ListBaseT<CacheObjectPath> *object_paths)
{
std::vector<const char *> filepaths;
filepaths.push_back(filepath);
while (layers) {
if ((layers->flag & CACHEFILE_LAYER_HIDDEN) == 0) {
filepaths.push_back(layers->filepath);
}
layers = layers->next;
}
/* We need to reverse the order as overriding archives should come first. */
std::reverse(filepaths.begin(), filepaths.end());
ArchiveReader *archive = ArchiveReader::get(bmain, filepaths);
if (!archive || !archive->valid()) {
delete archive;
return nullptr;
}
if (object_paths) {
gather_objects_paths(archive->getTop(), object_paths);
}
AlembicArchiveData *archive_data = new AlembicArchiveData();
archive_data->archive_reader = archive;
archive_data->settings = new ImportSettings();
return handle_from_archive(archive_data);
}
void ABC_free_handle(CacheArchiveHandle *handle)
{
delete archive_from_handle(handle);
}
int ABC_get_version()
{
return ALEMBIC_LIBRARY_VERSION;
}
static void find_iobject(const IObject &object, IObject &ret, const std::string &path)
{
if (!object.valid()) {
return;
}
std::vector<std::string> tokens;
split(path, '/', tokens);
IObject tmp = object;
std::vector<std::string>::iterator iter;
for (iter = tokens.begin(); iter != tokens.end(); ++iter) {
IObject child = tmp.getChild(*iter);
tmp = child;
}
ret = tmp;
}
/* ********************** Import file ********************** */
/**
* Generates an AbcObjectReader for this Alembic object and its children.
*
* \param object: The Alembic IObject to visit.
* \param readers: The created AbcObjectReader * will be appended to this vector.
* \param settings: Import settings, not used directly but passed to the
* AbcObjectReader subclass constructors.
* \param r_assign_as_parent: Return parameter, contains a list of reader
* pointers, whose parent pointer should still be set.
* This is filled when this call to visit_object() didn't create
* a reader that should be the parent.
* \return A pair of boolean and reader pointer. The boolean indicates whether
* this IObject claims its parent as part of the same object
* (for example an IPolyMesh object would claim its parent, as the mesh
* is interpreted as the object's data, and the parent IXform as its
* Blender object). The pointer is the AbcObjectReader that represents
* the IObject parameter.
*
* NOTE: this function is similar to gather_object_paths above, need to keep
* them in sync. */
static std::pair<bool, AbcObjectReader *> visit_object(
const IObject &object,
AbcObjectReader::ptr_vector &readers,
ImportSettings &settings,
AbcObjectReader::ptr_vector &r_assign_as_parent)
{
const std::string &full_name = object.getFullName();
if (!object.valid()) {
std::cerr << " - " << full_name << ": object is invalid, skipping it and all its children.\n";
return std::make_pair(false, static_cast<AbcObjectReader *>(nullptr));
}
/* The interpretation of data by the children determine the role of this
* object. This is especially important for Xform objects, as they can be
* either part of a Blender object or a Blender object (Empty) themselves.
*/
size_t children_claiming_this_object = 0;
size_t num_children = object.getNumChildren();
AbcObjectReader::ptr_vector claiming_child_readers;
AbcObjectReader::ptr_vector nonclaiming_child_readers;
AbcObjectReader::ptr_vector assign_as_parent;
for (size_t i = 0; i < num_children; i++) {
const IObject ichild = object.getChild(i);
/* TODO: When we only support C++11, use std::tie() instead. */
std::pair<bool, AbcObjectReader *> child_result;
child_result = visit_object(ichild, readers, settings, assign_as_parent);
bool child_claims_this_object = child_result.first;
AbcObjectReader *child_reader = child_result.second;
if (child_reader == nullptr) {
BLI_assert(!child_claims_this_object);
}
else {
if (child_claims_this_object) {
claiming_child_readers.push_back(child_reader);
}
else {
nonclaiming_child_readers.push_back(child_reader);
}
}
children_claiming_this_object += child_claims_this_object ? 1 : 0;
}
BLI_assert(children_claiming_this_object == claiming_child_readers.size());
UNUSED_VARS_NDEBUG(children_claiming_this_object);
AbcObjectReader *reader = nullptr;
const MetaData &md = object.getMetaData();
bool parent_is_part_of_this_object = false;
const AbcReaderConstructorArgs args = create_reader_constructor_args(object, settings);
if (!object.getParent()) {
/* The root itself is not an object we should import. */
}
else if (IXform::matches(md)) {
bool create_empty;
/* An xform can either be a Blender Object (if it contains a mesh, for
* example), but it can also be an Empty. Its correct translation to
* Blender's data model depends on its children. */
/* Check whether or not this object is a Maya locator, which is
* similar to empties used as parent object in Blender. */
if (has_property(object.getProperties(), "locator")) {
create_empty = true;
}
else {
create_empty = claiming_child_readers.empty();
}
if (create_empty) {
reader = new AbcEmptyReader(args);
}
}
else if (IPolyMesh::matches(md)) {
reader = new AbcMeshReader(args);
parent_is_part_of_this_object = true;
}
else if (ISubD::matches(md)) {
reader = new AbcSubDReader(args);
parent_is_part_of_this_object = true;
}
else if (INuPatch::matches(md)) {
#ifdef USE_NURBS
/* TODO(kevin): importing cyclic NURBS from other software crashes
* at the moment. This is due to the fact that NURBS in other
* software have duplicated points which causes buffer overflows in
* Blender. Need to figure out exactly how these points are
* duplicated, in all cases (cyclic U, cyclic V, and cyclic UV).
* Until this is fixed, disabling NURBS reading. */
reader = new AbcNurbsReader(args);
parent_is_part_of_this_object = true;
#endif
}
else if (ICamera::matches(md)) {
reader = new AbcCameraReader(args);
parent_is_part_of_this_object = true;
}
else if (IPoints::matches(md)) {
reader = new AbcPointsReader(args);
parent_is_part_of_this_object = true;
}
else if (IMaterial::matches(md)) {
/* Pass for now. */
}
else if (ILight::matches(md)) {
/* Pass for now. */
}
else if (IFaceSet::matches(md)) {
/* Pass, those are handled in the mesh reader. */
}
else if (ICurves::matches(md)) {
reader = new AbcCurveReader(args);
parent_is_part_of_this_object = true;
}
else {
std::cerr << "Alembic object " << full_name << " is of unsupported schema type '"
<< object.getMetaData().get("schemaObjTitle") << "'" << std::endl;
}
if (reader) {
/* We have created a reader, which should imply that this object is
* not claimed as part of any child Alembic object. */
BLI_assert(claiming_child_readers.empty());
readers.push_back(reader);
reader->incref();
add_object_path(&settings.cache_file->object_paths, object);
/* We can now assign this reader as parent for our children. */
if (nonclaiming_child_readers.size() + assign_as_parent.size() > 0) {
for (AbcObjectReader *child_reader : nonclaiming_child_readers) {
child_reader->parent_reader = reader;
}
for (AbcObjectReader *child_reader : assign_as_parent) {
child_reader->parent_reader = reader;
}
}
}
else if (object.getParent()) {
if (!claiming_child_readers.empty()) {
/* The first claiming child will serve just fine as parent to
* our non-claiming children. Since all claiming children share
* the same XForm, it doesn't really matter which one we pick. */
AbcObjectReader *claiming_child = claiming_child_readers[0];
for (AbcObjectReader *child_reader : nonclaiming_child_readers) {
child_reader->parent_reader = claiming_child;
}
for (AbcObjectReader *child_reader : assign_as_parent) {
child_reader->parent_reader = claiming_child;
}
/* Claiming children should have our parent set as their parent. */
for (AbcObjectReader *child_reader : claiming_child_readers) {
r_assign_as_parent.push_back(child_reader);
}
}
else {
/* This object isn't claimed by any child, and didn't produce
* a reader. Odd situation, could be the top Alembic object, or
* an unsupported Alembic schema. Delegate to our parent. */
for (AbcObjectReader *child_reader : claiming_child_readers) {
r_assign_as_parent.push_back(child_reader);
}
for (AbcObjectReader *child_reader : nonclaiming_child_readers) {
r_assign_as_parent.push_back(child_reader);
}
for (AbcObjectReader *child_reader : assign_as_parent) {
r_assign_as_parent.push_back(child_reader);
}
}
}
return std::make_pair(parent_is_part_of_this_object, reader);
}
enum {
ABC_NO_ERROR = 0,
ABC_ARCHIVE_FAIL,
};
struct ImportJobData {
bContext *C;
Main *bmain;
Scene *scene;
ViewLayer *view_layer;
wmWindowManager *wm;
ImportSettings settings;
Vector<ArchiveReader *> archives;
Vector<AbcObjectReader *> readers;
Vector<std::string> paths;
/** Min time read from file import. */
chrono_t min_time = std::numeric_limits<chrono_t>::max();
/** Max time read from file import. */
chrono_t max_time = -std::numeric_limits<chrono_t>::max();
bool *stop;
bool *do_update;
float *progress;
char error_code;
bool was_cancelled;
bool import_ok;
bool is_background_job;
timeit::TimePoint start_time;
};
static void report_job_duration(const ImportJobData *data)
{
timeit::Nanoseconds duration = timeit::Clock::now() - data->start_time;
std::cout << "Alembic import took ";
timeit::print_duration(duration);
std::cout << '\n';
}
static void sort_readers(MutableSpan<AbcObjectReader *> readers)
{
parallel_sort(
readers.begin(), readers.end(), [](const AbcObjectReader *a, const AbcObjectReader *b) {
const char *na = a->name().c_str();
const char *nb = b->name().c_str();
return BLI_strcasecmp(na, nb) < 0;
});
}
static void import_file(ImportJobData *data, const char *filepath, float progress_factor)
{
timeit::TimePoint start_time = timeit::Clock::now();
ArchiveReader *archive = ArchiveReader::get(data->bmain, {filepath});
if (!archive || !archive->valid()) {
data->error_code = ABC_ARCHIVE_FAIL;
delete archive;
return;
}
CacheFile *cache_file = static_cast<CacheFile *>(
BKE_cachefile_add(data->bmain, BLI_path_basename(filepath)));
/* Decrement the ID ref-count because it is going to be incremented for each
* modifier and constraint that it will be attached to, so since currently
* it is not used by anyone, its use count will be off by one. */
id_us_min(&cache_file->id);
cache_file->is_sequence = data->settings.is_sequence;
cache_file->scale = data->settings.scale;
STRNCPY(cache_file->filepath, filepath);
data->archives.append(archive);
data->settings.cache_file = cache_file;
data->settings.blender_archive_version_prior_44 = archive->is_blender_archive_version_prior_44();
*data->do_update = true;
*data->progress += 0.05f * progress_factor;
/* Parse Alembic Archive. */
AbcObjectReader::ptr_vector assign_as_parent;
std::vector<AbcObjectReader *> readers{};
visit_object(archive->getTop(), readers, data->settings, assign_as_parent);
/* There shouldn't be any orphans. */
BLI_assert(assign_as_parent.empty());
if (G.is_break) {
data->was_cancelled = true;
data->readers.extend(readers);
return;
}
*data->do_update = true;
*data->progress += 0.05f * progress_factor;
/* Create objects and set scene frame range. */
/* Sort readers by name: when creating a lot of objects in Blender,
* it is much faster if the order is sorted by name. */
sort_readers(readers);
data->readers.extend(readers);
const float size = float(readers.size());
ISampleSelector sample_sel(0.0);
std::vector<AbcObjectReader *>::iterator iter;
const float read_object_progress_step = (0.6f / size) * progress_factor;
for (iter = readers.begin(); iter != readers.end(); ++iter) {
AbcObjectReader *reader = *iter;
if (reader->valid()) {
reader->readObjectData(data->bmain, sample_sel);
reader->readVisibility();
}
else {
std::cerr << "Object " << reader->name() << " in Alembic file " << filepath
<< " is invalid.\n";
}
*data->progress += read_object_progress_step;
*data->do_update = true;
if (G.is_break) {
data->was_cancelled = true;
return;
}
}
const TimeInfo time_info = archive->getTimeInfo();
if (time_info.is_valid()) {
data->min_time = std::min(data->min_time, time_info.min_time);
data->max_time = std::max(data->max_time, time_info.max_time);
Vector<std::unique_ptr<FCurveCreationHelper>> keyframing_helpers;
for (iter = readers.begin(); iter != readers.end(); ++iter) {
AbcObjectReader *reader = *iter;
if (reader->valid()) {
reader->getKeyFramingHelpers(keyframing_helpers);
}
}
create_keyframes(data->bmain, data->scene, keyframing_helpers, time_info);
}
/* Setup parenthood. */
for (iter = readers.begin(); iter != readers.end(); ++iter) {
const AbcObjectReader *reader = *iter;
const AbcObjectReader *parent_reader = reader->parent_reader;
Object *ob = reader->object();
if (parent_reader == nullptr || !reader->inherits_xform()) {
ob->parent = nullptr;
}
else {
ob->parent = parent_reader->object();
}
}
/* Setup transformations and constraints. */
const float setup_object_transform_progress_step = (0.3f / size) * progress_factor;
for (iter = readers.begin(); iter != readers.end(); ++iter) {
AbcObjectReader *reader = *iter;
reader->setupObjectTransform(0.0);
*data->progress += setup_object_transform_progress_step;
*data->do_update = true;
if (G.is_break) {
data->was_cancelled = true;
return;
}
}
timeit::Nanoseconds duration = timeit::Clock::now() - start_time;
std::cout << "Alembic import " << filepath << " took ";
timeit::print_duration(duration);
std::cout << '\n';
}
static void set_frame_range(ImportJobData *data)
{
if (!data->settings.set_frame_range) {
return;
}
Scene *scene = data->scene;
if (data->settings.is_sequence) {
scene->r.sfra = data->settings.sequence_min_frame;
scene->r.efra = data->settings.sequence_max_frame;
scene->r.cfra = scene->r.sfra;
}
else if (data->min_time < data->max_time) {
scene->r.sfra = int(round(data->min_time * scene->frames_per_second()));
scene->r.efra = int(round(data->max_time * scene->frames_per_second()));
scene->r.cfra = scene->r.sfra;
}
}
static void import_startjob(void *user_data, wmJobWorkerStatus *worker_status)
{
ImportJobData *data = static_cast<ImportJobData *>(user_data);
data->stop = &worker_status->stop;
data->do_update = &worker_status->do_update;
data->progress = &worker_status->progress;
data->start_time = timeit::Clock::now();
WM_locked_interface_set(data->wm, true);
float file_progress_factor = 1.0f / float(data->paths.size());
for (int idx : data->paths.index_range()) {
import_file(data, data->paths[idx].c_str(), file_progress_factor);
if (G.is_break || data->was_cancelled) {
data->was_cancelled = true;
return;
}
worker_status->progress = float(idx + 1) * file_progress_factor;
}
set_frame_range(data);
}
static void import_endjob(void *user_data)
{
ImportJobData *data = static_cast<ImportJobData *>(user_data);
/* Delete objects on cancellation. */
if (data->was_cancelled) {
for (AbcObjectReader *reader : data->readers) {
Object *ob = reader->object();
/* It's possible that cancellation occurred between the creation of
* the reader and the creation of the Blender object. */
if (ob == nullptr) {
continue;
}
BKE_id_free_us(data->bmain, ob);
}
}
else {
const Main *bmain = data->bmain;
const Scene *scene = data->scene;
ViewLayer *view_layer = data->view_layer;
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
if (!ID_IS_EDITABLE(lc->collection)) {
WM_global_report(RPT_WARNING,
"Could not find an editable collection in current scene, imported data "
"will not be instantiated");
}
for (AbcObjectReader *reader : data->readers) {
Object *ob = reader->object();
BKE_collection_object_add(data->bmain, lc->collection, ob);
}
/* Sync and do the view layer operations. */
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
bool has_instantiated_object = false;
bool has_uninstantiated_object = false;
for (AbcObjectReader *reader : data->readers) {
Object *ob = reader->object();
Base *base = BKE_view_layer_base_find(view_layer, ob);
if (!base) {
/* Object not instantiated in current viewlayer. */
has_uninstantiated_object = true;
continue;
}
has_instantiated_object = true;
/* TODO: is setting active needed? */
BKE_view_layer_base_select_and_set_active(view_layer, base);
/* If the object is hidden, we set the base as hidden instead so that hide/unhide shortcuts
* work and the outliner shows the right value. We also unset the flag on the object as users
* are more likely to interact with viewport visibility from the outliner or shortcuts than
* in the object visibility panel.
* We don't do this if keyframes are added for the visibility, otherwise the objects won't
* show up in the viewport and we cannot transfer the keyframes to the base. */
if ((ob->visibility_flag & OB_HIDE_VIEWPORT) != 0 && !reader->has_visibility_keyframes()) {
base->flag |= BASE_HIDDEN;
ob->visibility_flag &= ~OB_HIDE_VIEWPORT;
/* Needed for the shortcut (ALT+H) to work. */
BKE_base_eval_flags(base);
}
DEG_id_tag_update(&lc->collection->id, ID_RECALC_SYNC_TO_EVAL);
DEG_id_tag_update_ex(data->bmain,
&ob->id,
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
ID_RECALC_BASE_FLAGS);
}
if (has_instantiated_object && has_uninstantiated_object) {
CLOG_ERROR(&LOG, "Some imported objects were not instantiated, while others were");
}
DEG_id_tag_update(&data->scene->id, ID_RECALC_BASE_FLAGS);
DEG_relations_tag_update(data->bmain);
if (data->is_background_job) {
/* Blender already returned from the import operator, so we need to store our own extra undo
* step. */
ED_undo_push(data->C, "Alembic Import Finished");
}
}
for (AbcObjectReader *reader : data->readers) {
reader->decref();
if (reader->refcount() == 0) {
delete reader;
}
}
WM_locked_interface_set(data->wm, false);
switch (data->error_code) {
default:
case ABC_NO_ERROR:
data->import_ok = !data->was_cancelled;
break;
case ABC_ARCHIVE_FAIL:
WM_global_report(RPT_ERROR,
"Could not open Alembic archive for reading, see console for detail");
break;
}
WM_main_add_notifier(NC_ID | NA_ADDED, nullptr);
report_job_duration(data);
}
static void import_freejob(void *user_data)
{
ImportJobData *data = static_cast<ImportJobData *>(user_data);
for (ArchiveReader *archive : data->archives) {
delete archive;
}
delete data;
}
bool ABC_import(bContext *C, const AlembicImportParams *params, bool as_background_job)
{
/* Using new here since MEM_* functions do not call constructor to properly initialize data. */
ImportJobData *job = new ImportJobData();
job->C = C;
job->bmain = CTX_data_main(C);
job->scene = CTX_data_scene(C);
job->view_layer = CTX_data_view_layer(C);
job->wm = CTX_wm_manager(C);
job->import_ok = false;
job->paths = params->paths;
job->settings.scale = params->global_scale;
job->settings.is_sequence = params->is_sequence;
job->settings.set_frame_range = params->set_frame_range;
job->settings.sequence_min_frame = params->sequence_min_frame;
job->settings.sequence_max_frame = params->sequence_max_frame;
job->settings.validate_meshes = params->validate_meshes;
job->settings.always_add_cache_reader = params->always_add_cache_reader;
job->error_code = ABC_NO_ERROR;
job->was_cancelled = false;
job->is_background_job = as_background_job;
G.is_break = false;
bool import_ok = false;
if (as_background_job) {
wmJob *wm_job = WM_jobs_get(CTX_wm_manager(C),
CTX_wm_window(C),
job->scene,
"Importing Alembic...",
WM_JOB_PROGRESS,
WM_JOB_TYPE_ALEMBIC_IMPORT);
/* setup job */
WM_jobs_customdata_set(wm_job, job, import_freejob);
WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_FRAME, NC_SCENE | ND_FRAME);
WM_jobs_callbacks(wm_job, import_startjob, nullptr, nullptr, import_endjob);
WM_jobs_start(CTX_wm_manager(C), wm_job);
}
else {
wmJobWorkerStatus worker_status = {};
import_startjob(job, &worker_status);
import_endjob(job);
import_ok = job->import_ok;
import_freejob(job);
}
return import_ok;
}
/* ************************************************************************** */
void ABC_get_transform(CacheReader *reader, float r_mat_world[4][4], double time, float scale)
{
if (!reader) {
return;
}
AbcObjectReader *abc_reader = reinterpret_cast<AbcObjectReader *>(reader);
bool is_constant = false;
/* Convert from the local matrix we obtain from Alembic to world coordinates
* for Blender. This conversion is done here rather than by Blender due to
* work around the non-standard interpretation of CONSTRAINT_SPACE_LOCAL in
* BKE_constraint_mat_convertspace(). */
Object *object = abc_reader->object();
if (object->parent == nullptr) {
/* No parent, so local space is the same as world space. */
abc_reader->read_matrix(r_mat_world, time, scale, is_constant);
return;
}
float mat_parent[4][4];
BKE_object_get_parent_matrix(object, object->parent, mat_parent);
float mat_local[4][4];
abc_reader->read_matrix(mat_local, time, scale, is_constant);
mul_m4_m4m4(r_mat_world, mat_parent, object->parentinv);
mul_m4_m4m4(r_mat_world, r_mat_world, mat_local);
}
/* ************************************************************************** */
static AbcObjectReader *get_abc_reader(CacheReader *reader, Object *ob, const char **r_err_str)
{
AbcObjectReader *abc_reader = reinterpret_cast<AbcObjectReader *>(reader);
IObject iobject = abc_reader->iobject();
if (!iobject.valid()) {
*r_err_str = RPT_("Invalid object: verify object path");
return nullptr;
}
const ObjectHeader &header = iobject.getHeader();
if (!abc_reader->accepts_object_type(header, ob, r_err_str)) {
/* r_err_str is set by acceptsObjectType() */
return nullptr;
}
return abc_reader;
}
static ISampleSelector sample_selector_for_time(chrono_t time)
{
/* kFloorIndex is used to be compatible with non-interpolating
* properties; they use the floor. */
return ISampleSelector(time, ISampleSelector::kFloorIndex);
}
void ABC_read_geometry(CacheReader *reader,
Object *ob,
bke::GeometrySet &geometry_set,
const ABCReadParams *params,
const char **r_err_str)
{
AbcObjectReader *abc_reader = get_abc_reader(reader, ob, r_err_str);
if (abc_reader == nullptr) {
return;
}
ISampleSelector sample_sel = sample_selector_for_time(params->time);
AbcReadGeometryParams read_params;
read_params.read_flag = params->read_flags;
read_params.velocity_name = params->velocity_name ? params->velocity_name : "";
read_params.velocity_scale = params->velocity_scale;
abc_reader->read_geometry(geometry_set, sample_sel, read_params, r_err_str);
}
bool ABC_mesh_topology_changed(CacheReader *reader,
Object *ob,
const Mesh *existing_mesh,
const double time,
const char **r_err_str)
{
AbcObjectReader *abc_reader = get_abc_reader(reader, ob, r_err_str);
if (abc_reader == nullptr) {
return false;
}
ISampleSelector sample_sel = sample_selector_for_time(time);
return abc_reader->topology_changed(existing_mesh, sample_sel);
}
/* ************************************************************************** */
void ABC_CacheReader_free(CacheReader *reader)
{
AbcObjectReader *abc_reader = reinterpret_cast<AbcObjectReader *>(reader);
abc_reader->decref();
if (abc_reader->refcount() == 0) {
delete abc_reader;
}
}
CacheReader *CacheReader_open_alembic_object(CacheArchiveHandle *handle,
CacheReader *reader,
Object *object,
const char *object_path,
const bool is_sequence)
{
if (object_path[0] == '\0') {
return reader;
}
AlembicArchiveData *archive_data = archive_from_handle(handle);
if (!archive_data) {
return reader;
}
ArchiveReader *archive = archive_data->archive_reader;
if (!archive || !archive->valid()) {
return reader;
}
IObject iobject;
find_iobject(archive->getTop(), iobject, object_path);
if (reader) {
ABC_CacheReader_free(reader);
}
archive_data->settings->is_sequence = is_sequence;
archive_data->settings->blender_archive_version_prior_44 =
archive->is_blender_archive_version_prior_44();
const AbcReaderConstructorArgs args = create_reader_constructor_args(iobject,
*archive_data->settings);
AbcObjectReader *abc_reader = create_reader(args);
if (abc_reader == nullptr) {
/* This object is not supported */
return nullptr;
}
abc_reader->object(object);
abc_reader->incref();
return reinterpret_cast<CacheReader *>(abc_reader);
}
} // namespace blender

View File

@@ -0,0 +1,162 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
/* Keep first since `BLI_utildefines.h` defines `AT` which conflicts with STL. */
#include "exporter/abc_archive.h"
#include "BKE_main.hh"
#include "BLI_fileops.h"
#include "BLI_string.h"
#include "DNA_scene_types.h"
#include "DEG_depsgraph.hh"
namespace blender::io::alembic {
class AlembicExportTest : public testing::Test {
protected:
ABCArchive *abc_archive;
AlembicExportParams params;
Scene scene;
Depsgraph *depsgraph;
Main *bmain;
void SetUp() override
{
abc_archive = nullptr;
/* Fake a 25 FPS scene with a nonzero base (because that's sometimes forgotten) */
scene.r.frs_sec = 50;
scene.r.frs_sec_base = 2;
STRNCPY(scene.id.name, "SCTestScene");
bmain = BKE_main_new();
DEG_register_node_types();
/* TODO(sergey): Pass scene layer somehow? */
ViewLayer *view_layer = (ViewLayer *)scene.view_layers.first;
depsgraph = DEG_graph_new(bmain, &scene, view_layer, DAG_EVAL_RENDER);
}
void TearDown() override
{
BKE_main_free(bmain);
DEG_graph_free(depsgraph);
DEG_free_node_types();
deleteArchive();
}
/* Call after setting up the parameters. */
void createArchive()
{
if (abc_archive != nullptr) {
deleteArchive();
}
abc_archive = new ABCArchive(bmain, &scene, params, "somefile.abc");
}
void deleteArchive()
{
delete abc_archive;
if (BLI_exists("somefile.abc")) {
BLI_delete("somefile.abc", false, false);
}
abc_archive = nullptr;
}
};
TEST_F(AlembicExportTest, TimeSamplesFullShutterUniform)
{
/* Test 5 samples per frame, for 2 frames. */
params.shutter_open = 0.0;
params.shutter_close = 1.0;
params.frame_start = 31.0;
params.frame_end = 32.0;
params.frame_samples_xform = params.frame_samples_shape = 5;
createArchive();
std::vector<double> frames(abc_archive->frames_begin(), abc_archive->frames_end());
EXPECT_EQ(10, frames.size());
EXPECT_NEAR(31.0, frames[0], 1e-5);
EXPECT_NEAR(31.2, frames[1], 1e-5);
EXPECT_NEAR(31.4, frames[2], 1e-5);
EXPECT_NEAR(31.6, frames[3], 1e-5);
EXPECT_NEAR(31.8, frames[4], 1e-5);
EXPECT_NEAR(32.0, frames[5], 1e-5);
EXPECT_NEAR(32.2, frames[6], 1e-5);
EXPECT_NEAR(32.4, frames[7], 1e-5);
EXPECT_NEAR(32.6, frames[8], 1e-5);
EXPECT_NEAR(32.8, frames[9], 1e-5);
for (double frame : frames) {
EXPECT_TRUE(abc_archive->is_xform_frame(frame));
EXPECT_TRUE(abc_archive->is_shape_frame(frame));
}
}
TEST_F(AlembicExportTest, TimeSamplesFullShutterDifferent)
{
/* Test 3 samples per frame for transforms, and 2 per frame for shapes, for 2 frames. */
params.shutter_open = 0.0;
params.shutter_close = 1.0;
params.frame_start = 31.0;
params.frame_end = 32.0;
params.frame_samples_xform = 3;
params.frame_samples_shape = 2;
createArchive();
std::vector<double> frames(abc_archive->frames_begin(), abc_archive->frames_end());
EXPECT_EQ(8, frames.size());
EXPECT_NEAR(31.0, frames[0], 1e-5); /* transform + shape */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[0]));
EXPECT_TRUE(abc_archive->is_shape_frame(frames[0]));
EXPECT_NEAR(31.33333, frames[1], 1e-5); /* transform */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[1]));
EXPECT_FALSE(abc_archive->is_shape_frame(frames[1]));
EXPECT_NEAR(31.5, frames[2], 1e-5); /* shape */
EXPECT_FALSE(abc_archive->is_xform_frame(frames[2]));
EXPECT_TRUE(abc_archive->is_shape_frame(frames[2]));
EXPECT_NEAR(31.66666, frames[3], 1e-5); /* transform */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[3]));
EXPECT_FALSE(abc_archive->is_shape_frame(frames[3]));
EXPECT_NEAR(32.0, frames[4], 1e-5); /* transform + shape */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[4]));
EXPECT_TRUE(abc_archive->is_shape_frame(frames[4]));
EXPECT_NEAR(32.33333, frames[5], 1e-5); /* transform */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[5]));
EXPECT_FALSE(abc_archive->is_shape_frame(frames[5]));
EXPECT_NEAR(32.5, frames[6], 1e-5); /* shape */
EXPECT_FALSE(abc_archive->is_xform_frame(frames[6]));
EXPECT_TRUE(abc_archive->is_shape_frame(frames[6]));
EXPECT_NEAR(32.66666, frames[7], 1e-5); /* transform */
EXPECT_TRUE(abc_archive->is_xform_frame(frames[7]));
EXPECT_FALSE(abc_archive->is_shape_frame(frames[7]));
}
TEST_F(AlembicExportTest, TimeSamples180degShutter)
{
/* Test 5 samples per frame, for 2 frames. */
params.shutter_open = -0.25;
params.shutter_close = 0.25;
params.frame_start = 31.0;
params.frame_end = 32.0;
params.frame_samples_xform = params.frame_samples_shape = 5;
createArchive();
std::vector<double> frames(abc_archive->frames_begin(), abc_archive->frames_end());
EXPECT_EQ(10, frames.size());
EXPECT_NEAR(31 - 0.25, frames[0], 1e-5);
EXPECT_NEAR(31 - 0.15, frames[1], 1e-5);
EXPECT_NEAR(31 - 0.05, frames[2], 1e-5);
EXPECT_NEAR(31 + 0.05, frames[3], 1e-5);
EXPECT_NEAR(31 + 0.15, frames[4], 1e-5);
EXPECT_NEAR(32 - 0.25, frames[5], 1e-5);
EXPECT_NEAR(32 - 0.15, frames[6], 1e-5);
EXPECT_NEAR(32 - 0.05, frames[7], 1e-5);
EXPECT_NEAR(32 + 0.05, frames[8], 1e-5);
EXPECT_NEAR(32 + 0.15, frames[9], 1e-5);
}
} // namespace blender::io::alembic

View File

@@ -0,0 +1,292 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
/* Keep first since `BLI_utildefines.h` defines `AT` which conflicts with STL. */
#include "intern/abc_axis_conversion.h"
#include "BLI_math_base.h"
#include "BLI_math_matrix.h"
namespace blender::io::alembic {
TEST(abc_matrix, CreateRotationMatrixY_YfromZ)
{
/* Input variables */
float rot_x_mat[3][3];
float rot_y_mat[3][3];
float rot_z_mat[3][3];
float euler[3] = {0.0f, M_PI_4, 0.0f};
/* Construct expected matrices */
float unit[3][3];
float rot_z_min_quart_pi[3][3]; /* rotation of -pi/4 radians over z-axis */
unit_m3(unit);
unit_m3(rot_z_min_quart_pi);
rot_z_min_quart_pi[0][0] = M_SQRT1_2;
rot_z_min_quart_pi[0][1] = -M_SQRT1_2;
rot_z_min_quart_pi[1][0] = M_SQRT1_2;
rot_z_min_quart_pi[1][1] = M_SQRT1_2;
/* Run tests */
create_swapped_rotation_matrix(rot_x_mat, rot_y_mat, rot_z_mat, euler, ABC_YUP_FROM_ZUP);
EXPECT_M3_NEAR(rot_x_mat, unit, 1e-5f);
EXPECT_M3_NEAR(rot_y_mat, unit, 1e-5f);
EXPECT_M3_NEAR(rot_z_mat, rot_z_min_quart_pi, 1e-5f);
}
TEST(abc_matrix, CreateRotationMatrixZ_YfromZ)
{
/* Input variables */
float rot_x_mat[3][3];
float rot_y_mat[3][3];
float rot_z_mat[3][3];
float euler[3] = {0.0f, 0.0f, M_PI_4};
/* Construct expected matrices */
float unit[3][3];
float rot_y_quart_pi[3][3]; /* rotation of pi/4 radians over y-axis */
unit_m3(unit);
unit_m3(rot_y_quart_pi);
rot_y_quart_pi[0][0] = M_SQRT1_2;
rot_y_quart_pi[0][2] = -M_SQRT1_2;
rot_y_quart_pi[2][0] = M_SQRT1_2;
rot_y_quart_pi[2][2] = M_SQRT1_2;
/* Run tests */
create_swapped_rotation_matrix(rot_x_mat, rot_y_mat, rot_z_mat, euler, ABC_YUP_FROM_ZUP);
EXPECT_M3_NEAR(rot_x_mat, unit, 1e-5f);
EXPECT_M3_NEAR(rot_y_mat, rot_y_quart_pi, 1e-5f);
EXPECT_M3_NEAR(rot_z_mat, unit, 1e-5f);
}
TEST(abc_matrix, CreateRotationMatrixXYZ_YfromZ)
{
/* Input variables */
float rot_x_mat[3][3];
float rot_y_mat[3][3];
float rot_z_mat[3][3];
/* in degrees: X=10, Y=20, Z=30 */
float euler[3] = {0.17453292012214f, 0.34906581044197f, 0.52359879016876f};
/* Construct expected matrices */
float rot_x_p10[3][3]; /* rotation of +10 degrees over x-axis */
float rot_y_p30[3][3]; /* rotation of +30 degrees over y-axis */
float rot_z_m20[3][3]; /* rotation of -20 degrees over z-axis */
unit_m3(rot_x_p10);
rot_x_p10[1][1] = 0.9848077297210693f;
rot_x_p10[1][2] = 0.1736481785774231f;
rot_x_p10[2][1] = -0.1736481785774231f;
rot_x_p10[2][2] = 0.9848077297210693f;
unit_m3(rot_y_p30);
rot_y_p30[0][0] = 0.8660253882408142f;
rot_y_p30[0][2] = -0.5f;
rot_y_p30[2][0] = 0.5f;
rot_y_p30[2][2] = 0.8660253882408142f;
unit_m3(rot_z_m20);
rot_z_m20[0][0] = 0.9396926164627075f;
rot_z_m20[0][1] = -0.3420201241970062f;
rot_z_m20[1][0] = 0.3420201241970062f;
rot_z_m20[1][1] = 0.9396926164627075f;
/* Run tests */
create_swapped_rotation_matrix(rot_x_mat, rot_y_mat, rot_z_mat, euler, ABC_YUP_FROM_ZUP);
EXPECT_M3_NEAR(rot_x_mat, rot_x_p10, 1e-5f);
EXPECT_M3_NEAR(rot_y_mat, rot_y_p30, 1e-5f);
EXPECT_M3_NEAR(rot_z_mat, rot_z_m20, 1e-5f);
}
TEST(abc_matrix, CreateRotationMatrixXYZ_ZfromY)
{
/* Input variables */
float rot_x_mat[3][3];
float rot_y_mat[3][3];
float rot_z_mat[3][3];
/* in degrees: X=10, Y=20, Z=30 */
float euler[3] = {0.1745329201221466f, 0.3490658104419708f, 0.5235987901687622f};
/* Construct expected matrices */
float rot_x_p10[3][3]; /* rotation of +10 degrees over x-axis */
float rot_y_m30[3][3]; /* rotation of -30 degrees over y-axis */
float rot_z_p20[3][3]; /* rotation of +20 degrees over z-axis */
unit_m3(rot_x_p10);
rot_x_p10[1][1] = 0.9848077297210693f;
rot_x_p10[1][2] = 0.1736481785774231f;
rot_x_p10[2][1] = -0.1736481785774231f;
rot_x_p10[2][2] = 0.9848077297210693f;
unit_m3(rot_y_m30);
rot_y_m30[0][0] = 0.8660253882408142f;
rot_y_m30[0][2] = 0.5f;
rot_y_m30[2][0] = -0.5f;
rot_y_m30[2][2] = 0.8660253882408142f;
unit_m3(rot_z_p20);
rot_z_p20[0][0] = 0.9396926164627075f;
rot_z_p20[0][1] = 0.3420201241970062f;
rot_z_p20[1][0] = -0.3420201241970062f;
rot_z_p20[1][1] = 0.9396926164627075f;
/* Run tests */
create_swapped_rotation_matrix(rot_x_mat, rot_y_mat, rot_z_mat, euler, ABC_ZUP_FROM_YUP);
EXPECT_M3_NEAR(rot_x_mat, rot_x_p10, 1e-5f);
EXPECT_M3_NEAR(rot_y_mat, rot_y_m30, 1e-5f);
EXPECT_M3_NEAR(rot_z_mat, rot_z_p20, 1e-5f);
}
TEST(abc_matrix, CopyM44AxisSwap_YfromZ)
{
float result[4][4];
/* Construct an input matrix that performs a rotation like the tests
* above. This matrix was created by rotating a cube in Blender over
* (X=10, Y=20, Z=30 degrees in XYZ order) and translating over (1, 2, 3) */
float input[4][4] = {
{0.81379765272f, 0.4698463380336f, -0.342020124197f, 0.0f},
{-0.44096961617f, 0.8825641274452f, 0.163175910711f, 0.0f},
{0.37852230668f, 0.0180283170193f, 0.925416588783f, 0.0f},
{1.0f, 2.0f, 3.0f, 1.0f},
};
copy_m44_axis_swap(result, input, ABC_YUP_FROM_ZUP);
/* Check the resulting rotation & translation. */
const float trans[4] = {1.0f, 3.0f, -2.0f, 1.0f};
EXPECT_V4_NEAR(trans, result[3], 1e-5f);
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=30, Z=-20 degrees in XZY order) and translating over (1, 3, -2) */
const float expect[4][4] = {
{0.813797652721f, -0.342020124197f, -0.469846338033f, 0.0f},
{0.378522306680f, 0.925416588783f, -0.018028317019f, 0.0f},
{0.440969616174f, -0.163175910711f, 0.882564127445f, 0.0f},
{1.0f, 3.0f, -2.0f, 1.0f},
};
EXPECT_M4_NEAR(expect, result, 1e-5f);
}
TEST(abc_matrix, CopyM44AxisSwapWithScale_YfromZ)
{
float result[4][4];
/* Construct an input matrix that performs a rotation like the tests
* above. This matrix was created by rotating a cube in Blender over
* (X=10, Y=20, Z=30 degrees in XYZ order), translating over (1, 2, 3),
* and scaling by (4, 5, 6). */
float input[4][4] = {
{3.25519061088f, 1.8793853521347f, -1.368080496788f, 0.0f},
{-2.20484805107f, 4.4128208160400f, 0.815879583358f, 0.0f},
{2.27113389968f, 0.1081698983907f, 5.552499771118f, 0.0f},
{1.0f, 2.0f, 3.0f, 1.0f},
};
copy_m44_axis_swap(result, input, ABC_YUP_FROM_ZUP);
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=30, Z=-20 degrees in XZY order), translating over (1, 3, -2)
* and scaling over (4, 6, 5). */
const float expect[4][4] = {
{3.255190610885f, -1.368080496788f, -1.879385352134f, 0.0f},
{2.271133899688f, 5.552499771118f, -0.108169898390f, 0.0f},
{2.204848051071f, -0.815879583358f, 4.412820816040f, 0.0f},
{1.0f, 3.0f, -2.0f, 1.0f},
};
EXPECT_M4_NEAR(expect, result, 1e-5f);
}
TEST(abc_matrix, CopyM44AxisSwap_ZfromY)
{
float result[4][4];
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=30, Z=-20 degrees in XZY order) and translating over (1, 3, -2) */
float input[4][4] = {
{0.813797652721f, -0.342020124197f, -0.469846338033f, 0.0f},
{0.378522306680f, 0.925416588783f, -0.018028317019f, 0.0f},
{0.440969616174f, -0.163175910711f, 0.882564127445f, 0.0f},
{1.0f, 3.0f, -2.0f, 1.0f},
};
copy_m44_axis_swap(result, input, ABC_ZUP_FROM_YUP);
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=20, Z=30 degrees in XYZ order) and translating over (1, 2, 3) */
const float expect[4][4] = {
{0.813797652721f, 0.469846338033f, -0.342020124197f, 0.0f},
{-0.44096961617f, 0.882564127445f, 0.163175910711f, 0.0f},
{0.378522306680f, 0.018028317019f, 0.925416588783f, 0.0f},
{1.0f, 2.0f, 3.0f, 1.0f},
};
EXPECT_M4_NEAR(expect, result, 1e-5f);
}
TEST(abc_matrix, CopyM44AxisSwapWithScale_ZfromY)
{
float result[4][4];
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=30, Z=-20 degrees in XZY order), translating over (1, 3, -2)
* and scaling over (4, 6, 5). */
float input[4][4] = {
{3.2551906108f, -1.36808049678f, -1.879385352134f, 0.0f},
{2.2711338996f, 5.55249977111f, -0.108169898390f, 0.0f},
{2.2048480510f, -0.81587958335f, 4.412820816040f, 0.0f},
{1.0f, 3.0f, -2.0f, 1.0f},
};
copy_m44_axis_swap(result, input, ABC_ZUP_FROM_YUP);
/* This matrix was created by rotating a cube in Blender over
* (X=10, Y=20, Z=30 degrees in XYZ order), translating over (1, 2, 3),
* and scaling by (4, 5, 6). */
const float expect[4][4] = {
{3.25519061088f, 1.879385352134f, -1.36808049678f, 0.0f},
{-2.2048480510f, 4.412820816040f, 0.81587958335f, 0.0f},
{2.27113389968f, 0.108169898390f, 5.55249977111f, 0.0f},
{1.0f, 2.0f, 3.0f, 1.0f},
};
EXPECT_M4_NEAR(expect, result, 1e-5f);
}
TEST(abc_matrix, CopyM44AxisSwapWithScale_gimbal_ZfromY)
{
float result[4][4];
/* This matrix represents a rotation over (-90, -0, -0) degrees,
* and a translation over (-0, -0.1, -0). It is in Y=up. */
float input[4][4] = {
{1.000f, 0.000f, 0.000f, 0.000f},
{0.000f, 0.000f, -1.000f, 0.000f},
{0.000f, 1.000f, 0.000f, 0.000f},
{-0.000f, -0.100f, -0.000f, 1.000f},
};
copy_m44_axis_swap(result, input, ABC_ZUP_FROM_YUP);
/* Since the rotation is only over the X-axis, it should not change.
* The translation does change. */
const float expect[4][4] = {
{1.000f, 0.000f, 0.000f, 0.000f},
{0.000f, 0.000f, -1.000f, 0.000f},
{0.000f, 1.000f, 0.000f, 0.000f},
{-0.000f, 0.000f, -0.100f, 1.000f},
};
EXPECT_M4_NEAR(expect, result, 1e-5f);
}
} // namespace blender::io::alembic