Add Chromium-only Blender WebEngine parity work
This commit is contained in:
35
blender-5.2.0/source/blender/io/CMakeLists.txt
Normal file
35
blender-5.2.0/source/blender/io/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(csv)
|
||||
|
||||
if(WITH_IO_WAVEFRONT_OBJ)
|
||||
add_subdirectory(wavefront_obj)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_PLY)
|
||||
add_subdirectory(ply)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_STL)
|
||||
add_subdirectory(stl)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_GREASE_PENCIL)
|
||||
add_subdirectory(grease_pencil)
|
||||
endif()
|
||||
|
||||
if(WITH_IO_FBX)
|
||||
add_subdirectory(fbx)
|
||||
endif()
|
||||
|
||||
if(WITH_ALEMBIC)
|
||||
add_subdirectory(alembic)
|
||||
endif()
|
||||
|
||||
if(WITH_USD)
|
||||
add_subdirectory(usd)
|
||||
endif()
|
||||
158
blender-5.2.0/source/blender/io/alembic/ABC_alembic.h
Normal file
158
blender-5.2.0/source/blender/io/alembic/ABC_alembic.h
Normal 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
|
||||
106
blender-5.2.0/source/blender/io/alembic/CMakeLists.txt
Normal file
106
blender-5.2.0/source/blender/io/alembic/CMakeLists.txt
Normal 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()
|
||||
250
blender-5.2.0/source/blender/io/alembic/exporter/abc_archive.cc
Normal file
250
blender-5.2.0/source/blender/io/alembic/exporter/abc_archive.cc
Normal 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 ¶ms,
|
||||
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 ¶ms,
|
||||
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 ¶ms,
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶ms)
|
||||
: 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 = ¶ms_;
|
||||
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
|
||||
@@ -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 ¶ms_;
|
||||
|
||||
public:
|
||||
ABCHierarchyIterator(Main *bmain,
|
||||
Depsgraph *depsgraph,
|
||||
ABCArchive *abc_archive,
|
||||
const AlembicExportParams ¶ms);
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
647
blender-5.2.0/source/blender/io/alembic/intern/abc_customdata.cc
Normal file
647
blender-5.2.0/source/blender/io/alembic/intern/abc_customdata.cc
Normal 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
|
||||
133
blender-5.2.0/source/blender/io/alembic/intern/abc_customdata.h
Normal file
133
blender-5.2.0/source/blender/io/alembic/intern/abc_customdata.h
Normal 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
|
||||
111
blender-5.2.0/source/blender/io/alembic/intern/abc_keyframing.cc
Normal file
111
blender-5.2.0/source/blender/io/alembic/intern/abc_keyframing.cc
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
1228
blender-5.2.0/source/blender/io/alembic/intern/abc_reader_mesh.cc
Normal file
1228
blender-5.2.0/source/blender/io/alembic/intern/abc_reader_mesh.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
210
blender-5.2.0/source/blender/io/alembic/intern/abc_util.cc
Normal file
210
blender-5.2.0/source/blender/io/alembic/intern/abc_util.cc
Normal 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
|
||||
84
blender-5.2.0/source/blender/io/alembic/intern/abc_util.h
Normal file
84
blender-5.2.0/source/blender/io/alembic/intern/abc_util.h
Normal 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
|
||||
997
blender-5.2.0/source/blender/io/alembic/intern/alembic_capi.cc
Normal file
997
blender-5.2.0/source/blender/io/alembic/intern/alembic_capi.cc
Normal 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
|
||||
162
blender-5.2.0/source/blender/io/alembic/tests/abc_export_test.cc
Normal file
162
blender-5.2.0/source/blender/io/alembic/tests/abc_export_test.cc
Normal 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
|
||||
292
blender-5.2.0/source/blender/io/alembic/tests/abc_matrix_test.cc
Normal file
292
blender-5.2.0/source/blender/io/alembic/tests/abc_matrix_test.cc
Normal 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
|
||||
63
blender-5.2.0/source/blender/io/common/CMakeLists.txt
Normal file
63
blender-5.2.0/source/blender/io/common/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
# SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
../../makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
intern/abstract_hierarchy_iterator.cc
|
||||
intern/dupli_parent_finder.cc
|
||||
intern/dupli_persistent_id.cc
|
||||
intern/mesh_utils.cc
|
||||
intern/object_identifier.cc
|
||||
intern/orientation.cc
|
||||
intern/path_util.cc
|
||||
intern/string_utils.cc
|
||||
intern/subdiv_disabler.cc
|
||||
|
||||
IO_abstract_hierarchy_iterator.h
|
||||
IO_dupli_persistent_id.hh
|
||||
IO_mesh_utils.hh
|
||||
IO_orientation.hh
|
||||
IO_path_util.hh
|
||||
IO_path_util_types.hh
|
||||
IO_string_utils.hh
|
||||
IO_subdiv_disabler.hh
|
||||
IO_types.hh
|
||||
IO_validate.hh
|
||||
intern/dupli_parent_finder.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
PRIVATE bf::extern::fast_float
|
||||
)
|
||||
|
||||
blender_add_lib(bf_io_common "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
|
||||
if(WITH_GTESTS)
|
||||
set(TEST_SRC
|
||||
intern/abstract_hierarchy_iterator_test.cc
|
||||
intern/object_identifier_test.cc
|
||||
intern/string_utils_tests.cc
|
||||
)
|
||||
set(TEST_INC
|
||||
../../blenloader
|
||||
)
|
||||
set(TEST_LIB
|
||||
bf_blenloader_test_util
|
||||
bf_io_common
|
||||
)
|
||||
blender_add_test_suite_lib(io_common "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
|
||||
endif()
|
||||
@@ -0,0 +1,401 @@
|
||||
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/*
|
||||
* This file contains the AbstractHierarchyIterator. It is intended for exporters for file
|
||||
* formats that concern an entire hierarchy of objects (rather than, for example, an OBJ file that
|
||||
* contains only a single mesh). Examples are Universal Scene Description (USD) and Alembic.
|
||||
* AbstractHierarchyIterator is intended to be subclassed to support concrete file formats.
|
||||
*
|
||||
* The AbstractHierarchyIterator makes a distinction between the actual object hierarchy and the
|
||||
* export hierarchy. The former is the parent/child structure in Blender, which can have multiple
|
||||
* parent-like objects. For example, a duplicated object can have both a duplicator and a parent,
|
||||
* both determining the final transform. The export hierarchy is the hierarchy as written to the
|
||||
* file, and every object has only one export-parent.
|
||||
*
|
||||
* Currently the AbstractHierarchyIterator does not make any decisions about *what* to export.
|
||||
* Selections like "selected only" or "no hair systems" are left to concrete subclasses.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IO_dupli_persistent_id.hh"
|
||||
|
||||
#include "BLI_hash.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_math_matrix_types.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Depsgraph;
|
||||
struct DupliObject;
|
||||
struct ID;
|
||||
struct Main;
|
||||
struct Object;
|
||||
struct ParticleSystem;
|
||||
|
||||
namespace io {
|
||||
|
||||
class AbstractHierarchyWriter;
|
||||
class DupliParentFinder;
|
||||
|
||||
/* HierarchyContext structs are created by the AbstractHierarchyIterator. Each HierarchyContext
|
||||
* struct contains everything necessary to export a single object to a file. */
|
||||
struct HierarchyContext {
|
||||
/*********** Determined during hierarchy iteration: ***************/
|
||||
Object *object; /* Evaluated object. */
|
||||
Object *export_parent;
|
||||
Object *duplicator;
|
||||
PersistentID persistent_id;
|
||||
float4x4 matrix_world;
|
||||
std::string export_name;
|
||||
|
||||
/* When weak_export=true, the object will be exported only as transform, and only if is an
|
||||
* ancestor of an object with weak_export=false.
|
||||
*
|
||||
* In other words: when weak_export=true but this object has no children, or all descendants also
|
||||
* have weak_export=true, this object (and by recursive reasoning all its descendants) will be
|
||||
* excluded from the export.
|
||||
*
|
||||
* The export hierarchy is kept as close to the hierarchy in Blender as possible. As such, an
|
||||
* object that serves as a parent for another object, but which should NOT be exported itself, is
|
||||
* exported only as transform (i.e. as empty). This happens with objects that are invisible when
|
||||
* exporting with "Visible Only" enabled, for example. */
|
||||
bool weak_export;
|
||||
|
||||
/* When true, this object should check its parents for animation data when determining whether
|
||||
* it's animated. This is necessary when a parent object in Blender is not part of the export. */
|
||||
bool animation_check_include_parent;
|
||||
|
||||
/* The flag makes unambiguous the fact that the current context targets object or data. This is
|
||||
* notably used in USDHierarchyIterator::create_usd_export_context: options like
|
||||
* merge_parent_xform option is meaningless for object, it only makes sense for data. */
|
||||
bool is_object_data_context;
|
||||
|
||||
/* This flag tells, within a object data context, if an object is the parent of other objects.
|
||||
* This is useful when exporting UsdGeomGprim: those cannot be nested into each other. For
|
||||
* example, an UsdGeomMesh cannot have other UsdGeomMesh as descendants and other hierarchy
|
||||
* strategies need to be adopted.
|
||||
*/
|
||||
bool is_parent;
|
||||
|
||||
/* When true this is duplisource object. This flag is used to identify instance prototypes. */
|
||||
bool is_duplisource;
|
||||
|
||||
/* This flag tells whether an object is a valid point instance of other objects.
|
||||
* If true, it means the object has a valid reference path and its value can be included
|
||||
* in the instances data of UsdGeomPointInstancer. */
|
||||
bool is_point_instance;
|
||||
|
||||
/* This flag tells if an object is a valid prototype of a point instancer. */
|
||||
bool is_point_proto;
|
||||
|
||||
/* True if this context is a descendant of any context with is_point_instance set to true.
|
||||
* This helps skip redundant instancing data during export. */
|
||||
bool has_point_instance_ancestor;
|
||||
|
||||
/*********** Determined during writer creation: ***************/
|
||||
float4x4 parent_matrix_inv_world; /* Inverse of the parent's world matrix. */
|
||||
std::string export_path; /* Hierarchical path, such as "/grandparent/parent/object_name". */
|
||||
ParticleSystem *particle_system; /* Only set for particle/hair writers. */
|
||||
|
||||
/* Hierarchical path of the object this object is duplicating; only set when this object should
|
||||
* be stored as a reference to its original. It can happen that the original is not part of the
|
||||
* exported objects, in which case this string is empty even though 'duplicator' is set. */
|
||||
std::string original_export_path;
|
||||
|
||||
/* Export path of the higher-up exported data. For transforms, this is the export path of the
|
||||
* parent object. For object data, this is the export path of that object's transform.
|
||||
*
|
||||
* From the exported file's point of view, this is the path to the parent in that file. The term
|
||||
* "parent" is not used here to avoid confusion with Blender's meaning of the word (which always
|
||||
* refers to a different object). */
|
||||
std::string higher_up_export_path;
|
||||
|
||||
/* Return a HierarchyContext representing the root of the export hierarchy. */
|
||||
static const HierarchyContext *root();
|
||||
|
||||
/* For handling instanced collections, instances created by particles, etc. */
|
||||
bool is_instance() const;
|
||||
void mark_as_instance_of(const std::string &reference_export_path);
|
||||
void mark_as_not_instanced();
|
||||
bool is_prototype() const;
|
||||
|
||||
/* For handling point instancing (Instance on Points geometry node). */
|
||||
bool is_point_instancer() const;
|
||||
|
||||
bool is_object_visible(enum eEvaluationMode evaluation_mode) const;
|
||||
};
|
||||
|
||||
/* Abstract writer for objects. Create concrete subclasses to write to USD, Alembic, etc.
|
||||
*
|
||||
* Instantiated by the AbstractHierarchyIterator on the first frame an object exists. Generally
|
||||
* that's the first frame to be exported, but can be later, for example when objects are
|
||||
* instantiated by particles. The AbstractHierarchyWriter::write() function is called on every
|
||||
* frame the object exists in the dependency graph and should be exported.
|
||||
*/
|
||||
class AbstractHierarchyWriter {
|
||||
public:
|
||||
virtual ~AbstractHierarchyWriter() = default;
|
||||
virtual void write(HierarchyContext &context) = 0;
|
||||
/* TODO(Sybren): add function like absent() that's called when a writer was previously created,
|
||||
* but wasn't used while exporting the current frame (for example, a particle-instanced mesh of
|
||||
* which the particle is no longer alive). */
|
||||
protected:
|
||||
/* Return true if the data written by this writer changes over time.
|
||||
* Note that this function assumes this is an object data writer. Transform writers should not
|
||||
* call this but implement their own logic. */
|
||||
virtual bool check_is_animated(const HierarchyContext &context) const;
|
||||
|
||||
/* Helper functions for animation checks. */
|
||||
static bool check_has_physics(const HierarchyContext &context);
|
||||
static bool check_has_deforming_physics(const HierarchyContext &context);
|
||||
};
|
||||
|
||||
/* Determines which subset of the writers actually gets to write. */
|
||||
struct ExportSubset {
|
||||
bool transforms : 1;
|
||||
bool shapes : 1;
|
||||
};
|
||||
|
||||
/* EnsuredWriter represents an AbstractHierarchyWriter* combined with information whether it was
|
||||
* newly created or not. It's returned by AbstractHierarchyIterator::ensure_writer(). */
|
||||
class EnsuredWriter {
|
||||
private:
|
||||
AbstractHierarchyWriter *writer_;
|
||||
|
||||
/* Is set to truth when ensure_writer() did not find existing writer and created a new one.
|
||||
* Is set to false when writer has been re-used or when allocation of the new one has failed
|
||||
* (`writer` will be `nullptr` in that case and bool(ensured_writer) will be false). */
|
||||
bool newly_created_;
|
||||
|
||||
EnsuredWriter(AbstractHierarchyWriter *writer, bool newly_created);
|
||||
|
||||
public:
|
||||
EnsuredWriter();
|
||||
|
||||
static EnsuredWriter empty();
|
||||
static EnsuredWriter existing(AbstractHierarchyWriter *writer);
|
||||
static EnsuredWriter newly_created(AbstractHierarchyWriter *writer);
|
||||
|
||||
bool is_newly_created() const;
|
||||
|
||||
/* These operators make an EnsuredWriter* act as an AbstractHierarchyWriter* */
|
||||
operator bool() const;
|
||||
AbstractHierarchyWriter *operator->();
|
||||
};
|
||||
|
||||
/* Unique identifier for a (potentially duplicated) object.
|
||||
*
|
||||
* Instances of this class serve as key in the export graph of the
|
||||
* AbstractHierarchyIterator. */
|
||||
class ObjectIdentifier {
|
||||
public:
|
||||
Object *object;
|
||||
Object *duplicated_by; /* nullptr for real objects. */
|
||||
PersistentID persistent_id;
|
||||
|
||||
protected:
|
||||
ObjectIdentifier(Object *object, Object *duplicated_by, const PersistentID &persistent_id);
|
||||
|
||||
public:
|
||||
static ObjectIdentifier for_graph_root();
|
||||
static ObjectIdentifier for_real_object(Object *object);
|
||||
static ObjectIdentifier for_hierarchy_context(const HierarchyContext *context);
|
||||
static ObjectIdentifier for_duplicated_object(const DupliObject *dupli_object,
|
||||
Object *duplicated_by);
|
||||
|
||||
bool is_root() const;
|
||||
|
||||
uint64_t hash() const
|
||||
{
|
||||
return get_default_hash(object, duplicated_by, persistent_id);
|
||||
}
|
||||
};
|
||||
|
||||
bool operator==(const ObjectIdentifier &obj_ident_a, const ObjectIdentifier &obj_ident_b);
|
||||
|
||||
/* AbstractHierarchyIterator iterates over objects in a dependency graph, and constructs export
|
||||
* writers. These writers are then called to perform the actual writing to a USD or Alembic file.
|
||||
*
|
||||
* Dealing with file- and scene-level data (for example, creating a USD scene, setting the frame
|
||||
* rate, etc.) is not part of the AbstractHierarchyIterator class structure, and should be done
|
||||
* in separate code.
|
||||
*/
|
||||
class AbstractHierarchyIterator {
|
||||
public:
|
||||
/* Mapping from export path to writer. */
|
||||
using WriterMap = Map<std::string, AbstractHierarchyWriter *>;
|
||||
/* All the children of some object, as per the export hierarchy. */
|
||||
using ExportChildren = Set<HierarchyContext *>;
|
||||
/* Mapping from an object and its duplicator to the object's export-children. */
|
||||
using ExportGraph = Map<ObjectIdentifier, ExportChildren>;
|
||||
/* Mapping from ID to its export path. This is used for instancing; given an
|
||||
* instanced datablock, the export path of the original can be looked up. */
|
||||
using ExportPathMap = Map<ID *, std::string>;
|
||||
/* Mapping from ID name to a set of names logically residing "under" it. Used for unique
|
||||
* name generation. */
|
||||
using ExportUsedNameMap = Map<std::string, Set<std::string>>;
|
||||
/* IDs of all duplisource objects, used to identify instance prototypes. */
|
||||
using DupliSources = Set<ID *>;
|
||||
|
||||
protected:
|
||||
ExportGraph export_graph_;
|
||||
ExportPathMap duplisource_export_path_;
|
||||
Main *bmain_;
|
||||
Depsgraph *depsgraph_;
|
||||
WriterMap writers_;
|
||||
ExportSubset export_subset_;
|
||||
DupliSources duplisources_;
|
||||
ExportUsedNameMap used_names_;
|
||||
|
||||
public:
|
||||
explicit AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph);
|
||||
virtual ~AbstractHierarchyIterator();
|
||||
|
||||
/* Iterate over the depsgraph, create writers, and tell the writers to write.
|
||||
* Main entry point for the AbstractHierarchyIterator, must be called for every to-be-exported
|
||||
* (sub)frame. */
|
||||
virtual void iterate_and_write();
|
||||
|
||||
/* Release all writers. Call after all frames have been exported. */
|
||||
void release_writers();
|
||||
|
||||
/* Determine which subset of writers is used for exporting.
|
||||
* Set this before calling iterate_and_write().
|
||||
*
|
||||
* Note that writers are created for each iterated object, regardless of this option. When a
|
||||
* writer is created it will also write the current iteration, to ensure the hierarchy is
|
||||
* complete. The `export_subset` option is only in effect when the writer already existed from a
|
||||
* previous iteration. */
|
||||
void set_export_subset(ExportSubset export_subset);
|
||||
|
||||
/* Convert the given name to something that is valid for the exported file format.
|
||||
* This base implementation is a no-op; override in a concrete subclass. */
|
||||
virtual std::string make_valid_name(const std::string &name) const;
|
||||
|
||||
virtual std::string make_unique_name(const std::string &original_name,
|
||||
Set<std::string> &used_names);
|
||||
|
||||
/* Return the name of this ID datablock that is valid for the exported file format. Overriding is
|
||||
* only necessary if make_valid_name(id->name+2) is not suitable for the exported file format.
|
||||
* NULL-safe: when `id == nullptr` this returns an empty string. */
|
||||
virtual std::string get_id_name(const ID *id) const;
|
||||
|
||||
/* Given a HierarchyContext of some Object *, return an export path that is valid for its
|
||||
* object->data. Overriding is necessary when the exported format does NOT expect the object's
|
||||
* data to be a child of the object. */
|
||||
virtual std::string get_object_data_path(const HierarchyContext *context) const;
|
||||
|
||||
private:
|
||||
void debug_print_export_graph(const ExportGraph &graph) const;
|
||||
|
||||
void export_graph_construct();
|
||||
void connect_loose_objects();
|
||||
void export_graph_prune();
|
||||
void export_graph_clear();
|
||||
|
||||
void visit_object(Object *object, Object *export_parent, bool weak_export);
|
||||
void visit_dupli_object(const DupliObject *dupli_object,
|
||||
Object *duplicator,
|
||||
const DupliParentFinder &dupli_parent_finder);
|
||||
|
||||
void context_update_for_graph_index(HierarchyContext *context,
|
||||
const ObjectIdentifier &graph_index) const;
|
||||
|
||||
void determine_export_paths(const HierarchyContext *parent_context);
|
||||
bool determine_duplication_references(const HierarchyContext *parent_context,
|
||||
const std::string &indent);
|
||||
|
||||
/* These three functions create writers and call their write() method. */
|
||||
void make_writers(const HierarchyContext *parent_context);
|
||||
void make_writer_object_data(const HierarchyContext *context);
|
||||
void make_writers_particle_systems(const HierarchyContext *transform_context);
|
||||
|
||||
/* Return the appropriate HierarchyContext for the data of the object represented by
|
||||
* object_context. */
|
||||
HierarchyContext context_for_object_data(const HierarchyContext *object_context) const;
|
||||
|
||||
/* Convenience wrappers around get_id_name(). */
|
||||
std::string get_object_name(const Object *object) const;
|
||||
std::string get_object_name(const Object *object, const Object *parent);
|
||||
std::string get_object_data_name(const Object *object) const;
|
||||
|
||||
using create_writer_func =
|
||||
AbstractHierarchyWriter *(AbstractHierarchyIterator::*)(const HierarchyContext *);
|
||||
/* Ensure that a writer exists; if it doesn't, call create_func(context).
|
||||
*
|
||||
* The create_func function should be one of the create_XXXX_writer(context) functions declared
|
||||
* below. */
|
||||
EnsuredWriter ensure_writer(const HierarchyContext *context, create_writer_func create_func);
|
||||
|
||||
protected:
|
||||
/* Construct a valid path for the export file format. This class concatenates by using '/' as a
|
||||
* path separator, which is valid for both Alembic and USD. */
|
||||
virtual std::string path_concatenate(const std::string &parent_path,
|
||||
const std::string &child_path) const;
|
||||
|
||||
/* Return whether this object should be marked as 'weak export' or not.
|
||||
*
|
||||
* When this returns false, writers for the transform and data are created,
|
||||
* and dupli-objects dupli-object generated from this object will be passed to
|
||||
* should_visit_dupli_object().
|
||||
*
|
||||
* When this returns true, only a transform writer is created and marked as
|
||||
* 'weak export'. In this case, the transform writer will be removed before
|
||||
* exporting starts, unless a descendant of this object is to be exported.
|
||||
* Dupli-object generated from this object will also be skipped.
|
||||
*
|
||||
* See HierarchyContext::weak_export.
|
||||
*/
|
||||
virtual bool mark_as_weak_export(const Object *object) const;
|
||||
|
||||
virtual bool should_visit_dupli_object(const DupliObject *dupli_object) const;
|
||||
|
||||
virtual ObjectIdentifier determine_graph_index_object(const HierarchyContext *context);
|
||||
virtual ObjectIdentifier determine_graph_index_dupli(
|
||||
const HierarchyContext *context,
|
||||
const DupliObject *dupli_object,
|
||||
const DupliParentFinder &dupli_parent_finder);
|
||||
|
||||
/* These functions should create an AbstractHierarchyWriter subclass instance, or return
|
||||
* nullptr if the object or its data should not be exported. Returning a nullptr for
|
||||
* data/hair/particle will NOT prevent the transform to be written.
|
||||
*
|
||||
* The returned writer is owned by the AbstractHierarchyWriter, and should be freed in
|
||||
* delete_object_writer().
|
||||
*
|
||||
* The created AbstractHierarchyWriter instances should NOT keep a copy of the context pointer.
|
||||
* The context can be stack-allocated and go out of scope. */
|
||||
virtual AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) = 0;
|
||||
virtual AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) = 0;
|
||||
virtual AbstractHierarchyWriter *create_hair_writer(const HierarchyContext *context) = 0;
|
||||
virtual AbstractHierarchyWriter *create_particle_writer(const HierarchyContext *context) = 0;
|
||||
|
||||
/* Called by release_writers() to free what the create_XXX_writer() functions allocated. */
|
||||
virtual void release_writer(AbstractHierarchyWriter *writer) = 0;
|
||||
|
||||
/* Return true if data writers should be created for this context. */
|
||||
virtual bool include_data_writers(const HierarchyContext *) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Return true if children of the context should be converted to writers. */
|
||||
virtual bool include_child_writers(const HierarchyContext *) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
AbstractHierarchyWriter *get_writer(const std::string &export_path) const;
|
||||
ExportChildren *graph_children(const HierarchyContext *context);
|
||||
};
|
||||
|
||||
} // namespace io
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,48 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "BKE_duplilist.hh"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
/* Wrapper for DupliObject::persistent_id that can act as a map key. */
|
||||
class PersistentID {
|
||||
protected:
|
||||
constexpr static int array_length_ = MAX_DUPLI_RECUR;
|
||||
using PIDArray = std::array<int, array_length_>;
|
||||
PIDArray persistent_id_;
|
||||
|
||||
explicit PersistentID(const PIDArray &persistent_id_values);
|
||||
|
||||
public:
|
||||
PersistentID();
|
||||
explicit PersistentID(const DupliObject *dupli_ob);
|
||||
|
||||
/* Return true if the persistent IDs are the same, ignoring the first digit. */
|
||||
bool is_from_same_instancer_as(const PersistentID &other) const;
|
||||
|
||||
/* Construct the persistent ID of this instance's instancer. */
|
||||
PersistentID instancer_pid() const;
|
||||
|
||||
/* Construct a string representation by reversing the persistent ID.
|
||||
* In case of a duplicator that is duplicated itself as well, this
|
||||
* results in strings like:
|
||||
* "3" for the duplicated duplicator, and
|
||||
* "3-0", "3-1", etc. for its duplis. */
|
||||
std::string as_object_name_suffix() const;
|
||||
|
||||
uint64_t hash() const;
|
||||
|
||||
friend bool operator==(const PersistentID &persistent_id_a, const PersistentID &persistent_id_b);
|
||||
|
||||
private:
|
||||
void copy_values_from(const PIDArray &persistent_id_values);
|
||||
};
|
||||
|
||||
} // namespace blender::io
|
||||
46
blender-5.2.0/source/blender/io/common/IO_mesh_utils.hh
Normal file
46
blender-5.2.0/source/blender/io/common/IO_mesh_utils.hh
Normal file
@@ -0,0 +1,46 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Depsgraph;
|
||||
struct Mesh;
|
||||
struct Object;
|
||||
|
||||
namespace io {
|
||||
|
||||
/**
|
||||
* Conditionally coerce objects to mesh for export.
|
||||
* For use by formats that only support meshes.
|
||||
*/
|
||||
struct MeshCoerceForExport {
|
||||
/**
|
||||
* Mesh to read geometry from, referencing evaluated, pre-modified or `owned` data.
|
||||
*
|
||||
* The same as `owned` when a conversion was performed.
|
||||
*/
|
||||
const Mesh *mesh = nullptr;
|
||||
/**
|
||||
* Mesh converted from a non-mesh, or null.
|
||||
*/
|
||||
Mesh *owned = nullptr;
|
||||
|
||||
~MeshCoerceForExport();
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the mesh to export for `obj_eval`.
|
||||
*
|
||||
* The mesh may be converted on demand and stored in #MeshCoerceForExport::owned.
|
||||
*
|
||||
* \return The mesh to export.
|
||||
*/
|
||||
const Mesh *mesh_coerce_for_export_setup(MeshCoerceForExport &coerce,
|
||||
Depsgraph *depsgraph,
|
||||
Object *obj_eval,
|
||||
bool apply_modifiers);
|
||||
|
||||
} // namespace io
|
||||
} // namespace blender
|
||||
28
blender-5.2.0/source/blender/io/common/IO_orientation.hh
Normal file
28
blender-5.2.0/source/blender/io/common/IO_orientation.hh
Normal file
@@ -0,0 +1,28 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct EnumPropertyItem;
|
||||
struct Main;
|
||||
struct PointerRNA;
|
||||
struct Scene;
|
||||
|
||||
enum eIOAxis {
|
||||
IO_AXIS_X = 0,
|
||||
IO_AXIS_Y = 1,
|
||||
IO_AXIS_Z = 2,
|
||||
IO_AXIS_NEGATIVE_X = 3,
|
||||
IO_AXIS_NEGATIVE_Y = 4,
|
||||
IO_AXIS_NEGATIVE_Z = 5,
|
||||
};
|
||||
|
||||
extern const EnumPropertyItem io_transform_axis[];
|
||||
|
||||
void io_ui_forward_axis_update(Main *main, Scene *scene, PointerRNA *ptr);
|
||||
void io_ui_up_axis_update(Main *main, Scene *scene, PointerRNA *ptr);
|
||||
|
||||
} // namespace blender
|
||||
31
blender-5.2.0/source/blender/io/common/IO_path_util.hh
Normal file
31
blender-5.2.0/source/blender/io/common/IO_path_util.hh
Normal file
@@ -0,0 +1,31 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "IO_path_util_types.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
/**
|
||||
* Return a filepath relative to a destination directory, for use with
|
||||
* exporters.
|
||||
*
|
||||
* When PATH_REFERENCE_COPY mode is used, the file path pair (source
|
||||
* path, destination path) is added to the `copy_set`.
|
||||
*
|
||||
* Equivalent of bpy_extras.io_utils.path_reference.
|
||||
*/
|
||||
std::string path_reference(StringRefNull filepath,
|
||||
StringRefNull base_src,
|
||||
StringRefNull base_dst,
|
||||
ePathReferenceMode mode,
|
||||
Set<std::pair<std::string, std::string>> *copy_set = nullptr);
|
||||
|
||||
/** Execute copying files of path_reference. */
|
||||
void path_reference_copy(const Set<std::pair<std::string, std::string>> ©_set);
|
||||
|
||||
} // namespace blender::io
|
||||
24
blender-5.2.0/source/blender/io/common/IO_path_util_types.hh
Normal file
24
blender-5.2.0/source/blender/io/common/IO_path_util_types.hh
Normal file
@@ -0,0 +1,24 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
/** Method used to reference paths. Equivalent of bpy_extras.io_utils.path_reference_mode. */
|
||||
enum ePathReferenceMode {
|
||||
/** Use relative paths with subdirectories only. */
|
||||
PATH_REFERENCE_AUTO = 0,
|
||||
/** Always write absolute paths. */
|
||||
PATH_REFERENCE_ABSOLUTE = 1,
|
||||
/** Write relative paths where possible. */
|
||||
PATH_REFERENCE_RELATIVE = 2,
|
||||
/** Match absolute/relative setting with input path. */
|
||||
PATH_REFERENCE_MATCH = 3,
|
||||
/** Filename only. */
|
||||
PATH_REFERENCE_STRIP = 4,
|
||||
/** Copy the file to the destination path. */
|
||||
PATH_REFERENCE_COPY = 5,
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
114
blender-5.2.0/source/blender/io/common/IO_string_utils.hh
Normal file
114
blender-5.2.0/source/blender/io/common/IO_string_utils.hh
Normal file
@@ -0,0 +1,114 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
/*
|
||||
* Various text parsing utilities used by importers.
|
||||
*
|
||||
* Many of these functions take two pointers (p, end) indicating
|
||||
* which part of a string to operate on, and return a possibly
|
||||
* changed new start of the string. They could be taking a StringRef
|
||||
* as input and returning a new StringRef, but this is a hot path
|
||||
* in CSV and OBJ parsing, and the StringRef approach does lose performance
|
||||
* (mostly due to return of StringRef being two register-size values
|
||||
* instead of just one pointer).
|
||||
*/
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
/**
|
||||
* Fetches next line from an input string buffer.
|
||||
*
|
||||
* The returned line will not have '\n' characters at the end;
|
||||
* the `buffer` is modified to contain remaining text without
|
||||
* the input line.
|
||||
*/
|
||||
StringRef read_next_line(StringRef &buffer);
|
||||
|
||||
/**
|
||||
* Drop leading white-space from a string part.
|
||||
*/
|
||||
const char *drop_whitespace(const char *p, const char *end);
|
||||
|
||||
/**
|
||||
* Drop leading non-white-space from a string part.
|
||||
*/
|
||||
const char *drop_non_whitespace(const char *p, const char *end);
|
||||
|
||||
/**
|
||||
* Parse an integer from an input string.
|
||||
* The parsed result is stored in `dst`. The function skips
|
||||
* leading white-space unless `skip_space=false`. If the
|
||||
* number can't be parsed (invalid syntax, out of range),
|
||||
* `success` value is false.
|
||||
*
|
||||
* Returns the start of remainder of the input string after parsing.
|
||||
*/
|
||||
const char *try_parse_int(
|
||||
const char *p, const char *end, int fallback, bool &success, int &dst, bool skip_space = true);
|
||||
|
||||
/**
|
||||
* Parse a float from an input string.
|
||||
* The parsed result is stored in `dst`. The function skips
|
||||
* leading white-space unless `skip_space=false`. If the
|
||||
* number can't be parsed (invalid syntax, out of range),
|
||||
* `success` value is false.
|
||||
*
|
||||
* Returns the start of remainder of the input string after parsing.
|
||||
*/
|
||||
const char *try_parse_float(const char *p,
|
||||
const char *end,
|
||||
int fallback,
|
||||
bool &success,
|
||||
float &dst,
|
||||
bool skip_space = true);
|
||||
|
||||
/**
|
||||
* Parse an integer from an input string.
|
||||
* The parsed result is stored in `dst`. The function skips
|
||||
* leading white-space unless `skip_space=false`. If the
|
||||
* number can't be parsed (invalid syntax, out of range),
|
||||
* `fallback` value is stored instead.
|
||||
*
|
||||
* Returns the start of remainder of the input string after parsing.
|
||||
*/
|
||||
const char *parse_int(
|
||||
const char *p, const char *end, int fallback, int &dst, bool skip_space = true);
|
||||
|
||||
/**
|
||||
* Parse a float from an input string.
|
||||
* The parsed result is stored in `dst`. The function skips
|
||||
* leading white-space unless `skip_space=false`. If the
|
||||
* number can't be parsed (invalid syntax, out of range),
|
||||
* `fallback` value is stored instead. If `require_trailing_space`
|
||||
* is true, the character after the number has to be whitespace.
|
||||
*
|
||||
* Returns the start of remainder of the input string after parsing.
|
||||
*/
|
||||
const char *parse_float(const char *p,
|
||||
const char *end,
|
||||
float fallback,
|
||||
float &dst,
|
||||
bool skip_space = true,
|
||||
bool require_trailing_space = false);
|
||||
|
||||
/**
|
||||
* Parse a number of white-space separated floats from an input string.
|
||||
* The parsed `count` numbers are stored in `dst`. If a
|
||||
* number can't be parsed (invalid syntax, out of range),
|
||||
* `fallback` value is stored instead.
|
||||
*
|
||||
* Returns the start of remainder of the input string after parsing.
|
||||
*/
|
||||
const char *parse_floats(const char *p,
|
||||
const char *end,
|
||||
float fallback,
|
||||
float *dst,
|
||||
int count,
|
||||
bool require_trailing_space = false);
|
||||
|
||||
} // namespace blender::io
|
||||
67
blender-5.2.0/source/blender/io/common/IO_subdiv_disabler.hh
Normal file
67
blender-5.2.0/source/blender/io/common/IO_subdiv_disabler.hh
Normal file
@@ -0,0 +1,67 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "DNA_modifier_types.h"
|
||||
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Depsgraph;
|
||||
struct ModifierData;
|
||||
struct Object;
|
||||
struct Scene;
|
||||
|
||||
namespace io {
|
||||
|
||||
/**
|
||||
* This code is shared between the Alembic and USD exporters.
|
||||
* Temporarily disable the subdiv modifier on mesh objects,
|
||||
* if the subdiv modifier is last on the modifier stack.
|
||||
*
|
||||
* The destructor restores all disabled modifiers.
|
||||
*
|
||||
* Currently, this class is used to disable Catmull-Clark subdivision modifiers.
|
||||
* It is done in a separate step before the exporter starts iterating over all
|
||||
* the frames, so that it only has to happen once per export.
|
||||
*/
|
||||
class SubdivModifierDisabler final {
|
||||
private:
|
||||
Depsgraph *depsgraph_;
|
||||
|
||||
/* TODO: Track the object and its disabled modifier in a single struct and use just 1 Vector. */
|
||||
Vector<ModifierData *> disabled_modifiers_;
|
||||
Vector<Object *> modified_objects_;
|
||||
|
||||
public:
|
||||
explicit SubdivModifierDisabler(Depsgraph *depsgraph);
|
||||
~SubdivModifierDisabler();
|
||||
|
||||
/**
|
||||
* Disable subdiv modifiers on all mesh objects.
|
||||
*/
|
||||
void disable_modifiers();
|
||||
|
||||
/**
|
||||
* Return the Catmull-Clark subdiv modifier on the mesh, if it's the last modifier
|
||||
* in the list or if it's the last modifier preceding any particle system modifiers.
|
||||
* This function ignores Simple subdiv modifiers.
|
||||
*/
|
||||
static ModifierData *get_subdiv_modifier(Scene *scene, const Object *ob, ModifierMode mode);
|
||||
|
||||
/* Disallow copying. */
|
||||
SubdivModifierDisabler(const SubdivModifierDisabler &) = delete;
|
||||
SubdivModifierDisabler &operator=(const SubdivModifierDisabler &) = delete;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Disable the given modifier and add it to the disabled
|
||||
* modifiers list.
|
||||
*/
|
||||
void disable_modifier(ModifierData *mod);
|
||||
};
|
||||
|
||||
} // namespace io
|
||||
} // namespace blender
|
||||
23
blender-5.2.0/source/blender/io/common/IO_types.hh
Normal file
23
blender-5.2.0/source/blender/io/common/IO_types.hh
Normal file
@@ -0,0 +1,23 @@
|
||||
/* SPDX-FileCopyrightText: 2021 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
/* The CacheArchiveHandle struct is only used for anonymous pointers,
|
||||
* to interface between C and C++ code. This is currently used
|
||||
* to hide pointers to alembic ArchiveReader and USDStageReader. */
|
||||
struct CacheArchiveHandle {
|
||||
int unused;
|
||||
};
|
||||
|
||||
/* The CacheReader struct is only used for anonymous pointers,
|
||||
* to interface between C and C++ code. This is currently used
|
||||
* to hide pointers to AbcObjectReader and USDPrimReader
|
||||
* (or subclasses thereof). */
|
||||
struct CacheReader {
|
||||
int unused;
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
30
blender-5.2.0/source/blender/io/common/IO_validate.hh
Normal file
30
blender-5.2.0/source/blender/io/common/IO_validate.hh
Normal file
@@ -0,0 +1,30 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
/** \file
|
||||
* \ingroup io
|
||||
*
|
||||
* Validation utilities for importers.
|
||||
*/
|
||||
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
|
||||
namespace blender::io::validate {
|
||||
|
||||
/** Check if size fits in an `int` as used by Blender geometry types. */
|
||||
inline bool size_fits_in_int(const int64_t size)
|
||||
{
|
||||
return size >= 0 && size <= INT_MAX;
|
||||
}
|
||||
|
||||
/** Check if index fits in the range. */
|
||||
inline bool index_in_range(const int64_t index, const int64_t size)
|
||||
{
|
||||
return (index >= 0 && index < size);
|
||||
}
|
||||
|
||||
} // namespace blender::io::validate
|
||||
@@ -0,0 +1,849 @@
|
||||
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_abstract_hierarchy_iterator.h"
|
||||
#include "dupli_parent_finder.hh"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_duplilist.hh"
|
||||
#include "BKE_geometry_set.hh"
|
||||
#include "BKE_geometry_set_instances.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_particle.h"
|
||||
|
||||
#include "BLI_assert.h"
|
||||
#include "BLI_math_matrix.hh"
|
||||
#include "BLI_math_matrix_types.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string_utils.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
#include "DNA_layer_types.h"
|
||||
#include "DNA_modifier_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_particle_types.h"
|
||||
#include "DNA_rigidbody_types.h"
|
||||
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
const HierarchyContext *HierarchyContext::root()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool HierarchyContext::is_instance() const
|
||||
{
|
||||
return !original_export_path.empty();
|
||||
}
|
||||
void HierarchyContext::mark_as_instance_of(const std::string &reference_export_path)
|
||||
{
|
||||
original_export_path = reference_export_path;
|
||||
}
|
||||
void HierarchyContext::mark_as_not_instanced()
|
||||
{
|
||||
original_export_path.clear();
|
||||
}
|
||||
|
||||
bool HierarchyContext::is_prototype() const
|
||||
{
|
||||
/* The context is for a prototype if it's for a duplisource or
|
||||
* for a duplicated object that was designated to be a prototype
|
||||
* because the original was not included in the export. */
|
||||
return is_duplisource || (duplicator != nullptr && !is_instance());
|
||||
}
|
||||
|
||||
bool HierarchyContext::is_object_visible(const enum eEvaluationMode evaluation_mode) const
|
||||
{
|
||||
const bool is_dupli = duplicator != nullptr;
|
||||
int base_flag;
|
||||
|
||||
if (is_dupli) {
|
||||
/* Construct the object's base flags from its dupli-parent, just like is done in
|
||||
* deg_objects_dupli_iterator_next(). Without this, the visibility check below will fail. Doing
|
||||
* this here, instead of a more suitable location in AbstractHierarchyIterator, prevents
|
||||
* copying the Object for every dupli. */
|
||||
base_flag = object->base_flag;
|
||||
object->base_flag = duplicator->base_flag | BASE_FROM_DUPLI;
|
||||
}
|
||||
|
||||
const int visibility = BKE_object_visibility(object, evaluation_mode);
|
||||
|
||||
if (is_dupli) {
|
||||
object->base_flag = base_flag;
|
||||
}
|
||||
|
||||
return (visibility & OB_VISIBLE_SELF) != 0;
|
||||
}
|
||||
|
||||
EnsuredWriter::EnsuredWriter() : writer_(nullptr), newly_created_(false) {}
|
||||
|
||||
EnsuredWriter::EnsuredWriter(AbstractHierarchyWriter *writer, bool newly_created)
|
||||
: writer_(writer), newly_created_(newly_created)
|
||||
{
|
||||
}
|
||||
|
||||
EnsuredWriter EnsuredWriter::empty()
|
||||
{
|
||||
return EnsuredWriter(nullptr, false);
|
||||
}
|
||||
EnsuredWriter EnsuredWriter::existing(AbstractHierarchyWriter *writer)
|
||||
{
|
||||
return EnsuredWriter(writer, false);
|
||||
}
|
||||
EnsuredWriter EnsuredWriter::newly_created(AbstractHierarchyWriter *writer)
|
||||
{
|
||||
return EnsuredWriter(writer, true);
|
||||
}
|
||||
|
||||
bool EnsuredWriter::is_newly_created() const
|
||||
{
|
||||
return newly_created_;
|
||||
}
|
||||
|
||||
EnsuredWriter::operator bool() const
|
||||
{
|
||||
return writer_ != nullptr;
|
||||
}
|
||||
|
||||
AbstractHierarchyWriter *EnsuredWriter::operator->()
|
||||
{
|
||||
return writer_;
|
||||
}
|
||||
|
||||
bool AbstractHierarchyWriter::check_is_animated(const HierarchyContext &context) const
|
||||
{
|
||||
Object *object = context.object;
|
||||
|
||||
if (BKE_animdata_id_is_animated(object->data)) {
|
||||
return true;
|
||||
}
|
||||
if (BKE_key_from_object(object) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
if (check_has_deforming_physics(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Test modifiers. */
|
||||
/* TODO(Sybren): replace this with a check on the depsgraph to properly check for dependency on
|
||||
* time. */
|
||||
ModifierData *md = static_cast<ModifierData *>(object->modifiers.first);
|
||||
while (md) {
|
||||
if (md->type != eModifierType_Subsurf) {
|
||||
return true;
|
||||
}
|
||||
md = md->next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AbstractHierarchyWriter::check_has_physics(const HierarchyContext &context)
|
||||
{
|
||||
const RigidBodyOb *rbo = context.object->rigidbody_object;
|
||||
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE;
|
||||
}
|
||||
|
||||
bool AbstractHierarchyWriter::check_has_deforming_physics(const HierarchyContext &context)
|
||||
{
|
||||
const RigidBodyOb *rbo = context.object->rigidbody_object;
|
||||
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE && (rbo->flag & RBO_FLAG_USE_DEFORM) != 0;
|
||||
}
|
||||
|
||||
bool HierarchyContext::is_point_instancer() const
|
||||
{
|
||||
if (!object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Collection instancers are handled elsewhere as part of Scene instancing. */
|
||||
if (object->type == OB_EMPTY && object->instance_collection != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bke::GeometrySet geometry_set = bke::object_get_evaluated_geometry_set(*object);
|
||||
return geometry_set.has_instances();
|
||||
}
|
||||
|
||||
AbstractHierarchyIterator::AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
|
||||
: bmain_(bmain), depsgraph_(depsgraph), export_subset_({true, true})
|
||||
{
|
||||
}
|
||||
|
||||
AbstractHierarchyIterator::~AbstractHierarchyIterator()
|
||||
{
|
||||
/* release_writers() cannot be called here directly, as it calls into the pure-virtual
|
||||
* release_writer() function. By the time this destructor is called, the subclass that implements
|
||||
* that pure-virtual function is already destructed. */
|
||||
BLI_assert_msg(
|
||||
writers_.is_empty(),
|
||||
"release_writers() should be called before the AbstractHierarchyIterator goes out of scope");
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::iterate_and_write()
|
||||
{
|
||||
export_graph_construct();
|
||||
connect_loose_objects();
|
||||
export_graph_prune();
|
||||
determine_export_paths(HierarchyContext::root());
|
||||
determine_duplication_references(HierarchyContext::root(), "");
|
||||
make_writers(HierarchyContext::root());
|
||||
export_graph_clear();
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::release_writers()
|
||||
{
|
||||
for (AbstractHierarchyWriter *writer : writers_.values()) {
|
||||
release_writer(writer);
|
||||
}
|
||||
writers_.clear();
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::set_export_subset(ExportSubset export_subset)
|
||||
{
|
||||
export_subset_ = export_subset;
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::make_valid_name(const std::string &name) const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::get_id_name(const ID *id) const
|
||||
{
|
||||
if (id == nullptr) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return make_valid_name(std::string(id->name + 2));
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::make_unique_name(const std::string &original_name,
|
||||
Set<std::string> &used_names)
|
||||
{
|
||||
if (original_name.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string name = BLI_uniquename_cb(
|
||||
[&](const StringRef check_name) { return used_names.contains_as(check_name); },
|
||||
'_',
|
||||
make_valid_name(original_name));
|
||||
|
||||
used_names.add_new(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::get_object_data_path(const HierarchyContext *context) const
|
||||
{
|
||||
BLI_assert(!context->export_path.empty());
|
||||
BLI_assert(context->object->data);
|
||||
|
||||
return path_concatenate(context->export_path, get_object_data_name(context->object));
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::debug_print_export_graph(const ExportGraph &graph) const
|
||||
{
|
||||
size_t total_graph_size = 0;
|
||||
for (const auto item : graph.items()) {
|
||||
const ObjectIdentifier &parent_info = item.key;
|
||||
const Object *const export_parent = parent_info.object;
|
||||
const Object *const duplicator = parent_info.duplicated_by;
|
||||
|
||||
if (duplicator != nullptr) {
|
||||
fmt::println(" DU {} (as dupped by {}):",
|
||||
export_parent == nullptr ? "-null-" : (export_parent->id.name + 2),
|
||||
duplicator->id.name + 2);
|
||||
}
|
||||
else {
|
||||
fmt::println(" OB {}:",
|
||||
export_parent == nullptr ? "-null-" : (export_parent->id.name + 2));
|
||||
}
|
||||
|
||||
total_graph_size += item.value.size();
|
||||
for (HierarchyContext *child_ctx : item.value) {
|
||||
if (child_ctx->duplicator == nullptr) {
|
||||
fmt::println(" - {}{}{}",
|
||||
child_ctx->export_name.c_str(),
|
||||
child_ctx->weak_export ? " (weak)" : "",
|
||||
child_ctx->original_export_path.empty() ?
|
||||
"" :
|
||||
(std::string("ref ") + child_ctx->original_export_path).c_str());
|
||||
}
|
||||
else {
|
||||
fmt::println(" - {} (dup by {}{}) {}",
|
||||
child_ctx->export_name.c_str(),
|
||||
child_ctx->duplicator->id.name + 2,
|
||||
child_ctx->weak_export ? ", weak" : "",
|
||||
child_ctx->original_export_path.empty() ?
|
||||
"" :
|
||||
(std::string("ref ") + child_ctx->original_export_path).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt::println(" (Total graph size: {} objects)", total_graph_size);
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::export_graph_construct()
|
||||
{
|
||||
/* Add a "null" root node with no children immediately for the case where the top-most node in
|
||||
* the scene is not being exported and a root node otherwise wouldn't get added. */
|
||||
ObjectIdentifier root_node_id = ObjectIdentifier::for_real_object(nullptr);
|
||||
export_graph_.add_new(root_node_id, {});
|
||||
|
||||
DEGObjectIterSettings deg_iter_settings{};
|
||||
deg_iter_settings.depsgraph = depsgraph_;
|
||||
deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
|
||||
DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET;
|
||||
DupliList duplilist;
|
||||
DEG_OBJECT_ITER_BEGIN (°_iter_settings, object) {
|
||||
/* Non-instanced objects always have their object-parent as export-parent. */
|
||||
const bool weak_export = mark_as_weak_export(object);
|
||||
visit_object(object, object->parent, weak_export);
|
||||
|
||||
if (weak_export) {
|
||||
/* If a duplicator shouldn't be exported, its duplilist also shouldn't be. */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Export the duplicated objects instanced by this object. */
|
||||
object_duplilist(depsgraph_, object, nullptr, duplilist);
|
||||
if (!duplilist.is_empty()) {
|
||||
DupliParentFinder dupli_parent_finder;
|
||||
|
||||
for (const DupliObject &dupli_object : duplilist) {
|
||||
if (!should_visit_dupli_object(&dupli_object)) {
|
||||
continue;
|
||||
}
|
||||
dupli_parent_finder.insert(&dupli_object);
|
||||
}
|
||||
|
||||
for (const DupliObject &dupli_object : duplilist) {
|
||||
if (!should_visit_dupli_object(&dupli_object)) {
|
||||
continue;
|
||||
}
|
||||
visit_dupli_object(&dupli_object, object, dupli_parent_finder);
|
||||
}
|
||||
}
|
||||
|
||||
duplilist.clear();
|
||||
}
|
||||
DEG_OBJECT_ITER_END;
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::connect_loose_objects()
|
||||
{
|
||||
/* Find those objects whose parent is not part of the export graph; these
|
||||
* objects would be skipped when traversing the graph as a hierarchy.
|
||||
* These objects will have to be re-attached to some parent object in order to
|
||||
* fit into the hierarchy. */
|
||||
ExportGraph loose_objects_graph = export_graph_;
|
||||
for (const ExportChildren &children : export_graph_.values()) {
|
||||
for (const HierarchyContext *child : children) {
|
||||
/* An object that is marked as a child of another object is not considered 'loose'. */
|
||||
ObjectIdentifier child_oid = ObjectIdentifier::for_hierarchy_context(child);
|
||||
loose_objects_graph.remove(child_oid);
|
||||
}
|
||||
}
|
||||
/* The root of the hierarchy is always found, so it's never considered 'loose'. */
|
||||
loose_objects_graph.remove_contained(ObjectIdentifier::for_graph_root());
|
||||
|
||||
/* Iterate over the loose objects and connect them to their export parent. */
|
||||
for (const ObjectIdentifier &graph_key : loose_objects_graph.keys()) {
|
||||
Object *object = graph_key.object;
|
||||
|
||||
while (true) {
|
||||
/* Loose objects will all be real objects, as duplicated objects always have
|
||||
* their duplicator or other exported duplicated object as ancestor. */
|
||||
|
||||
const bool found = export_graph_.contains(ObjectIdentifier::for_real_object(object->parent));
|
||||
visit_object(object, object->parent, true);
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
/* 'object->parent' will never be nullptr here, as the export graph contains the
|
||||
* root as nullptr and thus will cause a break above. */
|
||||
BLI_assert(object->parent != nullptr);
|
||||
|
||||
object = object->parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool remove_weak_subtrees(const HierarchyContext *context,
|
||||
AbstractHierarchyIterator::ExportGraph &clean_graph,
|
||||
const AbstractHierarchyIterator::ExportGraph &input_graph)
|
||||
{
|
||||
bool all_is_weak = context != nullptr && context->weak_export;
|
||||
const ObjectIdentifier map_key = ObjectIdentifier::for_hierarchy_context(context);
|
||||
|
||||
const AbstractHierarchyIterator::ExportChildren *children = input_graph.lookup_ptr(map_key);
|
||||
if (children) {
|
||||
for (HierarchyContext *child_context : *children) {
|
||||
bool child_tree_is_weak = remove_weak_subtrees(child_context, clean_graph, input_graph);
|
||||
all_is_weak &= child_tree_is_weak;
|
||||
|
||||
if (child_tree_is_weak) {
|
||||
/* This subtree is all weak, so we can remove it from the current object's children. */
|
||||
clean_graph.lookup(map_key).remove(child_context);
|
||||
delete child_context;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (all_is_weak) {
|
||||
/* This node and all its children are weak, so it can be removed from the export graph. */
|
||||
clean_graph.remove(map_key);
|
||||
}
|
||||
|
||||
return all_is_weak;
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::export_graph_prune()
|
||||
{
|
||||
/* Take a copy of the map so that we can modify while recusing. */
|
||||
ExportGraph unpruned_export_graph = export_graph_;
|
||||
remove_weak_subtrees(HierarchyContext::root(), export_graph_, unpruned_export_graph);
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::export_graph_clear()
|
||||
{
|
||||
for (const ExportChildren &children : export_graph_.values()) {
|
||||
for (HierarchyContext *context : children) {
|
||||
delete context;
|
||||
}
|
||||
}
|
||||
export_graph_.clear();
|
||||
used_names_.clear_and_keep_capacity();
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::visit_object(Object *object,
|
||||
Object *export_parent,
|
||||
bool weak_export)
|
||||
{
|
||||
HierarchyContext *context = new HierarchyContext();
|
||||
context->object = object;
|
||||
context->is_object_data_context = false;
|
||||
context->export_name = get_object_name(object, export_parent);
|
||||
context->export_parent = export_parent;
|
||||
context->duplicator = nullptr;
|
||||
context->weak_export = weak_export;
|
||||
context->animation_check_include_parent = false;
|
||||
context->export_path = "";
|
||||
context->original_export_path = "";
|
||||
context->higher_up_export_path = "";
|
||||
context->is_duplisource = false;
|
||||
context->matrix_world = object->object_to_world();
|
||||
|
||||
ObjectIdentifier graph_index = determine_graph_index_object(context);
|
||||
context_update_for_graph_index(context, graph_index);
|
||||
|
||||
/* Store this HierarchyContext as child of the export parent. */
|
||||
export_graph_.lookup_or_add(graph_index, {}).add_new(context);
|
||||
|
||||
/* Create an empty entry for this object to indicate it is part of the export. This will be used
|
||||
* by connect_loose_objects(). Having such an "indicator" will make it possible to do an O(log n)
|
||||
* check on whether an object is part of the export, rather than having to check all objects in
|
||||
* the map. Note that it's not possible to simply search for (object->parent, nullptr), as the
|
||||
* object's parent in Blender may not be the same as its export-parent. */
|
||||
ObjectIdentifier object_key = ObjectIdentifier::for_real_object(object);
|
||||
export_graph_.add(object_key, {});
|
||||
}
|
||||
|
||||
ObjectIdentifier AbstractHierarchyIterator::determine_graph_index_object(
|
||||
const HierarchyContext *context)
|
||||
{
|
||||
return ObjectIdentifier::for_real_object(context->export_parent);
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::visit_dupli_object(const DupliObject *dupli_object,
|
||||
Object *duplicator,
|
||||
const DupliParentFinder &dupli_parent_finder)
|
||||
{
|
||||
HierarchyContext *context = new HierarchyContext();
|
||||
context->object = dupli_object->ob;
|
||||
context->is_object_data_context = false;
|
||||
context->duplicator = duplicator;
|
||||
context->persistent_id = PersistentID(dupli_object);
|
||||
context->weak_export = false;
|
||||
context->export_path = "";
|
||||
context->original_export_path = "";
|
||||
context->animation_check_include_parent = false;
|
||||
context->is_duplisource = false;
|
||||
context->matrix_world = float4x4(dupli_object->mat);
|
||||
|
||||
/* Construct export name for the dupli-instance. */
|
||||
std::string export_name = get_object_name(context->object) + "-" +
|
||||
context->persistent_id.as_object_name_suffix();
|
||||
|
||||
Set<std::string> &used_names = used_names_.lookup_or_add(duplicator->id.name, {});
|
||||
context->export_name = make_unique_name(make_valid_name(export_name), used_names);
|
||||
|
||||
ObjectIdentifier graph_index = determine_graph_index_dupli(
|
||||
context, dupli_object, dupli_parent_finder);
|
||||
context_update_for_graph_index(context, graph_index);
|
||||
|
||||
export_graph_.lookup_or_add(graph_index, {}).add_new(context);
|
||||
|
||||
if (dupli_object->ob) {
|
||||
this->duplisources_.add(&dupli_object->ob->id);
|
||||
}
|
||||
}
|
||||
|
||||
ObjectIdentifier AbstractHierarchyIterator::determine_graph_index_dupli(
|
||||
const HierarchyContext *context,
|
||||
const DupliObject *dupli_object,
|
||||
const DupliParentFinder &dupli_parent_finder)
|
||||
{
|
||||
const DupliObject *dupli_parent = dupli_parent_finder.find_suitable_export_parent(dupli_object);
|
||||
|
||||
if (dupli_parent != nullptr) {
|
||||
return ObjectIdentifier::for_duplicated_object(dupli_parent, context->duplicator);
|
||||
}
|
||||
return ObjectIdentifier::for_real_object(context->duplicator);
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::context_update_for_graph_index(
|
||||
HierarchyContext *context, const ObjectIdentifier &graph_index) const
|
||||
{
|
||||
/* Update the HierarchyContext so that it is consistent with the graph index. */
|
||||
context->export_parent = graph_index.object;
|
||||
|
||||
/* If the parent type is such that it cannot be exported (at least not currently to USD or
|
||||
* Alembic), always check the parent for animation. */
|
||||
const eObject_Partype partype = context->object->partype & PARTYPE;
|
||||
context->animation_check_include_parent |= ELEM(partype, PARBONE, PARVERT1, PARVERT3, PARSKEL);
|
||||
|
||||
if (context->export_parent != context->object->parent) {
|
||||
/* The parent object in Blender is NOT used as the export parent. This means
|
||||
* that the world transform of this object can be influenced by objects that
|
||||
* are not part of its export graph. */
|
||||
context->animation_check_include_parent = true;
|
||||
}
|
||||
}
|
||||
|
||||
AbstractHierarchyIterator::ExportChildren *AbstractHierarchyIterator::graph_children(
|
||||
const HierarchyContext *context)
|
||||
{
|
||||
/* Note: `graph_children` is called during recursive iteration and MUST NOT change the export
|
||||
* graph, which would invalidate the iteration. As a result, we cannot add an entry in the
|
||||
* graph if the incoming `context` is not found. */
|
||||
return export_graph_.lookup_ptr(ObjectIdentifier::for_hierarchy_context(context));
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::determine_export_paths(const HierarchyContext *parent_context)
|
||||
{
|
||||
const std::string &parent_export_path = parent_context ? parent_context->export_path : "";
|
||||
|
||||
const ExportChildren *children = graph_children(parent_context);
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (HierarchyContext *context : *children) {
|
||||
context->export_path = path_concatenate(parent_export_path, context->export_name);
|
||||
|
||||
if (context->duplicator == nullptr) {
|
||||
/* This is an original (i.e. non-instanced) object, so we should keep track of where it was
|
||||
* exported to, just in case it gets instanced somewhere. */
|
||||
ID *source_ob = &context->object->id;
|
||||
duplisource_export_path_.add(source_ob, context->export_path);
|
||||
|
||||
if (context->object->data != nullptr) {
|
||||
ID *source_data = context->object->data;
|
||||
duplisource_export_path_.add(source_data, get_object_data_path(context));
|
||||
}
|
||||
}
|
||||
|
||||
determine_export_paths(context);
|
||||
}
|
||||
}
|
||||
|
||||
bool AbstractHierarchyIterator::determine_duplication_references(
|
||||
const HierarchyContext *parent_context, const std::string &indent)
|
||||
{
|
||||
const ExportChildren *children = graph_children(parent_context);
|
||||
if (!children) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Will be set to true if any child contexts are instances that were designated
|
||||
* as proxies for the original prototype. */
|
||||
bool contains_proxy_prototype = false;
|
||||
|
||||
for (HierarchyContext *context : *children) {
|
||||
if (context->duplicator != nullptr) {
|
||||
ID *source_id = &context->object->id;
|
||||
const std::string *source_path = duplisource_export_path_.lookup_ptr(source_id);
|
||||
if (!source_path) {
|
||||
/* The original was not found, so mark this instance as "the original". */
|
||||
context->mark_as_not_instanced();
|
||||
duplisource_export_path_.add_new(source_id, context->export_path);
|
||||
contains_proxy_prototype = true;
|
||||
}
|
||||
else {
|
||||
context->mark_as_instance_of(*source_path);
|
||||
}
|
||||
|
||||
if (context->object->data) {
|
||||
ID *source_data_id = context->object->data;
|
||||
if (!duplisource_export_path_.contains(source_data_id)) {
|
||||
/* The original was not found, so mark this instance as "original". */
|
||||
std::string data_path = get_object_data_path(context);
|
||||
context->mark_as_not_instanced();
|
||||
duplisource_export_path_.add_overwrite(source_id, context->export_path);
|
||||
duplisource_export_path_.add_new(source_data_id, data_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Determine is this context is for an instance prototype. */
|
||||
ID *id = &context->object->id;
|
||||
if (duplisources_.contains(id)) {
|
||||
context->is_duplisource = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (determine_duplication_references(context, indent + " ")) {
|
||||
/* A descendant was designated a prototype proxy. If the current context
|
||||
* is an instance, we must change it to a prototype proxy as well. */
|
||||
if (context->is_instance()) {
|
||||
context->mark_as_not_instanced();
|
||||
ID *source_id = &context->object->id;
|
||||
duplisource_export_path_.add_overwrite(source_id, context->export_path);
|
||||
}
|
||||
contains_proxy_prototype = true;
|
||||
}
|
||||
}
|
||||
return contains_proxy_prototype;
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::make_writers(const HierarchyContext *parent_context)
|
||||
{
|
||||
float4x4 parent_matrix_inv_world;
|
||||
|
||||
if (parent_context) {
|
||||
parent_matrix_inv_world = math::invert(parent_context->matrix_world);
|
||||
}
|
||||
else {
|
||||
parent_matrix_inv_world = float4x4::identity();
|
||||
}
|
||||
|
||||
const ExportChildren *children = graph_children(parent_context);
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_point_instance_ancestor = false;
|
||||
if (parent_context &&
|
||||
(parent_context->is_point_instance || parent_context->has_point_instance_ancestor))
|
||||
{
|
||||
has_point_instance_ancestor = true;
|
||||
}
|
||||
|
||||
for (HierarchyContext *context : *children) {
|
||||
context->has_point_instance_ancestor = has_point_instance_ancestor;
|
||||
|
||||
/* Update the context so that it is correct for this parent-child relation. */
|
||||
context->parent_matrix_inv_world = parent_matrix_inv_world;
|
||||
if (parent_context != nullptr) {
|
||||
context->higher_up_export_path = parent_context->export_path;
|
||||
}
|
||||
|
||||
/* Get or create the transform writer. */
|
||||
EnsuredWriter transform_writer = ensure_writer(
|
||||
context, &AbstractHierarchyIterator::create_transform_writer);
|
||||
|
||||
if (!transform_writer) {
|
||||
/* Unable to export, so there is nothing to attach any children to; just abort this entire
|
||||
* branch of the export hierarchy. */
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool need_writers = context->is_point_proto || (!context->is_point_instance &&
|
||||
!context->has_point_instance_ancestor);
|
||||
|
||||
BLI_assert(DEG_is_evaluated_id(&context->object->id));
|
||||
if ((transform_writer.is_newly_created() || export_subset_.transforms) && need_writers) {
|
||||
/* XXX This can lead to too many XForms being written. For example, a camera writer can
|
||||
* refuse to write an orthographic camera. By the time that this is known, the XForm has
|
||||
* already been written. */
|
||||
transform_writer->write(*context);
|
||||
}
|
||||
|
||||
if (!context->weak_export && include_data_writers(context) && need_writers) {
|
||||
make_writers_particle_systems(context);
|
||||
make_writer_object_data(context);
|
||||
}
|
||||
|
||||
if (include_child_writers(context)) {
|
||||
/* Recurse into this object's children. */
|
||||
make_writers(context);
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO(Sybren): iterate over all unused writers and call unused_during_iteration() or something.
|
||||
*/
|
||||
}
|
||||
|
||||
HierarchyContext AbstractHierarchyIterator::context_for_object_data(
|
||||
const HierarchyContext *object_context) const
|
||||
{
|
||||
HierarchyContext data_context = *object_context;
|
||||
data_context.is_object_data_context = true;
|
||||
data_context.higher_up_export_path = object_context->export_path;
|
||||
data_context.export_name = get_object_data_name(data_context.object);
|
||||
data_context.export_path = path_concatenate(data_context.higher_up_export_path,
|
||||
data_context.export_name);
|
||||
|
||||
const ObjectIdentifier object_key = ObjectIdentifier::for_hierarchy_context(&data_context);
|
||||
const ExportChildren *children = export_graph_.lookup_ptr(object_key);
|
||||
data_context.is_parent = children ? (children->size() > 0) : false;
|
||||
|
||||
return data_context;
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::make_writer_object_data(const HierarchyContext *context)
|
||||
{
|
||||
if (context->object->data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
HierarchyContext data_context = context_for_object_data(context);
|
||||
if (data_context.is_instance()) {
|
||||
ID *object_data = context->object->data;
|
||||
data_context.original_export_path = duplisource_export_path_.lookup(object_data);
|
||||
|
||||
/* If the object is marked as an instance, so should the object data. */
|
||||
BLI_assert(data_context.is_instance());
|
||||
}
|
||||
|
||||
/* Always write upon creation, otherwise depend on which subset is active. */
|
||||
EnsuredWriter data_writer = ensure_writer(&data_context,
|
||||
&AbstractHierarchyIterator::create_data_writer);
|
||||
if (!data_writer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data_writer.is_newly_created() || export_subset_.shapes) {
|
||||
data_writer->write(data_context);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractHierarchyIterator::make_writers_particle_systems(
|
||||
const HierarchyContext *transform_context)
|
||||
{
|
||||
Object *object = transform_context->object;
|
||||
ParticleSystem *psys = static_cast<ParticleSystem *>(object->particlesystem.first);
|
||||
for (; psys; psys = psys->next) {
|
||||
if (!psys_check_enabled(object, psys, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HierarchyContext hair_context = *transform_context;
|
||||
hair_context.export_name = make_valid_name(psys->name);
|
||||
hair_context.export_path = path_concatenate(transform_context->export_path,
|
||||
hair_context.export_name);
|
||||
hair_context.higher_up_export_path = transform_context->export_path;
|
||||
hair_context.particle_system = psys;
|
||||
|
||||
EnsuredWriter writer;
|
||||
switch (psys->part->type) {
|
||||
case PART_HAIR:
|
||||
writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_hair_writer);
|
||||
break;
|
||||
case PART_EMITTER:
|
||||
case PART_FLUID_FLIP:
|
||||
case PART_FLUID_SPRAY:
|
||||
case PART_FLUID_BUBBLE:
|
||||
case PART_FLUID_FOAM:
|
||||
case PART_FLUID_TRACER:
|
||||
case PART_FLUID_SPRAYFOAM:
|
||||
case PART_FLUID_SPRAYBUBBLE:
|
||||
case PART_FLUID_FOAMBUBBLE:
|
||||
case PART_FLUID_SPRAYFOAMBUBBLE:
|
||||
writer = ensure_writer(&hair_context, &AbstractHierarchyIterator::create_particle_writer);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!writer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Always write upon creation, otherwise depend on which subset is active. */
|
||||
if (writer.is_newly_created() || export_subset_.shapes) {
|
||||
writer->write(hair_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::get_object_name(const Object *object) const
|
||||
{
|
||||
return get_id_name(&object->id);
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::get_object_name(const Object *object, const Object *parent)
|
||||
{
|
||||
Set<std::string> &used_names = used_names_.lookup_or_add(parent ? parent->id.name : "", {});
|
||||
return make_unique_name(object->id.name + 2, used_names);
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::get_object_data_name(const Object *object) const
|
||||
{
|
||||
const ID *object_data = object->data;
|
||||
return get_id_name(object_data);
|
||||
}
|
||||
|
||||
AbstractHierarchyWriter *AbstractHierarchyIterator::get_writer(
|
||||
const std::string &export_path) const
|
||||
{
|
||||
return writers_.lookup_default(export_path, nullptr);
|
||||
}
|
||||
|
||||
EnsuredWriter AbstractHierarchyIterator::ensure_writer(
|
||||
const HierarchyContext *context, AbstractHierarchyIterator::create_writer_func create_func)
|
||||
{
|
||||
AbstractHierarchyWriter *writer = get_writer(context->export_path);
|
||||
if (writer != nullptr) {
|
||||
return EnsuredWriter::existing(writer);
|
||||
}
|
||||
|
||||
writer = (this->*create_func)(context);
|
||||
if (writer == nullptr) {
|
||||
return EnsuredWriter::empty();
|
||||
}
|
||||
|
||||
writers_.add_new(context->export_path, writer);
|
||||
return EnsuredWriter::newly_created(writer);
|
||||
}
|
||||
|
||||
std::string AbstractHierarchyIterator::path_concatenate(const std::string &parent_path,
|
||||
const std::string &child_path) const
|
||||
{
|
||||
return parent_path + "/" + child_path;
|
||||
}
|
||||
|
||||
bool AbstractHierarchyIterator::mark_as_weak_export(const Object * /*object*/) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool AbstractHierarchyIterator::should_visit_dupli_object(const DupliObject *dupli_object) const
|
||||
{
|
||||
/* Do not visit dupli objects if their `no_draw` flag is set (things like custom bone shapes) or
|
||||
* if they are meta-balls / text objects / NURBS surfaces. */
|
||||
if (dupli_object->no_draw || ELEM(dupli_object->ob->type, OB_MBALL, OB_FONT, OB_SURF)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,348 @@
|
||||
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_abstract_hierarchy_iterator.h"
|
||||
|
||||
#include "tests/blendfile_loading_base_test.h"
|
||||
|
||||
#include "BKE_scene.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLO_readfile.hh"
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
namespace {
|
||||
|
||||
/* Mapping from ID.name to set of export hierarchy path. Duplicated objects can be exported
|
||||
* multiple times with different export paths, hence the set. */
|
||||
using used_writers = Map<std::string, Set<std::string>>;
|
||||
|
||||
class TestHierarchyWriter : public AbstractHierarchyWriter {
|
||||
public:
|
||||
std::string writer_type;
|
||||
used_writers &writers_map;
|
||||
|
||||
TestHierarchyWriter(const std::string &writer_type, used_writers &writers_map)
|
||||
: writer_type(writer_type), writers_map(writers_map)
|
||||
{
|
||||
}
|
||||
|
||||
void write(HierarchyContext &context) override
|
||||
{
|
||||
const char *id_name = context.object->id.name;
|
||||
Set<std::string> &writers = writers_map.lookup_or_add(id_name, {});
|
||||
|
||||
if (writers.contains(context.export_path)) {
|
||||
ADD_FAILURE() << "Unexpectedly found another " << writer_type << " writer for " << id_name
|
||||
<< " to export to " << context.export_path;
|
||||
}
|
||||
writers.add_new(context.export_path);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestingHierarchyIterator : public AbstractHierarchyIterator {
|
||||
public: /* Public so that the test cases can directly inspect the created writers. */
|
||||
used_writers transform_writers;
|
||||
used_writers data_writers;
|
||||
used_writers hair_writers;
|
||||
used_writers particle_writers;
|
||||
|
||||
explicit TestingHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
|
||||
: AbstractHierarchyIterator(bmain, depsgraph)
|
||||
{
|
||||
}
|
||||
~TestingHierarchyIterator() override
|
||||
{
|
||||
release_writers();
|
||||
}
|
||||
|
||||
protected:
|
||||
AbstractHierarchyWriter *create_transform_writer(const HierarchyContext * /*context*/) override
|
||||
{
|
||||
return new TestHierarchyWriter("transform", transform_writers);
|
||||
}
|
||||
AbstractHierarchyWriter *create_data_writer(const HierarchyContext * /*context*/) override
|
||||
{
|
||||
return new TestHierarchyWriter("data", data_writers);
|
||||
}
|
||||
AbstractHierarchyWriter *create_hair_writer(const HierarchyContext * /*context*/) override
|
||||
{
|
||||
return new TestHierarchyWriter("hair", hair_writers);
|
||||
}
|
||||
AbstractHierarchyWriter *create_particle_writer(const HierarchyContext * /*context*/) override
|
||||
{
|
||||
return new TestHierarchyWriter("particle", particle_writers);
|
||||
}
|
||||
|
||||
void release_writer(AbstractHierarchyWriter *writer) override
|
||||
{
|
||||
delete writer;
|
||||
}
|
||||
};
|
||||
|
||||
class AbstractHierarchyIteratorTest : public BlendfileLoadingBaseTest {
|
||||
protected:
|
||||
TestingHierarchyIterator *iterator;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
BlendfileLoadingBaseTest::SetUp();
|
||||
iterator = nullptr;
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
iterator_free();
|
||||
BlendfileLoadingBaseTest::TearDown();
|
||||
}
|
||||
|
||||
/* Create a test iterator. */
|
||||
void iterator_create()
|
||||
{
|
||||
iterator = new TestingHierarchyIterator(bfile->main, depsgraph);
|
||||
}
|
||||
/* Free the test iterator if it is not nullptr. */
|
||||
void iterator_free()
|
||||
{
|
||||
if (iterator == nullptr) {
|
||||
return;
|
||||
}
|
||||
delete iterator;
|
||||
iterator = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AbstractHierarchyIteratorTest, ExportHierarchyTest)
|
||||
{
|
||||
/* Load the test blend file. */
|
||||
if (!blendfile_load("usd" SEP_STR "usd_hierarchy_export_test.blend")) {
|
||||
return;
|
||||
}
|
||||
depsgraph_create(DAG_EVAL_RENDER);
|
||||
iterator_create();
|
||||
|
||||
iterator->iterate_and_write();
|
||||
|
||||
/* Mapping from object name to set of export paths. */
|
||||
used_writers expected_transforms = {
|
||||
{"OBCamera", {"/Camera"}},
|
||||
{"OBDupli1", {"/Dupli1"}},
|
||||
{"OBDupli2", {"/ParentOfDupli2/Dupli2"}},
|
||||
{"OBGEO_Ear_L",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1"}},
|
||||
{"OBGEO_Ear_R",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2"}},
|
||||
{"OBGEO_Head",
|
||||
{"/Dupli1/GEO_Head-0",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0"}},
|
||||
{"OBGEO_Nose",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Nose-3",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3"}},
|
||||
{"OBGround plane", {"/Ground plane"}},
|
||||
{"OBOutsideDupliGrandParent", {"/Ground plane/OutsideDupliGrandParent"}},
|
||||
{"OBOutsideDupliParent", {"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent"}},
|
||||
{"OBParentOfDupli2", {"/ParentOfDupli2"}}};
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
|
||||
used_writers expected_data = {
|
||||
{"OBCamera", {"/Camera/Camera"}},
|
||||
{"OBGEO_Ear_L",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1/Ear",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1/Ear"}},
|
||||
{"OBGEO_Ear_R",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2/Ear",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2/Ear"}},
|
||||
{"OBGEO_Head",
|
||||
{"/Dupli1/GEO_Head-0/Face",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/Face"}},
|
||||
{"OBGEO_Nose",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Nose-3/Nose",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3/Nose"}},
|
||||
{"OBGround plane", {"/Ground plane/Plane"}},
|
||||
{"OBParentOfDupli2", {"/ParentOfDupli2/Icosphere"}},
|
||||
};
|
||||
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
|
||||
/* The scene has no hair or particle systems. */
|
||||
EXPECT_EQ(0, iterator->hair_writers.size());
|
||||
EXPECT_EQ(0, iterator->particle_writers.size());
|
||||
|
||||
/* On the second iteration, everything should be written as well.
|
||||
* This tests the default value of iterator->export_subset_. */
|
||||
iterator->transform_writers.clear();
|
||||
iterator->data_writers.clear();
|
||||
iterator->iterate_and_write();
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
}
|
||||
|
||||
TEST_F(AbstractHierarchyIteratorTest, ExportSubsetTest)
|
||||
{
|
||||
/* The scene has no hair or particle systems, and this is already covered by ExportHierarchyTest,
|
||||
* so not included here. Update this test when hair & particle systems are included. */
|
||||
|
||||
/* Load the test blend file. */
|
||||
if (!blendfile_load("usd" SEP_STR "usd_hierarchy_export_test.blend")) {
|
||||
return;
|
||||
}
|
||||
depsgraph_create(DAG_EVAL_RENDER);
|
||||
iterator_create();
|
||||
|
||||
/* Mapping from object name to set of export paths. */
|
||||
used_writers expected_transforms = {
|
||||
{"OBCamera", {"/Camera"}},
|
||||
{"OBDupli1", {"/Dupli1"}},
|
||||
{"OBDupli2", {"/ParentOfDupli2/Dupli2"}},
|
||||
{"OBGEO_Ear_L",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1"}},
|
||||
{"OBGEO_Ear_R",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2"}},
|
||||
{"OBGEO_Head",
|
||||
{"/Dupli1/GEO_Head-0",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0"}},
|
||||
{"OBGEO_Nose",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Nose-3",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3"}},
|
||||
{"OBGround plane", {"/Ground plane"}},
|
||||
{"OBOutsideDupliGrandParent", {"/Ground plane/OutsideDupliGrandParent"}},
|
||||
{"OBOutsideDupliParent", {"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent"}},
|
||||
{"OBParentOfDupli2", {"/ParentOfDupli2"}}};
|
||||
|
||||
used_writers expected_data = {
|
||||
{"OBCamera", {"/Camera/Camera"}},
|
||||
{"OBGEO_Ear_L",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_L-1/Ear",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_L-1/Ear"}},
|
||||
{"OBGEO_Ear_R",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Ear_R-2/Ear",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Ear_R-2/Ear"}},
|
||||
{"OBGEO_Head",
|
||||
{"/Dupli1/GEO_Head-0/Face",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/Face"}},
|
||||
{"OBGEO_Nose",
|
||||
{"/Dupli1/GEO_Head-0/GEO_Nose-3/Nose",
|
||||
"/Ground plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose",
|
||||
"/ParentOfDupli2/Dupli2/GEO_Head-0/GEO_Nose-3/Nose"}},
|
||||
{"OBGround plane", {"/Ground plane/Plane"}},
|
||||
{"OBParentOfDupli2", {"/ParentOfDupli2/Icosphere"}},
|
||||
};
|
||||
|
||||
/* Even when only asking an export of transforms, on the first frame everything should be
|
||||
* exported. */
|
||||
{
|
||||
ExportSubset export_subset = {false};
|
||||
export_subset.transforms = true;
|
||||
export_subset.shapes = false;
|
||||
iterator->set_export_subset(export_subset);
|
||||
}
|
||||
iterator->iterate_and_write();
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
|
||||
/* Clear data to prepare for the next iteration. */
|
||||
iterator->transform_writers.clear();
|
||||
iterator->data_writers.clear();
|
||||
|
||||
/* Second iteration, should only write transforms now. */
|
||||
iterator->iterate_and_write();
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
EXPECT_EQ(0, iterator->data_writers.size());
|
||||
|
||||
/* Clear data to prepare for the next iteration. */
|
||||
iterator->transform_writers.clear();
|
||||
iterator->data_writers.clear();
|
||||
|
||||
/* Third iteration, should only write data now. */
|
||||
{
|
||||
ExportSubset export_subset = {false};
|
||||
export_subset.transforms = false;
|
||||
export_subset.shapes = true;
|
||||
iterator->set_export_subset(export_subset);
|
||||
}
|
||||
iterator->iterate_and_write();
|
||||
EXPECT_EQ(0, iterator->transform_writers.size());
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
|
||||
/* Clear data to prepare for the next iteration. */
|
||||
iterator->transform_writers.clear();
|
||||
iterator->data_writers.clear();
|
||||
|
||||
/* Fourth iteration, should export everything now. */
|
||||
{
|
||||
ExportSubset export_subset = {false};
|
||||
export_subset.transforms = true;
|
||||
export_subset.shapes = true;
|
||||
iterator->set_export_subset(export_subset);
|
||||
}
|
||||
iterator->iterate_and_write();
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
}
|
||||
|
||||
/* Test class that constructs a depsgraph in such a way that it includes invisible objects. */
|
||||
class AbstractHierarchyIteratorInvisibleTest : public AbstractHierarchyIteratorTest {
|
||||
protected:
|
||||
void depsgraph_create(eEvaluationMode depsgraph_evaluation_mode) override
|
||||
{
|
||||
depsgraph = DEG_graph_new(
|
||||
bfile->main, bfile->curscene, bfile->cur_view_layer, depsgraph_evaluation_mode);
|
||||
DEG_graph_build_for_all_objects(depsgraph);
|
||||
BKE_scene_graph_update_tagged(depsgraph, bfile->main);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AbstractHierarchyIteratorInvisibleTest, ExportInvisibleTest)
|
||||
{
|
||||
if (!blendfile_load("alembic" SEP_STR "visibility.blend")) {
|
||||
return;
|
||||
}
|
||||
depsgraph_create(DAG_EVAL_RENDER);
|
||||
iterator_create();
|
||||
|
||||
iterator->iterate_and_write();
|
||||
|
||||
/* Mapping from object name to set of export paths. */
|
||||
used_writers expected_transforms = {{"OBInvisibleAnimatedCube", {"/InvisibleAnimatedCube"}},
|
||||
{"OBInvisibleCube", {"/InvisibleCube"}},
|
||||
{"OBVisibleCube", {"/VisibleCube"}}};
|
||||
EXPECT_EQ(expected_transforms, iterator->transform_writers);
|
||||
|
||||
used_writers expected_data = {{"OBInvisibleAnimatedCube", {"/InvisibleAnimatedCube/Cube"}},
|
||||
{"OBInvisibleCube", {"/InvisibleCube/Cube"}},
|
||||
{"OBVisibleCube", {"/VisibleCube/Cube"}}};
|
||||
|
||||
EXPECT_EQ(expected_data, iterator->data_writers);
|
||||
|
||||
/* The scene has no hair or particle systems. */
|
||||
EXPECT_EQ(0, iterator->hair_writers.size());
|
||||
EXPECT_EQ(0, iterator->particle_writers.size());
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,81 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "dupli_parent_finder.hh"
|
||||
|
||||
#include "BLI_assert.h"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
void DupliParentFinder::insert(const DupliObject *dupli_ob)
|
||||
{
|
||||
dupli_set_.add(dupli_ob->ob);
|
||||
|
||||
PersistentID dupli_pid(dupli_ob);
|
||||
pid_to_dupli_.add(dupli_pid, dupli_ob);
|
||||
instancer_pid_to_duplis_.lookup_or_add(dupli_pid.instancer_pid(), {}).add(dupli_ob);
|
||||
}
|
||||
|
||||
bool DupliParentFinder::is_duplicated(const Object *object) const
|
||||
{
|
||||
return dupli_set_.contains(object);
|
||||
}
|
||||
|
||||
const DupliObject *DupliParentFinder::find_suitable_export_parent(
|
||||
const DupliObject *dupli_ob) const
|
||||
{
|
||||
if (dupli_ob->ob->parent != nullptr) {
|
||||
const DupliObject *parent = find_duplicated_parent(dupli_ob);
|
||||
if (parent != nullptr) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
return find_instancer(dupli_ob);
|
||||
}
|
||||
|
||||
const DupliObject *DupliParentFinder::find_duplicated_parent(const DupliObject *dupli_ob) const
|
||||
{
|
||||
const PersistentID dupli_pid(dupli_ob);
|
||||
PersistentID parent_pid = dupli_pid.instancer_pid();
|
||||
|
||||
const Object *parent_ob = dupli_ob->ob->parent;
|
||||
BLI_assert(parent_ob != nullptr);
|
||||
|
||||
const Set<const DupliObject *> *found = instancer_pid_to_duplis_.lookup_ptr(parent_pid);
|
||||
if (!found) {
|
||||
/* Unexpected, as there should be at least one entry here, for the dupli_ob itself. */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (const DupliObject *potential_parent_dupli : *found) {
|
||||
if (potential_parent_dupli->ob != parent_ob) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PersistentID potential_parent_pid(potential_parent_dupli);
|
||||
if (potential_parent_pid.is_from_same_instancer_as(dupli_pid)) {
|
||||
return potential_parent_dupli;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const DupliObject *DupliParentFinder::find_instancer(const DupliObject *dupli_ob) const
|
||||
{
|
||||
PersistentID dupli_pid(dupli_ob);
|
||||
PersistentID parent_pid = dupli_pid.instancer_pid();
|
||||
|
||||
const DupliObject *const *found = pid_to_dupli_.lookup_ptr(parent_pid);
|
||||
if (!found) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const DupliObject *instancer_dupli = *found;
|
||||
return instancer_dupli;
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,41 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "IO_dupli_persistent_id.hh"
|
||||
|
||||
#include "BKE_duplilist.hh"
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
/* Find relations between duplicated objects. This class should be instanced for a single real
|
||||
* object, and fed its dupli-objects. */
|
||||
class DupliParentFinder final {
|
||||
private:
|
||||
/* To check whether an Object * is instanced by this duplicator. */
|
||||
Set<const Object *> dupli_set_;
|
||||
|
||||
/* To find the DupliObject given its Persistent ID. */
|
||||
using PIDToDupliMap = Map<const PersistentID, const DupliObject *>;
|
||||
PIDToDupliMap pid_to_dupli_;
|
||||
|
||||
/* Mapping from instancer PID to duplis instanced by it. */
|
||||
using InstancerPIDToDuplisMap = Map<const PersistentID, Set<const DupliObject *>>;
|
||||
InstancerPIDToDuplisMap instancer_pid_to_duplis_;
|
||||
|
||||
public:
|
||||
void insert(const DupliObject *dupli_ob);
|
||||
|
||||
bool is_duplicated(const Object *object) const;
|
||||
const DupliObject *find_suitable_export_parent(const DupliObject *dupli_ob) const;
|
||||
|
||||
private:
|
||||
const DupliObject *find_duplicated_parent(const DupliObject *dupli_ob) const;
|
||||
const DupliObject *find_instancer(const DupliObject *dupli_ob) const;
|
||||
};
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,126 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "dupli_parent_finder.hh"
|
||||
|
||||
#include <climits>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
PersistentID::PersistentID()
|
||||
{
|
||||
persistent_id_[0] = INT_MAX;
|
||||
}
|
||||
|
||||
PersistentID::PersistentID(const DupliObject *dupli_ob)
|
||||
{
|
||||
for (int index = 0; index < array_length_; ++index) {
|
||||
persistent_id_[index] = dupli_ob->persistent_id[index];
|
||||
}
|
||||
}
|
||||
|
||||
PersistentID::PersistentID(const PIDArray &persistent_id_values)
|
||||
{
|
||||
persistent_id_ = persistent_id_values;
|
||||
}
|
||||
|
||||
bool PersistentID::is_from_same_instancer_as(const PersistentID &other) const
|
||||
{
|
||||
if (persistent_id_[0] == INT_MAX || other.persistent_id_[0] == INT_MAX) {
|
||||
/* Either one or the other is not instanced at all, so definitely not from the same instancer.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Start at index 1 to skip the first digit. */
|
||||
for (int index = 1; index < array_length_; ++index) {
|
||||
const int pid_digit_a = persistent_id_[index];
|
||||
const int pid_digit_b = other.persistent_id_[index];
|
||||
|
||||
if (pid_digit_a != pid_digit_b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid_digit_a == INT_MAX) {
|
||||
/* Both persistent IDs were identical so far, and this marks the end of the useful data. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PersistentID PersistentID::instancer_pid() const
|
||||
{
|
||||
if (persistent_id_[0] == INT_MAX) {
|
||||
return PersistentID();
|
||||
}
|
||||
|
||||
/* Left-shift the entire PID by 1. */
|
||||
PIDArray new_pid_values;
|
||||
int index;
|
||||
for (index = 0; index < array_length_ - 1; ++index) {
|
||||
new_pid_values[index] = persistent_id_[index + 1];
|
||||
}
|
||||
new_pid_values[index] = INT_MAX;
|
||||
|
||||
return PersistentID(new_pid_values);
|
||||
}
|
||||
|
||||
std::string PersistentID::as_object_name_suffix() const
|
||||
{
|
||||
fmt::basic_memory_buffer<char, 64> buf;
|
||||
|
||||
/* Find one past the last index. */
|
||||
int index;
|
||||
for (index = 0; index < array_length_ && persistent_id_[index] < INT_MAX; ++index) {
|
||||
;
|
||||
}
|
||||
|
||||
/* Iterate backward to construct the string. */
|
||||
--index;
|
||||
for (; index >= 0; --index) {
|
||||
fmt::format_to(fmt::appender(buf), "{}", persistent_id_[index]);
|
||||
if (index > 0) {
|
||||
fmt::format_to(fmt::appender(buf), "-");
|
||||
}
|
||||
}
|
||||
|
||||
return fmt::to_string(buf);
|
||||
}
|
||||
|
||||
uint64_t PersistentID::hash() const
|
||||
{
|
||||
uint64_t hash = 5381;
|
||||
for (const int value : persistent_id_) {
|
||||
if (value == INT_MAX) {
|
||||
break;
|
||||
}
|
||||
hash = hash * 33 ^ uint64_t(value);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool operator==(const PersistentID &persistent_id_a, const PersistentID &persistent_id_b)
|
||||
{
|
||||
const PersistentID::PIDArray &pid_a = persistent_id_a.persistent_id_;
|
||||
const PersistentID::PIDArray &pid_b = persistent_id_b.persistent_id_;
|
||||
|
||||
for (int index = 0; index < PersistentID::array_length_; ++index) {
|
||||
const int pid_digit_a = pid_a[index];
|
||||
const int pid_digit_b = pid_b[index];
|
||||
|
||||
if (pid_digit_a != pid_digit_b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid_a[index] == INT_MAX) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
55
blender-5.2.0/source/blender/io/common/intern/mesh_utils.cc
Normal file
55
blender-5.2.0/source/blender/io/common/intern/mesh_utils.cc
Normal file
@@ -0,0 +1,55 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "DNA_ID.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_object_types.hh"
|
||||
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
#include "IO_mesh_utils.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
const Mesh *mesh_coerce_for_export_setup(MeshCoerceForExport &coerce,
|
||||
Depsgraph *depsgraph,
|
||||
Object *obj_eval,
|
||||
const bool apply_modifiers)
|
||||
{
|
||||
/* Curves and NURBS surfaces have no mesh in their pre-modified state,
|
||||
* convert one on demand. */
|
||||
bool is_original_mesh_type = true;
|
||||
if (const ID *data_orig = obj_eval->runtime->data_orig) {
|
||||
if (GS(data_orig->name) != ID_ME) {
|
||||
is_original_mesh_type = false;
|
||||
if (!apply_modifiers) {
|
||||
coerce.owned = BKE_mesh_new_from_object(
|
||||
depsgraph, DEG_get_original(obj_eval), true, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apply_modifiers) {
|
||||
coerce.mesh = BKE_object_get_evaluated_mesh(obj_eval);
|
||||
}
|
||||
else {
|
||||
coerce.mesh = is_original_mesh_type ? BKE_object_get_pre_modified_mesh(obj_eval) :
|
||||
coerce.owned;
|
||||
}
|
||||
|
||||
return coerce.mesh;
|
||||
}
|
||||
|
||||
MeshCoerceForExport::~MeshCoerceForExport()
|
||||
{
|
||||
if (owned) {
|
||||
BKE_id_free(nullptr, owned);
|
||||
owned = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,74 @@
|
||||
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_abstract_hierarchy_iterator.h"
|
||||
|
||||
#include "BLI_assert.h"
|
||||
|
||||
#include "BKE_duplilist.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
ObjectIdentifier::ObjectIdentifier(Object *object,
|
||||
Object *duplicated_by,
|
||||
const PersistentID &persistent_id)
|
||||
: object(object), duplicated_by(duplicated_by), persistent_id(persistent_id)
|
||||
{
|
||||
/* Class invariants:
|
||||
* If duplicated_by is null, persistent_id must be default.
|
||||
* If duplicated_by is not null, persistent_id must not be default. */
|
||||
BLI_assert(duplicated_by == nullptr ? persistent_id == PersistentID() :
|
||||
!(persistent_id == PersistentID()));
|
||||
}
|
||||
|
||||
ObjectIdentifier ObjectIdentifier::for_real_object(Object *object)
|
||||
{
|
||||
return ObjectIdentifier(object, nullptr, PersistentID());
|
||||
}
|
||||
|
||||
ObjectIdentifier ObjectIdentifier::for_hierarchy_context(const HierarchyContext *context)
|
||||
{
|
||||
if (context == nullptr) {
|
||||
return for_graph_root();
|
||||
}
|
||||
if (context->duplicator != nullptr) {
|
||||
return ObjectIdentifier(context->object, context->duplicator, context->persistent_id);
|
||||
}
|
||||
return for_real_object(context->object);
|
||||
}
|
||||
|
||||
ObjectIdentifier ObjectIdentifier::for_duplicated_object(const DupliObject *dupli_object,
|
||||
Object *duplicated_by)
|
||||
{
|
||||
return ObjectIdentifier(dupli_object->ob, duplicated_by, PersistentID(dupli_object));
|
||||
}
|
||||
|
||||
ObjectIdentifier ObjectIdentifier::for_graph_root()
|
||||
{
|
||||
return ObjectIdentifier(nullptr, nullptr, PersistentID());
|
||||
}
|
||||
|
||||
bool ObjectIdentifier::is_root() const
|
||||
{
|
||||
return object == nullptr;
|
||||
}
|
||||
|
||||
bool operator==(const ObjectIdentifier &obj_ident_a, const ObjectIdentifier &obj_ident_b)
|
||||
{
|
||||
if (obj_ident_a.object != obj_ident_b.object) {
|
||||
return false;
|
||||
}
|
||||
if (obj_ident_a.duplicated_by != obj_ident_b.duplicated_by) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Return early if we know the expensive persistent_id check won't be necessary. */
|
||||
if (obj_ident_a.duplicated_by == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Same object, both are duplicated, use the persistent IDs to determine equality. */
|
||||
return obj_ident_a.persistent_id == obj_ident_b.persistent_id;
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,209 @@
|
||||
/* SPDX-FileCopyrightText: 2019 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_abstract_hierarchy_iterator.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
namespace {
|
||||
|
||||
/* Return object pointer for use in tests. This makes it possible to reliably test for
|
||||
* order/equality functions while using hard-coded values for simplicity. */
|
||||
Object *fake_pointer(int value)
|
||||
{
|
||||
return static_cast<Object *>(POINTER_FROM_INT(value));
|
||||
}
|
||||
|
||||
/* PersistentID subclass for use in tests, making it easier to construct test values. */
|
||||
class TestPersistentID : public PersistentID {
|
||||
public:
|
||||
TestPersistentID(int value0,
|
||||
int value1,
|
||||
int value2,
|
||||
int value3,
|
||||
int value4,
|
||||
int value5,
|
||||
int value6,
|
||||
int value7)
|
||||
{
|
||||
persistent_id_[0] = value0;
|
||||
persistent_id_[1] = value1;
|
||||
persistent_id_[2] = value2;
|
||||
persistent_id_[3] = value3;
|
||||
persistent_id_[4] = value4;
|
||||
persistent_id_[5] = value5;
|
||||
persistent_id_[6] = value6;
|
||||
persistent_id_[7] = value7;
|
||||
}
|
||||
TestPersistentID(int value0, int value1, int value2)
|
||||
: TestPersistentID(value0, value1, value2, INT_MAX, INT_MAX, INT_MAX, INT_MAX, INT_MAX)
|
||||
{
|
||||
}
|
||||
TestPersistentID(int value0, int value1) : TestPersistentID(value0, value1, INT_MAX) {}
|
||||
explicit TestPersistentID(int value0) : TestPersistentID(value0, INT_MAX) {}
|
||||
};
|
||||
|
||||
/* ObjectIdentifier subclass for use in tests, making it easier to construct test values. */
|
||||
class TestObjectIdentifier : public ObjectIdentifier {
|
||||
public:
|
||||
TestObjectIdentifier(Object *object, Object *duplicated_by, const PersistentID &persistent_id)
|
||||
: ObjectIdentifier(object, duplicated_by, persistent_id)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class ObjectIdentifierOrderTest : public testing::Test {};
|
||||
|
||||
TEST_F(ObjectIdentifierOrderTest, graph_root)
|
||||
{
|
||||
ObjectIdentifier id_root_1 = ObjectIdentifier::for_graph_root();
|
||||
ObjectIdentifier id_root_2 = ObjectIdentifier::for_graph_root();
|
||||
EXPECT_TRUE(id_root_1 == id_root_2);
|
||||
EXPECT_TRUE(id_root_1.hash() == id_root_2.hash());
|
||||
|
||||
ObjectIdentifier id_a = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
EXPECT_FALSE(id_root_1 == id_a);
|
||||
|
||||
ObjectIdentifier id_accidental_root = ObjectIdentifier::for_real_object(nullptr);
|
||||
EXPECT_TRUE(id_root_1 == id_accidental_root);
|
||||
EXPECT_TRUE(id_root_1.hash() == id_accidental_root.hash());
|
||||
}
|
||||
|
||||
TEST_F(ObjectIdentifierOrderTest, real_objects)
|
||||
{
|
||||
ObjectIdentifier id_a = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
ObjectIdentifier id_b = ObjectIdentifier::for_real_object(fake_pointer(2));
|
||||
EXPECT_FALSE(id_a == id_b);
|
||||
|
||||
ObjectIdentifier id_c = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
EXPECT_TRUE(id_a == id_c);
|
||||
EXPECT_TRUE(id_a.hash() == id_c.hash());
|
||||
}
|
||||
|
||||
TEST_F(ObjectIdentifierOrderTest, duplicated_objects)
|
||||
{
|
||||
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
|
||||
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
|
||||
TestObjectIdentifier id_same_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
|
||||
TestObjectIdentifier id_different_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(1));
|
||||
|
||||
EXPECT_FALSE(id_real_a == id_dupli_a);
|
||||
EXPECT_FALSE(id_dupli_a == id_dupli_b);
|
||||
|
||||
EXPECT_FALSE(id_dupli_b == id_different_dupli_b);
|
||||
EXPECT_FALSE(id_dupli_a == id_different_dupli_b);
|
||||
|
||||
EXPECT_TRUE(id_dupli_a == id_same_dupli_a);
|
||||
EXPECT_TRUE(id_dupli_a.hash() == id_same_dupli_a.hash());
|
||||
}
|
||||
|
||||
TEST_F(ObjectIdentifierOrderTest, behavior_as_map_keys)
|
||||
{
|
||||
ObjectIdentifier id_root = ObjectIdentifier::for_graph_root();
|
||||
ObjectIdentifier id_another_root = ObjectIdentifier::for_graph_root();
|
||||
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
|
||||
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
|
||||
AbstractHierarchyIterator::ExportGraph graph;
|
||||
|
||||
/* This inserts the keys with default values. */
|
||||
graph.add_new(id_root, {});
|
||||
graph.add_new(id_real_a, {});
|
||||
graph.add_new(id_dupli_a, {});
|
||||
graph.add_new(id_dupli_b, {});
|
||||
graph.add(id_another_root, {});
|
||||
|
||||
EXPECT_EQ(4, graph.size());
|
||||
|
||||
graph.remove_contained(id_another_root);
|
||||
EXPECT_EQ(3, graph.size());
|
||||
|
||||
TestObjectIdentifier id_another_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
|
||||
graph.remove_contained(id_another_dupli_b);
|
||||
EXPECT_EQ(2, graph.size());
|
||||
}
|
||||
|
||||
TEST_F(ObjectIdentifierOrderTest, map_copy_and_update)
|
||||
{
|
||||
ObjectIdentifier id_root = ObjectIdentifier::for_graph_root();
|
||||
ObjectIdentifier id_real_a = ObjectIdentifier::for_real_object(fake_pointer(1));
|
||||
TestObjectIdentifier id_dupli_a(fake_pointer(1), fake_pointer(2), TestPersistentID(0));
|
||||
TestObjectIdentifier id_dupli_b(fake_pointer(1), fake_pointer(3), TestPersistentID(0));
|
||||
TestObjectIdentifier id_dupli_c(fake_pointer(1), fake_pointer(3), TestPersistentID(1));
|
||||
AbstractHierarchyIterator::ExportGraph graph;
|
||||
|
||||
/* This inserts the keys with default values. */
|
||||
graph.add_new(id_root, {});
|
||||
graph.add_new(id_real_a, {});
|
||||
graph.add_new(id_dupli_a, {});
|
||||
graph.add_new(id_dupli_b, {});
|
||||
graph.add_new(id_dupli_c, {});
|
||||
EXPECT_EQ(5, graph.size());
|
||||
|
||||
AbstractHierarchyIterator::ExportGraph graph_copy = graph;
|
||||
EXPECT_EQ(5, graph_copy.size());
|
||||
|
||||
/* Updating a value in a copy should not update the original. */
|
||||
HierarchyContext ctx1;
|
||||
HierarchyContext ctx2;
|
||||
ctx1.object = fake_pointer(1);
|
||||
ctx2.object = fake_pointer(2);
|
||||
|
||||
graph_copy.lookup(id_root).add_new(&ctx1);
|
||||
EXPECT_EQ(0, graph.lookup(id_root).size());
|
||||
|
||||
/* Deleting a key in the copy should not update the original. */
|
||||
graph_copy.remove_contained(id_dupli_c);
|
||||
EXPECT_EQ(4, graph_copy.size());
|
||||
EXPECT_EQ(5, graph.size());
|
||||
}
|
||||
|
||||
class PersistentIDTest : public testing::Test {};
|
||||
|
||||
TEST_F(PersistentIDTest, is_from_same_instancer)
|
||||
{
|
||||
PersistentID child_id_a = TestPersistentID(42, 327);
|
||||
PersistentID child_id_b = TestPersistentID(17, 327);
|
||||
PersistentID child_id_c = TestPersistentID(17);
|
||||
|
||||
EXPECT_TRUE(child_id_a.is_from_same_instancer_as(child_id_b));
|
||||
EXPECT_FALSE(child_id_a.is_from_same_instancer_as(child_id_c));
|
||||
}
|
||||
|
||||
TEST_F(PersistentIDTest, instancer_id)
|
||||
{
|
||||
PersistentID child_id = TestPersistentID(42, 327);
|
||||
|
||||
PersistentID expect_instancer_id = TestPersistentID(327);
|
||||
EXPECT_EQ(expect_instancer_id, child_id.instancer_pid());
|
||||
EXPECT_EQ(expect_instancer_id.hash(), child_id.instancer_pid().hash());
|
||||
|
||||
PersistentID empty_id;
|
||||
EXPECT_EQ(empty_id, child_id.instancer_pid().instancer_pid());
|
||||
EXPECT_EQ(empty_id.hash(), child_id.instancer_pid().instancer_pid().hash());
|
||||
}
|
||||
|
||||
TEST_F(PersistentIDTest, as_object_name_suffix)
|
||||
{
|
||||
EXPECT_EQ("", PersistentID().as_object_name_suffix());
|
||||
EXPECT_EQ("47", TestPersistentID(47).as_object_name_suffix());
|
||||
EXPECT_EQ("327-47", TestPersistentID(47, 327).as_object_name_suffix());
|
||||
EXPECT_EQ("42-327-47", TestPersistentID(47, 327, 42).as_object_name_suffix());
|
||||
|
||||
EXPECT_EQ("7-6-5-4-3-2-1-0", TestPersistentID(0, 1, 2, 3, 4, 5, 6, 7).as_object_name_suffix());
|
||||
|
||||
EXPECT_EQ("0-0-0", TestPersistentID(0, 0, 0).as_object_name_suffix());
|
||||
EXPECT_EQ("0-0", TestPersistentID(0, 0).as_object_name_suffix());
|
||||
EXPECT_EQ("-3--2--1", TestPersistentID(-1, -2, -3).as_object_name_suffix());
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
41
blender-5.2.0/source/blender/io/common/intern/orientation.cc
Normal file
41
blender-5.2.0/source/blender/io/common/intern/orientation.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_types.hh"
|
||||
|
||||
#include "IO_orientation.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
const EnumPropertyItem io_transform_axis[] = {
|
||||
{IO_AXIS_X, "X", 0, "X", "Positive X axis"},
|
||||
{IO_AXIS_Y, "Y", 0, "Y", "Positive Y axis"},
|
||||
{IO_AXIS_Z, "Z", 0, "Z", "Positive Z axis"},
|
||||
{IO_AXIS_NEGATIVE_X, "NEGATIVE_X", 0, "-X", "Negative X axis"},
|
||||
{IO_AXIS_NEGATIVE_Y, "NEGATIVE_Y", 0, "-Y", "Negative Y axis"},
|
||||
{IO_AXIS_NEGATIVE_Z, "NEGATIVE_Z", 0, "-Z", "Negative Z axis"},
|
||||
{0, nullptr, 0, nullptr, nullptr}};
|
||||
|
||||
void io_ui_forward_axis_update(Main * /*main*/, Scene * /*scene*/, PointerRNA *ptr)
|
||||
{
|
||||
/* Both forward and up axes cannot be along the same direction. */
|
||||
|
||||
int forward = RNA_enum_get(ptr, "forward_axis");
|
||||
int up = RNA_enum_get(ptr, "up_axis");
|
||||
if ((forward % 3) == (up % 3)) {
|
||||
RNA_enum_set(ptr, "up_axis", (up + 1) % 6);
|
||||
}
|
||||
}
|
||||
|
||||
void io_ui_up_axis_update(Main * /*main*/, Scene * /*scene*/, PointerRNA *ptr)
|
||||
{
|
||||
int forward = RNA_enum_get(ptr, "forward_axis");
|
||||
int up = RNA_enum_get(ptr, "up_axis");
|
||||
if ((forward % 3) == (up % 3)) {
|
||||
RNA_enum_set(ptr, "forward_axis", (forward + 1) % 6);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
91
blender-5.2.0/source/blender/io/common/intern/path_util.cc
Normal file
91
blender-5.2.0/source/blender/io/common/intern/path_util.cc
Normal file
@@ -0,0 +1,91 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_path_util.hh"
|
||||
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"io.common"};
|
||||
|
||||
namespace io {
|
||||
|
||||
std::string path_reference(StringRefNull filepath,
|
||||
StringRefNull base_src,
|
||||
StringRefNull base_dst,
|
||||
ePathReferenceMode mode,
|
||||
Set<std::pair<std::string, std::string>> *copy_set)
|
||||
{
|
||||
const bool is_relative = BLI_path_is_rel(filepath.c_str());
|
||||
char filepath_abs[PATH_MAX];
|
||||
STRNCPY(filepath_abs, filepath.c_str());
|
||||
BLI_path_abs(filepath_abs, base_src.c_str());
|
||||
BLI_path_normalize(filepath_abs);
|
||||
|
||||
/* Figure out final mode to be used. */
|
||||
if (mode == PATH_REFERENCE_MATCH) {
|
||||
mode = is_relative ? PATH_REFERENCE_RELATIVE : PATH_REFERENCE_ABSOLUTE;
|
||||
}
|
||||
else if (mode == PATH_REFERENCE_AUTO) {
|
||||
mode = BLI_path_contains(base_dst.c_str(), filepath_abs) ? PATH_REFERENCE_RELATIVE :
|
||||
PATH_REFERENCE_ABSOLUTE;
|
||||
}
|
||||
else if (mode == PATH_REFERENCE_COPY) {
|
||||
char filepath_cpy[PATH_MAX];
|
||||
BLI_path_join(filepath_cpy, PATH_MAX, base_dst.c_str(), BLI_path_basename(filepath_abs));
|
||||
copy_set->add(std::make_pair(filepath_abs, filepath_cpy));
|
||||
STRNCPY(filepath_abs, filepath_cpy);
|
||||
mode = PATH_REFERENCE_RELATIVE;
|
||||
}
|
||||
|
||||
/* Now we know the final path mode. */
|
||||
if (mode == PATH_REFERENCE_ABSOLUTE) {
|
||||
return filepath_abs;
|
||||
}
|
||||
if (mode == PATH_REFERENCE_RELATIVE) {
|
||||
char rel_path[PATH_MAX];
|
||||
STRNCPY(rel_path, filepath_abs);
|
||||
BLI_path_rel(rel_path, base_dst.c_str());
|
||||
/* Can't always find relative path (e.g. between different drives). */
|
||||
if (!BLI_path_is_rel(rel_path)) {
|
||||
return filepath_abs;
|
||||
}
|
||||
return rel_path + 2; /* Skip blender's internal "//" prefix. */
|
||||
}
|
||||
if (mode == PATH_REFERENCE_STRIP) {
|
||||
return BLI_path_basename(filepath_abs);
|
||||
}
|
||||
BLI_assert_msg(false, "Invalid path reference mode");
|
||||
return filepath_abs;
|
||||
}
|
||||
|
||||
void path_reference_copy(const Set<std::pair<std::string, std::string>> ©_set)
|
||||
{
|
||||
for (const auto © : copy_set) {
|
||||
const char *src = copy.first.c_str();
|
||||
const char *dst = copy.second.c_str();
|
||||
if (!BLI_exists(src)) {
|
||||
CLOG_WARN(&LOG, "Missing source file '%s', not copying", src);
|
||||
continue;
|
||||
}
|
||||
if (0 == BLI_path_cmp_normalized(src, dst)) {
|
||||
continue; /* Source and destination are the same. */
|
||||
}
|
||||
if (!BLI_file_ensure_parent_dir_exists(dst)) {
|
||||
CLOG_WARN(&LOG, "Can't make directory for '%s', not copying", dst);
|
||||
continue;
|
||||
}
|
||||
if (BLI_copy(src, dst) != 0) {
|
||||
CLOG_WARN(&LOG, "Can't copy '%s' to '%s'", src, dst);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace io
|
||||
} // namespace blender
|
||||
169
blender-5.2.0/source/blender/io/common/intern/string_utils.cc
Normal file
169
blender-5.2.0/source/blender/io/common/intern/string_utils.cc
Normal file
@@ -0,0 +1,169 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "IO_string_utils.hh"
|
||||
|
||||
/* NOTE: we could use C++17 <charconv> from_chars to parse
|
||||
* floats, but even if some compilers claim full support,
|
||||
* their standard libraries are not quite there yet.
|
||||
* LLVM/libc++ only has a float parser since LLVM 14,
|
||||
* and GCC/libstdc++ since 11.1. So until at least these are
|
||||
* the minimum spec, use an external library. */
|
||||
#include "fast_float.h"
|
||||
#include <charconv>
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
StringRef read_next_line(StringRef &buffer)
|
||||
{
|
||||
const char *start = buffer.begin();
|
||||
const char *end = buffer.end();
|
||||
size_t len = 0;
|
||||
const char *ptr = start;
|
||||
while (ptr < end) {
|
||||
char c = *ptr++;
|
||||
if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
++len;
|
||||
}
|
||||
|
||||
buffer = StringRef(ptr, end);
|
||||
return StringRef(start, len);
|
||||
}
|
||||
|
||||
static bool is_whitespace(char c)
|
||||
{
|
||||
return c <= ' ';
|
||||
}
|
||||
|
||||
const char *drop_whitespace(const char *p, const char *end)
|
||||
{
|
||||
while (p < end && is_whitespace(*p)) {
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *drop_non_whitespace(const char *p, const char *end)
|
||||
{
|
||||
while (p < end && !is_whitespace(*p)) {
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static const char *drop_sign(const char *p, const char *end, int &sign)
|
||||
{
|
||||
sign = 1;
|
||||
if (p < end) {
|
||||
if (*p == '+') {
|
||||
++p;
|
||||
}
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
++p;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *try_parse_float(
|
||||
const char *p, const char *end, int fallback, bool &success, float &dst, bool skip_space)
|
||||
{
|
||||
if (skip_space) {
|
||||
p = drop_whitespace(p, end);
|
||||
}
|
||||
int sign = 0;
|
||||
p = drop_sign(p, end, sign);
|
||||
fast_float::from_chars_result res = fast_float::from_chars(p, end, dst);
|
||||
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range) || res.ptr < end) {
|
||||
dst = fallback;
|
||||
success = false;
|
||||
}
|
||||
else {
|
||||
dst *= sign;
|
||||
success = true;
|
||||
}
|
||||
return res.ptr;
|
||||
}
|
||||
|
||||
const char *try_parse_int(
|
||||
const char *p, const char *end, int fallback, bool &success, int &dst, bool skip_space)
|
||||
{
|
||||
if (skip_space) {
|
||||
p = drop_whitespace(p, end);
|
||||
}
|
||||
int sign = 0;
|
||||
p = drop_sign(p, end, sign);
|
||||
std::from_chars_result res = std::from_chars(p, end, dst);
|
||||
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range) || res.ptr < end) {
|
||||
dst = fallback;
|
||||
success = false;
|
||||
}
|
||||
else {
|
||||
dst *= sign;
|
||||
success = true;
|
||||
}
|
||||
return res.ptr;
|
||||
}
|
||||
|
||||
static const char *drop_plus(const char *p, const char *end)
|
||||
{
|
||||
if (p < end && *p == '+') {
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *parse_float(const char *p,
|
||||
const char *end,
|
||||
float fallback,
|
||||
float &dst,
|
||||
bool skip_space,
|
||||
bool require_trailing_space)
|
||||
{
|
||||
if (skip_space) {
|
||||
p = drop_whitespace(p, end);
|
||||
}
|
||||
p = drop_plus(p, end);
|
||||
fast_float::from_chars_result res = fast_float::from_chars(p, end, dst);
|
||||
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
|
||||
dst = fallback;
|
||||
}
|
||||
else if (require_trailing_space && res.ptr < end && !is_whitespace(*res.ptr)) {
|
||||
/* If there are trailing non-space characters, do not eat up the number. */
|
||||
dst = fallback;
|
||||
return p;
|
||||
}
|
||||
return res.ptr;
|
||||
}
|
||||
|
||||
const char *parse_floats(const char *p,
|
||||
const char *end,
|
||||
float fallback,
|
||||
float *dst,
|
||||
int count,
|
||||
bool require_trailing_space)
|
||||
{
|
||||
for (int i = 0; i < count; ++i) {
|
||||
p = parse_float(p, end, fallback, dst[i], true, require_trailing_space);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const char *parse_int(const char *p, const char *end, int fallback, int &dst, bool skip_space)
|
||||
{
|
||||
if (skip_space) {
|
||||
p = drop_whitespace(p, end);
|
||||
}
|
||||
p = drop_plus(p, end);
|
||||
std::from_chars_result res = std::from_chars(p, end, dst);
|
||||
if (ELEM(res.ec, std::errc::invalid_argument, std::errc::result_out_of_range)) {
|
||||
dst = fallback;
|
||||
}
|
||||
return res.ptr;
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
@@ -0,0 +1,139 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
#include "IO_string_utils.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
#define EXPECT_STRREF_EQ(str1, str2) EXPECT_STREQ(str1, std::string(str2).c_str())
|
||||
|
||||
TEST(io_common_string_utils, read_next_line)
|
||||
{
|
||||
std::string str = "abc\n \n\nline with \t spaces\nCRLF ending:\r\na";
|
||||
StringRef s = str;
|
||||
EXPECT_STRREF_EQ("abc", read_next_line(s));
|
||||
EXPECT_STRREF_EQ(" ", read_next_line(s));
|
||||
EXPECT_STRREF_EQ("", read_next_line(s));
|
||||
EXPECT_STRREF_EQ("line with \t spaces", read_next_line(s));
|
||||
EXPECT_STRREF_EQ("CRLF ending:\r", read_next_line(s));
|
||||
EXPECT_STRREF_EQ("a", read_next_line(s));
|
||||
EXPECT_TRUE(s.is_empty());
|
||||
}
|
||||
|
||||
static StringRef drop_whitespace(StringRef s)
|
||||
{
|
||||
return StringRef(drop_whitespace(s.begin(), s.end()), s.end());
|
||||
}
|
||||
static StringRef parse_int(StringRef s, int fallback, int &dst, bool skip_space = true)
|
||||
{
|
||||
return StringRef(parse_int(s.begin(), s.end(), fallback, dst, skip_space), s.end());
|
||||
}
|
||||
static StringRef parse_float(StringRef s,
|
||||
float fallback,
|
||||
float &dst,
|
||||
bool skip_space = true,
|
||||
bool require_trailing_space = false)
|
||||
{
|
||||
return StringRef(
|
||||
parse_float(s.begin(), s.end(), fallback, dst, skip_space, require_trailing_space), s.end());
|
||||
}
|
||||
|
||||
TEST(io_common_string_utils, drop_whitespace)
|
||||
{
|
||||
/* Empty */
|
||||
EXPECT_STRREF_EQ("", drop_whitespace(""));
|
||||
/* Only whitespace */
|
||||
EXPECT_STRREF_EQ("", drop_whitespace(" "));
|
||||
EXPECT_STRREF_EQ("", drop_whitespace(" "));
|
||||
EXPECT_STRREF_EQ("", drop_whitespace(" \t\n\r "));
|
||||
/* Drops leading whitespace */
|
||||
EXPECT_STRREF_EQ("a", drop_whitespace(" a"));
|
||||
EXPECT_STRREF_EQ("a b", drop_whitespace(" a b"));
|
||||
EXPECT_STRREF_EQ("a b ", drop_whitespace(" a b "));
|
||||
/* No leading whitespace */
|
||||
EXPECT_STRREF_EQ("c", drop_whitespace("c"));
|
||||
/* Case with backslash, should be treated as whitespace */
|
||||
EXPECT_STRREF_EQ("d", drop_whitespace(" \t d"));
|
||||
}
|
||||
|
||||
TEST(io_common_string_utils, parse_int_valid)
|
||||
{
|
||||
std::string str = "1 -10 \t 1234 1234567890 +7 123a";
|
||||
StringRef s = str;
|
||||
int val;
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(1, val);
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(-10, val);
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(1234, val);
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(1234567890, val);
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(7, val);
|
||||
s = parse_int(s, 0, val);
|
||||
EXPECT_EQ(123, val);
|
||||
EXPECT_STRREF_EQ("a", s);
|
||||
}
|
||||
|
||||
TEST(io_common_string_utils, parse_int_invalid)
|
||||
{
|
||||
int val;
|
||||
/* Invalid syntax */
|
||||
EXPECT_STRREF_EQ("--123", parse_int("--123", -1, val));
|
||||
EXPECT_EQ(val, -1);
|
||||
EXPECT_STRREF_EQ("foobar", parse_int("foobar", -2, val));
|
||||
EXPECT_EQ(val, -2);
|
||||
/* Out of integer range */
|
||||
EXPECT_STRREF_EQ(" a", parse_int("1234567890123 a", -3, val));
|
||||
EXPECT_EQ(val, -3);
|
||||
/* Has leading white-space when we don't expect it */
|
||||
EXPECT_STRREF_EQ(" 1", parse_int(" 1", -4, val, false));
|
||||
EXPECT_EQ(val, -4);
|
||||
}
|
||||
|
||||
TEST(io_common_string_utils, parse_float_valid)
|
||||
{
|
||||
std::string str = "1 -10 123.5 -17.125 0.1 1e6 50.0e-1";
|
||||
StringRef s = str;
|
||||
float val;
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(1.0f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(-10.0f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(123.5f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(-17.125f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(0.1f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(1.0e6f, val);
|
||||
s = parse_float(s, 0, val);
|
||||
EXPECT_EQ(5.0f, val);
|
||||
EXPECT_TRUE(s.is_empty());
|
||||
}
|
||||
|
||||
TEST(io_common_string_utils, parse_float_invalid)
|
||||
{
|
||||
float val;
|
||||
/* Invalid syntax */
|
||||
EXPECT_STRREF_EQ("_0", parse_float("_0", -1.0f, val));
|
||||
EXPECT_EQ(val, -1.0f);
|
||||
EXPECT_STRREF_EQ("..5", parse_float("..5", -2.0f, val));
|
||||
EXPECT_EQ(val, -2.0f);
|
||||
/* Out of float range. */
|
||||
EXPECT_STRREF_EQ(" a", parse_float("9.0e500 a", -3.0f, val));
|
||||
EXPECT_EQ(val, -3.0f);
|
||||
/* Has leading white-space when we don't expect it */
|
||||
EXPECT_STRREF_EQ(" 1", parse_float(" 1", -4.0f, val, false));
|
||||
EXPECT_EQ(val, -4.0f);
|
||||
/* Has trailing non-number characters when we don't want them */
|
||||
EXPECT_STRREF_EQ("123.5.png", parse_float(" 123.5.png", -5.0f, val, true, true));
|
||||
EXPECT_EQ(val, -5.0f);
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
117
blender-5.2.0/source/blender/io/common/intern/subdiv_disabler.cc
Normal file
117
blender-5.2.0/source/blender/io/common/intern/subdiv_disabler.cc
Normal file
@@ -0,0 +1,117 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "IO_subdiv_disabler.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
#include "DNA_layer_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_modifier_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
|
||||
namespace blender::io {
|
||||
|
||||
ModifierData *SubdivModifierDisabler::get_subdiv_modifier(Scene *scene,
|
||||
const Object *ob,
|
||||
ModifierMode mode)
|
||||
{
|
||||
/* Returns the last subdiv modifier associated with an object,
|
||||
* if that modifier should be disabled.
|
||||
* We do not disable the subdiv modifier if other modifiers are
|
||||
* applied after it, with the sole exception of particle modifiers,
|
||||
* which are allowed.
|
||||
* Returns nullptr if there is not any subdiv modifier to disable.
|
||||
*/
|
||||
|
||||
ModifierData *md = static_cast<ModifierData *>(ob->modifiers.last);
|
||||
|
||||
for (; md; md = md->prev) {
|
||||
/* Ignore disabled modifiers. */
|
||||
if (!BKE_modifier_is_enabled(scene, md, mode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (md->type == eModifierType_Subsurf) {
|
||||
SubsurfModifierData *smd = reinterpret_cast<SubsurfModifierData *>(md);
|
||||
|
||||
if (smd->subdivType == ME_CC_SUBSURF) {
|
||||
/* This is a Catmull-Clark modifier. */
|
||||
return md;
|
||||
}
|
||||
|
||||
/* Not Catmull-Clark, so ignore it. */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* If any modifier other than a particle system exists after the
|
||||
* subdiv modifier, then abort. */
|
||||
if (md->type != eModifierType_ParticleSystem) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SubdivModifierDisabler::SubdivModifierDisabler(Depsgraph *depsgraph) : depsgraph_(depsgraph) {}
|
||||
|
||||
SubdivModifierDisabler::~SubdivModifierDisabler()
|
||||
{
|
||||
/* Enable previously disabled modifiers. */
|
||||
for (ModifierData *modifier : disabled_modifiers_) {
|
||||
modifier->mode &= ~eModifierMode_DisableTemporary;
|
||||
}
|
||||
|
||||
/* Update object to render with restored modifiers in the viewport. */
|
||||
for (Object *object : modified_objects_) {
|
||||
DEG_id_tag_update(&object->id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
}
|
||||
|
||||
void SubdivModifierDisabler::disable_modifiers()
|
||||
{
|
||||
eEvaluationMode eval_mode = DEG_get_mode(depsgraph_);
|
||||
const ModifierMode mode = eval_mode == DAG_EVAL_VIEWPORT ? eModifierMode_Realtime :
|
||||
eModifierMode_Render;
|
||||
|
||||
Scene *scene = DEG_get_input_scene(depsgraph_);
|
||||
ViewLayer *view_layer = DEG_get_input_view_layer(depsgraph_);
|
||||
|
||||
BKE_view_layer_synced_ensure(*DEG_get_bmain(depsgraph_), scene, view_layer);
|
||||
for (Base &base : *BKE_view_layer_object_bases_get(view_layer)) {
|
||||
Object *object = base.object;
|
||||
|
||||
if (object->type != OB_MESH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if a subdiv modifier exists, and should be disabled. */
|
||||
ModifierData *mod = get_subdiv_modifier(scene, object, mode);
|
||||
if (!mod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* This might disable more modifiers than necessary, as it doesn't take restrictions like
|
||||
* "export selected objects only" into account. However, with the subdivisions disabled,
|
||||
* moving to a different frame is also going to be faster, so in the end this is probably
|
||||
* a good thing to do. */
|
||||
disable_modifier(mod);
|
||||
modified_objects_.append(object);
|
||||
DEG_id_tag_update(&object->id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
}
|
||||
|
||||
void SubdivModifierDisabler::disable_modifier(ModifierData *mod)
|
||||
{
|
||||
mod->mode |= eModifierMode_DisableTemporary;
|
||||
disabled_modifiers_.append(mod);
|
||||
}
|
||||
|
||||
} // namespace blender::io
|
||||
31
blender-5.2.0/source/blender/io/csv/CMakeLists.txt
Normal file
31
blender-5.2.0/source/blender/io/csv/CMakeLists.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
importer
|
||||
../common
|
||||
../../blenkernel
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
importer/csv_reader.cc
|
||||
|
||||
IO_csv.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
bf_blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
bf_io_common
|
||||
PRIVATE bf::extern::fast_float
|
||||
)
|
||||
|
||||
blender_add_lib(bf_io_csv "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
31
blender-5.2.0/source/blender/io/csv/IO_csv.hh
Normal file
31
blender-5.2.0/source/blender/io/csv/IO_csv.hh
Normal file
@@ -0,0 +1,31 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup csv
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct PointCloud;
|
||||
struct ReportList;
|
||||
|
||||
namespace io::csv {
|
||||
|
||||
struct CSVImportParams {
|
||||
/** Full path to the source CSV file to import. */
|
||||
char filepath[FILE_MAX];
|
||||
char delimiter = ',';
|
||||
|
||||
ReportList *reports = nullptr;
|
||||
};
|
||||
|
||||
PointCloud *import_csv_as_pointcloud(const CSVImportParams &import_params);
|
||||
|
||||
} // namespace io::csv
|
||||
} // namespace blender
|
||||
369
blender-5.2.0/source/blender/io/csv/importer/csv_reader.cc
Normal file
369
blender-5.2.0/source/blender/io/csv/importer/csv_reader.cc
Normal file
@@ -0,0 +1,369 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup csv
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <charconv>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
#include "BLI_array_utils.hh"
|
||||
#include "fast_float.h"
|
||||
|
||||
#include "BKE_anonymous_attribute_id.hh"
|
||||
#include "BKE_attribute.hh"
|
||||
#include "BKE_pointcloud.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_csv_parse.hh"
|
||||
#include "BLI_fileops.hh"
|
||||
#include "BLI_implicit_sharing.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "IO_csv.hh"
|
||||
|
||||
namespace blender::io::csv {
|
||||
|
||||
struct ColumnInfo {
|
||||
StringRef name;
|
||||
bool has_invalid_name = false;
|
||||
std::atomic<bool> found_invalid = false;
|
||||
std::atomic<bool> found_int = false;
|
||||
std::atomic<bool> found_float = false;
|
||||
};
|
||||
|
||||
using ColumnData = std::variant<std::monostate, Vector<float>, Vector<int>>;
|
||||
|
||||
struct ChunkResult {
|
||||
int rows_num;
|
||||
Vector<ColumnData> columns;
|
||||
};
|
||||
|
||||
struct ParseFloatColumnResult {
|
||||
Vector<float> data;
|
||||
bool found_invalid = false;
|
||||
};
|
||||
|
||||
struct ParseIntColumnResult {
|
||||
Vector<int> data;
|
||||
bool found_invalid = false;
|
||||
bool found_float = false;
|
||||
};
|
||||
|
||||
static ParseFloatColumnResult parse_column_as_floats(const csv_parse::CsvRecords &records,
|
||||
const int column_i)
|
||||
{
|
||||
ParseFloatColumnResult result;
|
||||
result.data.reserve(records.size());
|
||||
for (const int row_i : records.index_range()) {
|
||||
const Span<char> value_span = records.record(row_i).field(column_i);
|
||||
const char *value_begin = value_span.begin();
|
||||
const char *value_end = value_span.end();
|
||||
/* Skip leading white-space and plus sign. */
|
||||
while (value_begin < value_end && ELEM(*value_begin, ' ', '+')) {
|
||||
value_begin++;
|
||||
}
|
||||
float value;
|
||||
fast_float::from_chars_result res = fast_float::from_chars(value_begin, value_end, value);
|
||||
if (res.ec != std::errc()) {
|
||||
result.found_invalid = true;
|
||||
return result;
|
||||
}
|
||||
if (res.ptr < value_end) {
|
||||
/* Allow trailing white-space in the value. */
|
||||
while (res.ptr < value_end && res.ptr[0] == ' ') {
|
||||
res.ptr++;
|
||||
}
|
||||
if (res.ptr < value_end) {
|
||||
result.found_invalid = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result.data.append(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static ParseIntColumnResult parse_column_as_ints(const csv_parse::CsvRecords &records,
|
||||
const int column_i)
|
||||
{
|
||||
ParseIntColumnResult result;
|
||||
result.data.reserve(records.size());
|
||||
for (const int row_i : records.index_range()) {
|
||||
const Span<char> value_span = records.record(row_i).field(column_i);
|
||||
const char *value_begin = value_span.begin();
|
||||
const char *value_end = value_span.end();
|
||||
/* Skip leading white-space and plus sign. */
|
||||
while (value_begin < value_end && ELEM(*value_begin, ' ', '+')) {
|
||||
value_begin++;
|
||||
}
|
||||
int value;
|
||||
std::from_chars_result res = std::from_chars(value_begin, value_end, value);
|
||||
if (res.ec != std::errc()) {
|
||||
result.found_invalid = true;
|
||||
return result;
|
||||
}
|
||||
if (res.ptr < value_end) {
|
||||
/* If the next character after the value is a dot, it should be parsed again as float. */
|
||||
if (res.ptr[0] == '.') {
|
||||
result.found_float = true;
|
||||
return result;
|
||||
}
|
||||
/* Allow trailing white-space in the value. */
|
||||
while (res.ptr < value_end && res.ptr[0] == ' ') {
|
||||
res.ptr++;
|
||||
}
|
||||
if (res.ptr < value_end) {
|
||||
result.found_invalid = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result.data.append(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static ChunkResult parse_records_chunk(const csv_parse::CsvRecords &records,
|
||||
MutableSpan<ColumnInfo> columns_info)
|
||||
{
|
||||
const int columns_num = columns_info.size();
|
||||
ChunkResult chunk_result;
|
||||
chunk_result.rows_num = records.size();
|
||||
chunk_result.columns.resize(columns_num);
|
||||
for (const int column_i : IndexRange(columns_num)) {
|
||||
ColumnInfo &column_info = columns_info[column_i];
|
||||
if (column_info.has_invalid_name) {
|
||||
/* Column can be ignored. */
|
||||
continue;
|
||||
}
|
||||
if (column_info.found_invalid.load(std::memory_order_relaxed)) {
|
||||
/* Invalid values have been found in this column already, skip it. */
|
||||
continue;
|
||||
}
|
||||
/* A float was found in this column already, so parse everything as floats. */
|
||||
const bool found_float = column_info.found_float.load(std::memory_order_relaxed);
|
||||
if (found_float) {
|
||||
ParseFloatColumnResult float_column_result = parse_column_as_floats(records, column_i);
|
||||
if (float_column_result.found_invalid) {
|
||||
column_info.found_invalid.store(true, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
chunk_result.columns[column_i] = std::move(float_column_result.data);
|
||||
continue;
|
||||
}
|
||||
/* No float was found so far in this column, so attempt to parse it as integers. */
|
||||
ParseIntColumnResult int_column_result = parse_column_as_ints(records, column_i);
|
||||
if (int_column_result.found_invalid) {
|
||||
column_info.found_invalid.store(true, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
if (!int_column_result.found_float) {
|
||||
chunk_result.columns[column_i] = std::move(int_column_result.data);
|
||||
column_info.found_int.store(true, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
/* While parsing it as integers, floats were detected. So parse it as floats again. */
|
||||
column_info.found_float.store(true, std::memory_order_relaxed);
|
||||
ParseFloatColumnResult float_column_result = parse_column_as_floats(records, column_i);
|
||||
if (float_column_result.found_invalid) {
|
||||
column_info.found_invalid.store(true, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
chunk_result.columns[column_i] = std::move(float_column_result.data);
|
||||
}
|
||||
return chunk_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* So far, the parsed data is still split into many chunks. This function flattens the chunks into
|
||||
* continuous buffers that can be used as attributes.
|
||||
*/
|
||||
static Array<std::optional<GArray<>>> flatten_valid_attribute_chunks(
|
||||
const Span<ColumnInfo> columns_info,
|
||||
OffsetIndices<int> chunk_offsets,
|
||||
MutableSpan<ChunkResult> chunks)
|
||||
{
|
||||
const int points_num = chunk_offsets.total_size();
|
||||
Array<std::optional<GArray<>>> flattened_attributes(columns_info.size());
|
||||
|
||||
threading::parallel_for(columns_info.index_range(), 1, [&](const IndexRange columns_range) {
|
||||
for (const int column_i : columns_range) {
|
||||
const ColumnInfo &column_info = columns_info[column_i];
|
||||
if (column_info.has_invalid_name || column_info.found_invalid) {
|
||||
/* Column can be ignored. */
|
||||
continue;
|
||||
}
|
||||
if (column_info.found_float) {
|
||||
/* Should read column as floats. */
|
||||
GArray<> attribute(CPPType::get<float>(), points_num);
|
||||
float *attribute_buffer = static_cast<float *>(attribute.data());
|
||||
threading::parallel_for(chunks.index_range(), 1, [&](const IndexRange chunks_range) {
|
||||
for (const int chunk_i : chunks_range) {
|
||||
const IndexRange dst_range = chunk_offsets[chunk_i];
|
||||
ChunkResult &chunk = chunks[chunk_i];
|
||||
ColumnData &column_data = chunk.columns[column_i];
|
||||
if (const auto *float_vec = std::get_if<Vector<float>>(&column_data)) {
|
||||
BLI_assert(float_vec->size() == dst_range.size());
|
||||
uninitialized_copy_n(
|
||||
float_vec->data(), dst_range.size(), attribute_buffer + dst_range.first());
|
||||
}
|
||||
else if (const auto *int_vec = std::get_if<Vector<int>>(&column_data)) {
|
||||
/* This chunk was read entirely as integers, so it still has to be converted to
|
||||
* floats. */
|
||||
BLI_assert(int_vec->size() == dst_range.size());
|
||||
uninitialized_convert_n(int_vec->data(), dst_range.size(), attribute_buffer);
|
||||
}
|
||||
else {
|
||||
/* Expected data to be available, because the `found_invalid` flag was not
|
||||
* set. */
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
/* Free data for chunk. */
|
||||
column_data = std::monostate{};
|
||||
}
|
||||
});
|
||||
flattened_attributes[column_i] = std::move(attribute);
|
||||
continue;
|
||||
}
|
||||
if (column_info.found_int) {
|
||||
/* Should read column as ints. */
|
||||
GArray<> attribute(CPPType::get<int>(), points_num);
|
||||
int *attribute_buffer = static_cast<int *>(attribute.data());
|
||||
threading::parallel_for(chunks.index_range(), 1, [&](const IndexRange chunks_range) {
|
||||
for (const int chunk_i : chunks_range) {
|
||||
const IndexRange dst_range = chunk_offsets[chunk_i];
|
||||
ChunkResult &chunk = chunks[chunk_i];
|
||||
ColumnData &column_data = chunk.columns[column_i];
|
||||
if (const auto *int_vec = std::get_if<Vector<int>>(&column_data)) {
|
||||
BLI_assert(int_vec->size() == dst_range.size());
|
||||
uninitialized_copy_n(
|
||||
int_vec->data(), dst_range.size(), attribute_buffer + dst_range.first());
|
||||
}
|
||||
else {
|
||||
/* Expected data to be available, because the `found_invalid` and
|
||||
* `found_float` flags were not set. */
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
/* Free data for chunk. */
|
||||
column_data = std::monostate{};
|
||||
}
|
||||
});
|
||||
flattened_attributes[column_i] = std::move(attribute);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
return flattened_attributes;
|
||||
}
|
||||
|
||||
PointCloud *import_csv_as_pointcloud(const CSVImportParams &import_params)
|
||||
{
|
||||
size_t buffer_len;
|
||||
char *buffer = BLI_file_read_text_as_mem(import_params.filepath, 0, &buffer_len);
|
||||
if (buffer == nullptr) {
|
||||
BKE_reportf(import_params.reports,
|
||||
RPT_ERROR,
|
||||
"CSV Import: Cannot open file '%s'",
|
||||
import_params.filepath);
|
||||
return nullptr;
|
||||
}
|
||||
BLI_SCOPED_DEFER([&]() { MEM_delete(buffer); });
|
||||
if (buffer_len == 0) {
|
||||
BKE_reportf(
|
||||
import_params.reports, RPT_ERROR, "CSV Import: empty file '%s'", import_params.filepath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LinearAllocator<> allocator;
|
||||
Array<ColumnInfo> columns_info;
|
||||
csv_parse::CsvParseOptions parse_options;
|
||||
parse_options.delimiter = import_params.delimiter;
|
||||
|
||||
const auto parse_header = [&](const csv_parse::CsvRecord &record) {
|
||||
columns_info.reinitialize(record.size());
|
||||
for (const int i : record.index_range()) {
|
||||
ColumnInfo &column_info = columns_info[i];
|
||||
const StringRef name = csv_parse::unescape_field(
|
||||
record.field_str(i), parse_options, allocator);
|
||||
column_info.name = name;
|
||||
if (!bke::allow_procedural_attribute_access(name) ||
|
||||
bke::attribute_name_is_anonymous(name) || name.is_empty())
|
||||
{
|
||||
column_info.has_invalid_name = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto parse_data_chunk = [&](const csv_parse::CsvRecords &records) {
|
||||
return parse_records_chunk(records, columns_info);
|
||||
};
|
||||
|
||||
const Span<char> buffer_span{buffer, int64_t(buffer_len)};
|
||||
std::optional<Vector<ChunkResult>> parsed_chunks = csv_parse::parse_csv_in_chunks<ChunkResult>(
|
||||
buffer_span, parse_options, parse_header, parse_data_chunk);
|
||||
|
||||
if (!parsed_chunks.has_value()) {
|
||||
BKE_reportf(import_params.reports,
|
||||
RPT_ERROR,
|
||||
"CSV import: failed to parse file '%s'",
|
||||
import_params.filepath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Count the total number of records and compute the offset of each chunk which is used when
|
||||
* flattening the parsed data. */
|
||||
Vector<int> chunk_offsets_vec;
|
||||
chunk_offsets_vec.append(0);
|
||||
for (const ChunkResult &chunk : *parsed_chunks) {
|
||||
chunk_offsets_vec.append(chunk_offsets_vec.last() + chunk.rows_num);
|
||||
}
|
||||
const OffsetIndices<int> chunk_offsets(chunk_offsets_vec);
|
||||
const int points_num = chunk_offsets_vec.last();
|
||||
|
||||
PointCloud *pointcloud = BKE_pointcloud_new_nomain(points_num);
|
||||
|
||||
Array<std::optional<GArray<>>> flattened_attributes;
|
||||
threading::memory_bandwidth_bound_task(points_num * 16, [&]() {
|
||||
threading::parallel_invoke(
|
||||
[&]() {
|
||||
array_utils::copy(VArray<float3>::from_single(float3(0), points_num),
|
||||
pointcloud->positions_for_write());
|
||||
},
|
||||
[&]() {
|
||||
flattened_attributes = flatten_valid_attribute_chunks(
|
||||
columns_info, chunk_offsets, *parsed_chunks);
|
||||
});
|
||||
});
|
||||
|
||||
/* Add all valid attributes to the pointcloud. */
|
||||
bke::MutableAttributeAccessor attributes = pointcloud->attributes_for_write();
|
||||
for (const int column_i : columns_info.index_range()) {
|
||||
std::optional<GArray<>> &attribute = flattened_attributes[column_i];
|
||||
if (!attribute.has_value()) {
|
||||
continue;
|
||||
}
|
||||
const auto *data = new ImplicitSharedValue<GArray<>>(std::move(*attribute));
|
||||
const bke::AttrType type = bke::cpp_type_to_attribute_type(attribute->type());
|
||||
const ColumnInfo &column_info = columns_info[column_i];
|
||||
attributes.add(column_info.name,
|
||||
bke::AttrDomain::Point,
|
||||
type,
|
||||
bke::AttributeInitShared{data->data.data(), *data});
|
||||
data->remove_user_and_delete_if_last();
|
||||
}
|
||||
|
||||
/* Since all positions are set to zero, the bounding box can be updated eagerly to avoid
|
||||
* computing it later. */
|
||||
pointcloud->runtime->bounds_cache.ensure([](Bounds<float3> &r_bounds) {
|
||||
r_bounds.min = float3(0);
|
||||
r_bounds.max = float3(0);
|
||||
});
|
||||
|
||||
return pointcloud;
|
||||
}
|
||||
|
||||
} // namespace blender::io::csv
|
||||
50
blender-5.2.0/source/blender/io/fbx/CMakeLists.txt
Normal file
50
blender-5.2.0/source/blender/io/fbx/CMakeLists.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
importer
|
||||
../common
|
||||
../../editors/include
|
||||
../../makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
IO_fbx.cc
|
||||
importer/fbx_import.cc
|
||||
importer/fbx_import_anim.cc
|
||||
importer/fbx_import_armature.cc
|
||||
importer/fbx_import_material.cc
|
||||
importer/fbx_import_mesh.cc
|
||||
importer/fbx_import_util.cc
|
||||
|
||||
IO_fbx.hh
|
||||
importer/fbx_import.hh
|
||||
importer/fbx_import_anim.hh
|
||||
importer/fbx_import_armature.hh
|
||||
importer/fbx_import_material.hh
|
||||
importer/fbx_import_mesh.hh
|
||||
importer/fbx_import_util.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::animrig
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::blentranslation
|
||||
PRIVATE bf::bmesh
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::nodes
|
||||
PRIVATE bf::imbuf
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
bf_io_common
|
||||
PRIVATE bf::extern::ufbx
|
||||
)
|
||||
|
||||
blender_add_lib(bf_io_fbx "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
41
blender-5.2.0/source/blender/io/fbx/IO_fbx.cc
Normal file
41
blender-5.2.0/source/blender/io/fbx/IO_fbx.cc
Normal file
@@ -0,0 +1,41 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BLI_timeit.hh"
|
||||
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_layer.hh"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
#include "fbx_import.hh"
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace blender {
|
||||
|
||||
using namespace blender::timeit;
|
||||
|
||||
static void report_duration(const char *job, const TimePoint &start_time, const char *path)
|
||||
{
|
||||
Nanoseconds duration = Clock::now() - start_time;
|
||||
fmt::print("FBX {} of '{}' took ", job, BLI_path_basename(path));
|
||||
print_duration(duration);
|
||||
fmt::print("\n");
|
||||
}
|
||||
|
||||
void FBX_import(bContext *C, const FBXImportParams ¶ms)
|
||||
{
|
||||
TimePoint start_time = Clock::now();
|
||||
Main *bmain = CTX_data_main(C);
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
ViewLayer *view_layer = CTX_data_view_layer(C);
|
||||
io::fbx::importer_main(bmain, scene, view_layer, params);
|
||||
report_duration("import", start_time, params.filepath);
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
58
blender-5.2.0/source/blender/io/fbx/IO_fbx.hh
Normal file
58
blender-5.2.0/source/blender/io/fbx/IO_fbx.hh
Normal file
@@ -0,0 +1,58 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
|
||||
#include "IO_orientation.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Mesh;
|
||||
struct bContext;
|
||||
struct ReportList;
|
||||
|
||||
/**
|
||||
* Behavior when the name of an imported material
|
||||
* conflicts with an existing material.
|
||||
*/
|
||||
enum class eFBXMtlNameCollisionMode {
|
||||
MakeUnique = 0,
|
||||
ReferenceExisting = 1,
|
||||
};
|
||||
|
||||
enum class eFBXVertexColorMode {
|
||||
None = 0,
|
||||
sRGB = 1,
|
||||
Linear = 2,
|
||||
};
|
||||
|
||||
struct FBXImportParams {
|
||||
char filepath[FILE_MAX] = "";
|
||||
float global_scale = 1.0f;
|
||||
eFBXMtlNameCollisionMode mtl_name_collision_mode = eFBXMtlNameCollisionMode::MakeUnique;
|
||||
eFBXVertexColorMode vertex_colors = eFBXVertexColorMode::sRGB;
|
||||
bool validate_meshes = true;
|
||||
bool use_custom_normals = true;
|
||||
bool import_subdivision = false;
|
||||
bool use_custom_props = true;
|
||||
bool props_enum_as_string = true;
|
||||
bool ignore_leaf_bones = false;
|
||||
|
||||
bool use_anim = true;
|
||||
float anim_offset = 1.0f;
|
||||
|
||||
ReportList *reports = nullptr;
|
||||
};
|
||||
|
||||
void FBX_import(bContext *C, const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace blender
|
||||
478
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.cc
Normal file
478
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.cc
Normal file
@@ -0,0 +1,478 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_camera.h"
|
||||
#include "BKE_layer.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_library.hh"
|
||||
#include "BKE_light.h"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
|
||||
#include "DNA_camera_types.h"
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_light_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
|
||||
#include "fbx_import.hh"
|
||||
#include "fbx_import_anim.hh"
|
||||
#include "fbx_import_armature.hh"
|
||||
#include "fbx_import_material.hh"
|
||||
#include "fbx_import_mesh.hh"
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
struct FbxImportContext {
|
||||
Main *bmain;
|
||||
const ufbx_scene &fbx;
|
||||
const FBXImportParams ¶ms;
|
||||
std::string base_dir;
|
||||
FbxElementMapping mapping;
|
||||
|
||||
FbxImportContext(Main *main, const ufbx_scene *fbx, const FBXImportParams ¶ms)
|
||||
: bmain(main), fbx(*fbx), params(params)
|
||||
{
|
||||
char basedir[FILE_MAX];
|
||||
BLI_path_split_dir_part(params.filepath, basedir, sizeof(basedir));
|
||||
base_dir = basedir;
|
||||
|
||||
ufbx_transform root_tr;
|
||||
root_tr.translation = ufbx_zero_vec3;
|
||||
root_tr.rotation = this->fbx.metadata.root_rotation;
|
||||
root_tr.scale.x = root_tr.scale.y = root_tr.scale.z = this->fbx.metadata.root_scale;
|
||||
this->mapping.global_conv_matrix = ufbx_transform_to_matrix(&root_tr);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
std::string debug_file_path = params.filepath;
|
||||
debug_file_path = debug_file_path.substr(0, debug_file_path.size() - 4) + "-dbg-b.txt";
|
||||
g_debug_file = BLI_fopen(debug_file_path.c_str(), "wb");
|
||||
#endif
|
||||
}
|
||||
|
||||
~FbxImportContext()
|
||||
{
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
if (g_debug_file) {
|
||||
fclose(g_debug_file);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void import_globals(Scene *scene) const;
|
||||
void import_materials();
|
||||
void import_meshes();
|
||||
void import_cameras();
|
||||
void import_lights();
|
||||
void import_empties();
|
||||
void import_armatures();
|
||||
void import_animation(double fps);
|
||||
|
||||
void setup_hierarchy();
|
||||
};
|
||||
|
||||
void FbxImportContext::import_globals(Scene *scene) const
|
||||
{
|
||||
/* Set scene frame-rate to that of FBX file. */
|
||||
double fps = this->fbx.settings.frames_per_second;
|
||||
scene->r.frs_sec = roundf(fps);
|
||||
scene->r.frs_sec_base = scene->r.frs_sec / fps;
|
||||
}
|
||||
|
||||
void FbxImportContext::import_materials()
|
||||
{
|
||||
for (const ufbx_material *fmat : this->fbx.materials) {
|
||||
Material *mat = nullptr;
|
||||
/* Check if a material with this name already exists in the main database */
|
||||
if (this->params.mtl_name_collision_mode == eFBXMtlNameCollisionMode::ReferenceExisting) {
|
||||
mat = (Material *)BKE_libblock_find_name(this->bmain, ID_MA, fmat->name.data);
|
||||
}
|
||||
|
||||
if (mat == nullptr) {
|
||||
mat = io::fbx::import_material(this->bmain, this->base_dir, *fmat);
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(fmat->props, mat->id, this->params.props_enum_as_string);
|
||||
}
|
||||
}
|
||||
this->mapping.mat_to_material.add(fmat, mat);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_meshes()
|
||||
{
|
||||
io::fbx::import_meshes(*this->bmain, this->fbx, this->mapping, this->params);
|
||||
}
|
||||
|
||||
static bool should_import_camera(const ufbx_scene &fbx, const ufbx_camera *camera)
|
||||
{
|
||||
BLI_assert(camera->instances.count > 0);
|
||||
const ufbx_node *node = camera->instances[0];
|
||||
/* Files produced by MotionBuilder have several cameras at the root,
|
||||
* which just map to "viewports" and should not get imported. */
|
||||
if (node->node_depth == 1 && node->children.count == 0 &&
|
||||
STREQ("MotionBuilder", fbx.metadata.original_application.name.data))
|
||||
{
|
||||
if (STREQ(node->name.data, camera->name.data)) {
|
||||
if (STREQ("Producer Perspective", node->name.data) ||
|
||||
STREQ("Producer Front", node->name.data) || STREQ("Producer Back", node->name.data) ||
|
||||
STREQ("Producer Right", node->name.data) || STREQ("Producer Left", node->name.data) ||
|
||||
STREQ("Producer Top", node->name.data) || STREQ("Producer Bottom", node->name.data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FbxImportContext::import_cameras()
|
||||
{
|
||||
for (const ufbx_camera *fcam : this->fbx.cameras) {
|
||||
if (fcam->instances.count == 0) {
|
||||
continue; /* Ignore if not used by any objects. */
|
||||
}
|
||||
if (!should_import_camera(this->fbx, fcam)) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_node *node = fcam->instances[0];
|
||||
|
||||
Camera *bcam = BKE_camera_add(this->bmain, get_fbx_name(fcam->name, "Camera"));
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(fcam->props, bcam->id, this->params.props_enum_as_string);
|
||||
}
|
||||
|
||||
bcam->type = fcam->projection_mode == UFBX_PROJECTION_MODE_ORTHOGRAPHIC ? CAM_ORTHO :
|
||||
CAM_PERSP;
|
||||
bcam->dof.focus_distance = ufbx_find_real(&fcam->props, "FocusDistance", 10.0f) *
|
||||
this->fbx.metadata.geometry_scale * this->fbx.metadata.root_scale;
|
||||
if (ufbx_find_bool(&fcam->props, "UseDepthOfField", false)) {
|
||||
bcam->dof.flag |= CAM_DOF_ENABLED;
|
||||
}
|
||||
bcam->lens = fcam->focal_length_mm;
|
||||
constexpr double m_to_in = 0.0393700787;
|
||||
bcam->sensor_x = fcam->film_size_inch.x / m_to_in;
|
||||
bcam->sensor_y = fcam->film_size_inch.y / m_to_in;
|
||||
|
||||
/* Note: do not use `fcam->orthographic_extent` to match Python importer behavior, which was
|
||||
* not taking ortho units into account. */
|
||||
bcam->ortho_scale = ufbx_find_real(&fcam->props, "OrthoZoom", 1.0);
|
||||
|
||||
bcam->shiftx = ufbx_find_real(&fcam->props, "FilmOffsetX", 0.0) / (m_to_in * bcam->sensor_x);
|
||||
bcam->shifty = ufbx_find_real(&fcam->props, "FilmOffsetY", 0.0) / (m_to_in * bcam->sensor_x);
|
||||
bcam->clip_start = fcam->near_plane * this->fbx.metadata.root_scale;
|
||||
bcam->clip_end = fcam->far_plane * this->fbx.metadata.root_scale;
|
||||
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_CAMERA, get_fbx_name(node->name));
|
||||
obj->data = id_cast<ID *>(bcam);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_lights()
|
||||
{
|
||||
for (const ufbx_light *flight : this->fbx.lights) {
|
||||
if (flight->instances.count == 0) {
|
||||
continue; /* Ignore if not used by any objects. */
|
||||
}
|
||||
const ufbx_node *node = flight->instances[0];
|
||||
|
||||
Light *lamp = BKE_light_add(this->bmain, get_fbx_name(flight->name, "Light"));
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(flight->props, lamp->id, this->params.props_enum_as_string);
|
||||
}
|
||||
switch (flight->type) {
|
||||
case UFBX_LIGHT_POINT:
|
||||
lamp->type = LA_LOCAL;
|
||||
break;
|
||||
case UFBX_LIGHT_DIRECTIONAL:
|
||||
lamp->type = LA_SUN;
|
||||
break;
|
||||
case UFBX_LIGHT_SPOT:
|
||||
lamp->type = LA_SPOT;
|
||||
lamp->spotsize = DEG2RAD(flight->outer_angle);
|
||||
lamp->spotblend = 1.0f - flight->inner_angle / flight->outer_angle;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
lamp->r = flight->color.x;
|
||||
lamp->g = flight->color.y;
|
||||
lamp->b = flight->color.z;
|
||||
lamp->energy = flight->intensity;
|
||||
lamp->exposure = ufbx_find_real(&flight->props, "Exposure", 0.0);
|
||||
if (flight->cast_shadows) {
|
||||
lamp->mode |= LA_SHADOW;
|
||||
}
|
||||
//@TODO: if hasattr(lamp, "cycles"): lamp.cycles.cast_shadow = lamp.use_shadow
|
||||
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_LAMP, get_fbx_name(node->name));
|
||||
obj->data = id_cast<ID *>(lamp);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_armatures()
|
||||
{
|
||||
io::fbx::import_armatures(*this->bmain, this->fbx, this->mapping, this->params);
|
||||
}
|
||||
|
||||
void FbxImportContext::import_empties()
|
||||
{
|
||||
/* Create empties for fbx nodes. */
|
||||
for (const ufbx_node *node : this->fbx.nodes) {
|
||||
/* Ignore root, bones and nodes for which we have created objects already. */
|
||||
if (node->is_root || this->mapping.node_is_blender_bone.contains(node) ||
|
||||
this->mapping.el_to_object.contains(&node->element))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
/* Ignore nodes at root for cameras (normally already imported, except for ignored cameras)
|
||||
* and camera switchers. */
|
||||
if (ELEM(node->attrib_type, UFBX_ELEMENT_CAMERA, UFBX_ELEMENT_CAMERA_SWITCHER) &&
|
||||
node->node_depth == 1 && node->children.count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Object *obj = BKE_object_add_only_object(this->bmain, OB_EMPTY, get_fbx_name(node->name));
|
||||
obj->data = nullptr;
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::import_animation(double fps)
|
||||
{
|
||||
if (this->params.use_anim) {
|
||||
io::fbx::import_animations(
|
||||
*this->bmain, this->fbx, this->mapping, fps, this->params.anim_offset);
|
||||
}
|
||||
}
|
||||
|
||||
void FbxImportContext::setup_hierarchy()
|
||||
{
|
||||
for (const auto &item : this->mapping.el_to_object.items()) {
|
||||
if (item.value->parent != nullptr) {
|
||||
continue; /* Parent is already set up (e.g. armature). */
|
||||
}
|
||||
const ufbx_node *node = ufbx_as_node(item.key);
|
||||
if (node == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (node->parent) {
|
||||
Object *obj_par = this->mapping.el_to_object.lookup_default(&node->parent->element, nullptr);
|
||||
if (!ELEM(obj_par, nullptr, item.value)) {
|
||||
item.value->parent = obj_par;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fbx_task_run_fn(void * /* user */,
|
||||
ufbx_thread_pool_context ctx,
|
||||
uint32_t /* group */,
|
||||
uint32_t start_index,
|
||||
uint32_t count)
|
||||
{
|
||||
threading::parallel_for_each(IndexRange(start_index, count), [&](const int64_t index) {
|
||||
ufbx_thread_pool_run_task(ctx, index);
|
||||
});
|
||||
}
|
||||
|
||||
static void fbx_task_wait_fn(void * /* user */,
|
||||
ufbx_thread_pool_context /* ctx */,
|
||||
uint32_t /* group */,
|
||||
uint32_t /* max_index */)
|
||||
{
|
||||
/* Empty implementation; #fbx_task_run_fn already waits for the tasks.
|
||||
* This means that only one fbx "task group" is effectively scheduled at once. */
|
||||
}
|
||||
|
||||
void importer_main(Main *bmain, Scene *scene, ViewLayer *view_layer, const FBXImportParams ¶ms)
|
||||
{
|
||||
FILE *file = BLI_fopen(params.filepath, "rb");
|
||||
if (!file) {
|
||||
CLOG_ERROR(&LOG, "Failed to open FBX file '%s'", params.filepath);
|
||||
BKE_reportf(params.reports, RPT_ERROR, "FBX Import: Cannot open file '%s'", params.filepath);
|
||||
return;
|
||||
}
|
||||
|
||||
ufbx_load_opts opts = {};
|
||||
opts.filename.data = params.filepath;
|
||||
opts.filename.length = strlen(params.filepath);
|
||||
opts.evaluate_skinning = false;
|
||||
opts.evaluate_caches = false;
|
||||
opts.load_external_files = false;
|
||||
opts.clean_skin_weights = true;
|
||||
opts.use_blender_pbr_material = true;
|
||||
|
||||
opts.geometry_transform_handling = UFBX_GEOMETRY_TRANSFORM_HANDLING_MODIFY_GEOMETRY;
|
||||
opts.pivot_handling = UFBX_PIVOT_HANDLING_ADJUST_TO_ROTATION_PIVOT;
|
||||
|
||||
opts.space_conversion = UFBX_SPACE_CONVERSION_ADJUST_TRANSFORMS;
|
||||
opts.target_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
opts.target_axes.front = UFBX_COORDINATE_AXIS_NEGATIVE_Y;
|
||||
opts.target_unit_meters = 1.0f / params.global_scale;
|
||||
|
||||
opts.target_camera_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_camera_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Y;
|
||||
opts.target_camera_axes.front = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
opts.target_light_axes.right = UFBX_COORDINATE_AXIS_POSITIVE_X;
|
||||
opts.target_light_axes.up = UFBX_COORDINATE_AXIS_POSITIVE_Y;
|
||||
opts.target_light_axes.front = UFBX_COORDINATE_AXIS_POSITIVE_Z;
|
||||
|
||||
/* Setup ufbx threading to go through our own task system. */
|
||||
opts.thread_opts.pool.run_fn = fbx_task_run_fn;
|
||||
opts.thread_opts.pool.wait_fn = fbx_task_wait_fn;
|
||||
|
||||
ufbx_error fbx_error;
|
||||
ufbx_scene *fbx = ufbx_load_stdio(file, &opts, &fbx_error);
|
||||
fclose(file);
|
||||
|
||||
if (!fbx) {
|
||||
CLOG_ERROR(&LOG,
|
||||
"Failed to import FBX file '%s': '%s'\n",
|
||||
params.filepath,
|
||||
fbx_error.description.data);
|
||||
BKE_reportf(params.reports,
|
||||
RPT_ERROR,
|
||||
"FBX Import: Cannot import file '%s': '%s'",
|
||||
params.filepath,
|
||||
fbx_error.description.data);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerCollection *lc = BKE_layer_collection_get_active_editable(view_layer);
|
||||
if (!ID_IS_EDITABLE(lc->collection)) {
|
||||
BKE_report(params.reports,
|
||||
RPT_WARNING,
|
||||
"Could not find an editable collection in current scene, imported data will not be "
|
||||
"instantiated");
|
||||
}
|
||||
//@TODO: do we need to sort objects by name? (faster to create within blender)
|
||||
|
||||
FbxImportContext ctx(bmain, fbx, params);
|
||||
ctx.import_globals(scene);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
{
|
||||
fprintf(g_debug_file, "Initial NODE local matrices:\n");
|
||||
Vector<const ufbx_node *> nodes;
|
||||
for (const ufbx_node *node : ctx.fbx.nodes) {
|
||||
if (node->is_root) {
|
||||
continue;
|
||||
}
|
||||
nodes.append(node);
|
||||
}
|
||||
std::ranges::sort(nodes, [](const ufbx_node *a, const ufbx_node *b) {
|
||||
int ncmp = strcmp(a->name.data, b->name.data);
|
||||
if (ncmp != 0) {
|
||||
return ncmp < 0;
|
||||
}
|
||||
return a->attrib_type > b->attrib_type;
|
||||
});
|
||||
for (const ufbx_node *node : nodes) {
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(node->node_depth < 2 ? &node->node_to_world :
|
||||
&node->node_to_parent,
|
||||
&node->geometry_to_node);
|
||||
fprintf(g_debug_file, "init NODE %s self.matrix:\n", node->name.data);
|
||||
print_matrix(mtx);
|
||||
}
|
||||
fprintf(g_debug_file, "\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
ctx.import_materials();
|
||||
ctx.import_armatures();
|
||||
ctx.import_meshes();
|
||||
ctx.import_cameras();
|
||||
ctx.import_lights();
|
||||
ctx.import_empties();
|
||||
ctx.import_animation(scene->frames_per_second());
|
||||
ctx.setup_hierarchy();
|
||||
|
||||
ufbx_free_scene(fbx);
|
||||
|
||||
/* Add objects to collection. */
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
BKE_collection_object_add(bmain, lc->collection, obj);
|
||||
}
|
||||
|
||||
/* Select objects, sync layers etc. */
|
||||
BKE_view_layer_base_deselect_all(*bmain, scene, view_layer);
|
||||
BKE_view_layer_synced_ensure(*bmain, scene, view_layer);
|
||||
bool has_instantiated_object = false;
|
||||
bool has_uninstantiated_object = false;
|
||||
for (Object *obj : ctx.mapping.imported_objects) {
|
||||
Base *base = BKE_view_layer_base_find(view_layer, obj);
|
||||
if (!base) {
|
||||
/* Object not instantiated in current viewlayer. */
|
||||
has_uninstantiated_object = true;
|
||||
continue;
|
||||
}
|
||||
has_instantiated_object = true;
|
||||
BKE_view_layer_base_select_and_set_active(view_layer, base);
|
||||
|
||||
int flags = ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION |
|
||||
ID_RECALC_BASE_FLAGS;
|
||||
DEG_id_tag_update_ex(bmain, &obj->id, flags);
|
||||
}
|
||||
|
||||
if (has_instantiated_object && has_uninstantiated_object) {
|
||||
CLOG_ERROR(&LOG, "Some imported objects were not instantiated, while others were");
|
||||
}
|
||||
|
||||
DEG_id_tag_update(&lc->collection->id, ID_RECALC_SYNC_TO_EVAL);
|
||||
|
||||
DEG_id_tag_update(&scene->id, ID_RECALC_BASE_FLAGS);
|
||||
DEG_relations_tag_update(bmain);
|
||||
}
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
26
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.hh
Normal file
26
blender-5.2.0/source/blender/io/fbx/importer/fbx_import.hh
Normal file
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
struct Scene;
|
||||
struct ViewLayer;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void importer_main(Main *bmain,
|
||||
Scene *scene,
|
||||
ViewLayer *view_layer,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
628
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_anim.cc
Normal file
628
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_anim.cc
Normal file
@@ -0,0 +1,628 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_animdata.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_object_types.hh"
|
||||
|
||||
#include "BLI_linear_allocator.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_math_axis_angle.hh"
|
||||
#include "BLI_math_quaternion.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_vector.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "fbx_import_anim.hh"
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
static FCurve *create_fcurve(animrig::Channelbag &channelbag,
|
||||
const animrig::FCurveDescriptor &descriptor,
|
||||
int64_t key_count)
|
||||
{
|
||||
if (!validate::size_fits_in_int(key_count)) {
|
||||
CLOG_WARN(&LOG, "Animation curve too large to import, exceeds max int size");
|
||||
key_count = 0;
|
||||
}
|
||||
FCurve &cu = channelbag.fcurve_ensure(nullptr, descriptor);
|
||||
BKE_fcurve_bezt_resize(cu, key_count);
|
||||
return &cu;
|
||||
}
|
||||
|
||||
static void set_curve_sample(FCurve *curve, int64_t key_index, float time, float value)
|
||||
{
|
||||
BLI_assert(key_index >= 0 && key_index < curve->totvert);
|
||||
BezTriple &bez = curve->bezt[key_index];
|
||||
bez.vec[1][0] = time;
|
||||
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_ANIM;
|
||||
}
|
||||
|
||||
struct ElementAnimations {
|
||||
const ufbx_element *fbx_elem = nullptr;
|
||||
ID *target_id = nullptr;
|
||||
eRotationModes object_rotmode = ROT_MODE_QUAT;
|
||||
int64_t order = 0;
|
||||
const ufbx_anim_prop *prop_position = nullptr;
|
||||
const ufbx_anim_prop *prop_rotation = nullptr;
|
||||
const ufbx_anim_prop *prop_scale = nullptr;
|
||||
const ufbx_anim_prop *prop_blend_shape = nullptr;
|
||||
const ufbx_anim_prop *prop_focal_length = nullptr;
|
||||
const ufbx_anim_prop *prop_focus_dist = nullptr;
|
||||
const ufbx_anim_prop *prop_mat_diffuse = nullptr;
|
||||
};
|
||||
|
||||
static Vector<ElementAnimations> gather_animated_properties(const FbxElementMapping &mapping,
|
||||
const ufbx_anim_layer &flayer)
|
||||
{
|
||||
int64_t order = 0;
|
||||
Map<const ufbx_element *, ElementAnimations> elem_map;
|
||||
for (const ufbx_anim_prop &fprop : flayer.anim_props) {
|
||||
if (fprop.anim_value->curves[0] == nullptr && fprop.anim_value->curves[1] == nullptr &&
|
||||
fprop.anim_value->curves[2] == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool supported_prop = false;
|
||||
//@TODO: "Visibility"?
|
||||
const bool is_position = STREQ(fprop.prop_name.data, "Lcl Translation");
|
||||
const bool is_rotation = STREQ(fprop.prop_name.data, "Lcl Rotation");
|
||||
const bool is_scale = STREQ(fprop.prop_name.data, "Lcl Scaling");
|
||||
const bool is_blend_shape = STREQ(fprop.prop_name.data, "DeformPercent");
|
||||
const bool is_focal_length = STREQ(fprop.prop_name.data, "FocalLength");
|
||||
const bool is_focus_dist = STREQ(fprop.prop_name.data, "FocusDistance");
|
||||
const bool is_diffuse = STREQ(fprop.prop_name.data, "DiffuseColor");
|
||||
if (is_position || is_rotation || is_scale || is_blend_shape || is_focal_length ||
|
||||
is_focus_dist || is_diffuse)
|
||||
{
|
||||
supported_prop = true;
|
||||
}
|
||||
|
||||
if (!supported_prop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool is_anim_camera = is_focal_length || is_focus_dist;
|
||||
const bool is_anim_mat = is_diffuse;
|
||||
|
||||
ID *target_id = nullptr;
|
||||
eRotationModes object_rotmode = ROT_MODE_QUAT;
|
||||
|
||||
if (is_blend_shape) {
|
||||
/* Animating blend shape weight. */
|
||||
Key *target_key = mapping.el_to_shape_key.lookup_default(fprop.element, nullptr);
|
||||
if (target_key != nullptr) {
|
||||
target_id = &target_key->id;
|
||||
}
|
||||
}
|
||||
else if (is_anim_camera) {
|
||||
/* Animating camera property. */
|
||||
if (fprop.element->instances.count > 0) {
|
||||
Object *obj = mapping.el_to_object.lookup_default(&fprop.element->instances[0]->element,
|
||||
nullptr);
|
||||
if (obj != nullptr && obj->type == OB_CAMERA) {
|
||||
target_id = (ID *)obj->data;
|
||||
object_rotmode = static_cast<eRotationModes>(obj->rotmode);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (is_anim_mat) {
|
||||
/* Animating material property. */
|
||||
Material *mat = mapping.mat_to_material.lookup_default((ufbx_material *)fprop.element,
|
||||
nullptr);
|
||||
if (mat != nullptr) {
|
||||
target_id = (ID *)mat;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Animating Bone/Armature/Object property. */
|
||||
const ufbx_node *fnode = ufbx_as_node(fprop.element);
|
||||
Object *obj = nullptr;
|
||||
if (fnode) {
|
||||
obj = mapping.bone_to_armature.lookup_default(fnode, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
obj = mapping.el_to_object.lookup_default(fprop.element, nullptr);
|
||||
}
|
||||
if (obj == nullptr) {
|
||||
continue;
|
||||
}
|
||||
/* Ignore animation of rigged meshes (very hard to handle; matches behavior of python fbx
|
||||
* importer). */
|
||||
if (obj->type == OB_MESH && obj->parent && obj->parent->type == OB_ARMATURE) {
|
||||
continue;
|
||||
}
|
||||
target_id = &obj->id;
|
||||
object_rotmode = static_cast<eRotationModes>(obj->rotmode);
|
||||
}
|
||||
|
||||
if (target_id == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ElementAnimations &anims = elem_map.lookup_or_add_default(fprop.element);
|
||||
anims.fbx_elem = fprop.element;
|
||||
anims.order = order++;
|
||||
anims.target_id = target_id;
|
||||
anims.object_rotmode = object_rotmode;
|
||||
|
||||
if (is_position) {
|
||||
anims.prop_position = &fprop;
|
||||
}
|
||||
if (is_rotation) {
|
||||
anims.prop_rotation = &fprop;
|
||||
}
|
||||
if (is_scale) {
|
||||
anims.prop_scale = &fprop;
|
||||
}
|
||||
if (is_blend_shape) {
|
||||
anims.prop_blend_shape = &fprop;
|
||||
}
|
||||
if (is_focal_length) {
|
||||
anims.prop_focal_length = &fprop;
|
||||
}
|
||||
if (is_focus_dist) {
|
||||
anims.prop_focus_dist = &fprop;
|
||||
}
|
||||
if (is_diffuse) {
|
||||
anims.prop_mat_diffuse = &fprop;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sort returned result in the original fbx file order. */
|
||||
Vector<ElementAnimations> animations(elem_map.values().begin(), elem_map.values().end());
|
||||
std::ranges::sort(animations, [](const ElementAnimations &a, const ElementAnimations &b) {
|
||||
return a.order < b.order;
|
||||
});
|
||||
return animations;
|
||||
}
|
||||
|
||||
static void finalize_curve(FCurve *cu)
|
||||
{
|
||||
if (cu != nullptr) {
|
||||
BKE_fcurve_handles_recalc(*cu);
|
||||
}
|
||||
}
|
||||
|
||||
static void create_transform_curve_desc(const FbxElementMapping &mapping,
|
||||
const ElementAnimations &anim,
|
||||
LinearAllocator<> &curve_name_alloc,
|
||||
Vector<animrig::FCurveDescriptor> &r_curve_desc)
|
||||
{
|
||||
/* For animated bones, prepend bone path to animation curve path. */
|
||||
std::string rna_prefix;
|
||||
std::string group_name_str = get_fbx_name(anim.fbx_elem->name);
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
group_name_str = mapping.node_to_name.lookup_default(fnode, "");
|
||||
rna_prefix = std::string("pose.bones[\"") + group_name_str + "\"].";
|
||||
}
|
||||
|
||||
StringRefNull group_name = curve_name_alloc.copy_string(group_name_str);
|
||||
|
||||
StringRefNull rna_position = curve_name_alloc.copy_string(rna_prefix + "location");
|
||||
|
||||
StringRefNull rna_rotation;
|
||||
int rot_channels = 3;
|
||||
/* Bones are created with quaternion rotation. */
|
||||
eRotationModes rot_mode = is_bone ? ROT_MODE_QUAT : anim.object_rotmode;
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_quaternion");
|
||||
rot_channels = 4;
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_axis_angle");
|
||||
rot_channels = 4;
|
||||
break;
|
||||
default:
|
||||
rna_rotation = curve_name_alloc.copy_string(rna_prefix + "rotation_euler");
|
||||
rot_channels = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
StringRefNull rna_scale = curve_name_alloc.copy_string(rna_prefix + "scale");
|
||||
|
||||
/* Fill the f-curve descriptors. */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
r_curve_desc.append({rna_position, i, {}, {}, group_name});
|
||||
}
|
||||
for (int i = 0; i < rot_channels; i++) {
|
||||
r_curve_desc.append({rna_rotation, i, {}, {}, group_name});
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
r_curve_desc.append({rna_scale, i, {}, {}, group_name});
|
||||
}
|
||||
}
|
||||
|
||||
static void create_transform_curve_data(const FbxElementMapping &mapping,
|
||||
const ufbx_anim *fbx_anim,
|
||||
const ElementAnimations &anim,
|
||||
const double fps,
|
||||
const float anim_offset,
|
||||
FCurve **curves)
|
||||
{
|
||||
const ufbx_node *fnode = ufbx_as_node(anim.fbx_elem);
|
||||
ufbx_matrix bone_xform = ufbx_identity_matrix;
|
||||
const bool is_bone = mapping.node_is_blender_bone.contains(fnode);
|
||||
if (is_bone) {
|
||||
/* Bone transform curves need to be transformed to the bind transform
|
||||
* in joint-local space:
|
||||
* - Calculate local space bind matrix: inv(parent_bind) * bind
|
||||
* - Invert the result; this will be used to transform loc/rot/scale curves. */
|
||||
|
||||
const bool bone_at_scene_root = fnode->node_depth <= 1;
|
||||
ufbx_matrix world_to_arm = ufbx_identity_matrix;
|
||||
if (!bone_at_scene_root) {
|
||||
Object *arm_obj = mapping.bone_to_armature.lookup_default(fnode, nullptr);
|
||||
if (arm_obj != nullptr) {
|
||||
world_to_arm = mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
}
|
||||
}
|
||||
|
||||
bone_xform = mapping.calc_local_bind_matrix(fnode, world_to_arm);
|
||||
bone_xform = ufbx_matrix_invert(&bone_xform);
|
||||
}
|
||||
|
||||
int rot_channels = 3;
|
||||
/* Bones are created with quaternion rotation. */
|
||||
eRotationModes rot_mode = is_bone ? ROT_MODE_QUAT : anim.object_rotmode;
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
rot_channels = 4;
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE:
|
||||
rot_channels = 4;
|
||||
break;
|
||||
default:
|
||||
rot_channels = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Note: Python importer was always creating all pos/rot/scale curves: "due to all FBX
|
||||
* transform magic, we need to add curves for whole loc/rot/scale in any case".
|
||||
*
|
||||
* Also, we create a full transform keyframe at any point where input pos/rot/scale curves have
|
||||
* a keyframe. It should not be needed if we fully imported curves with all their proper
|
||||
* handles, but again currently this is to match Python importer behavior. */
|
||||
const ufbx_anim_curve *input_curves[9] = {};
|
||||
if (anim.prop_position) {
|
||||
input_curves[0] = anim.prop_position->anim_value->curves[0];
|
||||
input_curves[1] = anim.prop_position->anim_value->curves[1];
|
||||
input_curves[2] = anim.prop_position->anim_value->curves[2];
|
||||
}
|
||||
if (anim.prop_rotation) {
|
||||
input_curves[3] = anim.prop_rotation->anim_value->curves[0];
|
||||
input_curves[4] = anim.prop_rotation->anim_value->curves[1];
|
||||
input_curves[5] = anim.prop_rotation->anim_value->curves[2];
|
||||
}
|
||||
if (anim.prop_scale) {
|
||||
input_curves[6] = anim.prop_scale->anim_value->curves[0];
|
||||
input_curves[7] = anim.prop_scale->anim_value->curves[1];
|
||||
input_curves[8] = anim.prop_scale->anim_value->curves[2];
|
||||
}
|
||||
|
||||
/* Figure out timestamps of where any of input curves have a keyframe. */
|
||||
Set<double> unique_key_times;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (input_curves[i] != nullptr) {
|
||||
for (const ufbx_keyframe &key : input_curves[i]->keyframes) {
|
||||
if (key.interpolation == UFBX_INTERPOLATION_CUBIC) {
|
||||
/* Hack: force cubic keyframes to be linear, to match Python importer behavior. */
|
||||
const_cast<ufbx_keyframe &>(key).interpolation = UFBX_INTERPOLATION_LINEAR;
|
||||
}
|
||||
unique_key_times.add(key.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector<double> sorted_key_times(unique_key_times.begin(), unique_key_times.end());
|
||||
std::ranges::sort(sorted_key_times);
|
||||
|
||||
int64_t pos_index = 0;
|
||||
int64_t rot_index = pos_index + 3;
|
||||
int64_t scale_index = rot_index + rot_channels;
|
||||
int64_t tot_curves = scale_index + 3;
|
||||
int64_t key_count = sorted_key_times.size();
|
||||
if (!validate::size_fits_in_int(key_count)) {
|
||||
CLOG_WARN(&LOG, "Animation curve too large to import, exceeds max int size");
|
||||
key_count = 0;
|
||||
}
|
||||
for (int64_t i = 0; i < tot_curves; i++) {
|
||||
BLI_assert_msg(curves[i], "fbx: animation curve was not created successfully");
|
||||
if (curves[i]) {
|
||||
BKE_fcurve_bezt_resize(*curves[i], key_count);
|
||||
}
|
||||
}
|
||||
|
||||
/* Evaluate transforms at all the key times. */
|
||||
math::Quaternion quat_prev = math::Quaternion::identity();
|
||||
for (int64_t i = 0; i < key_count; i++) {
|
||||
double t = sorted_key_times[i];
|
||||
float tf = float(t * fps + anim_offset);
|
||||
ufbx_transform xform = ufbx_evaluate_transform(fbx_anim, fnode, t);
|
||||
|
||||
if (is_bone) {
|
||||
ufbx_matrix matrix = calc_bone_pose_matrix(xform, *fnode, bone_xform);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
|
||||
set_curve_sample(curves[pos_index + 0], i, tf, float(xform.translation.x));
|
||||
set_curve_sample(curves[pos_index + 1], i, tf, float(xform.translation.y));
|
||||
set_curve_sample(curves[pos_index + 2], i, tf, float(xform.translation.z));
|
||||
|
||||
math::Quaternion quat(xform.rotation.w, xform.rotation.x, xform.rotation.y, xform.rotation.z);
|
||||
switch (rot_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
/* Ensure shortest interpolation path between consecutive quaternions. */
|
||||
if (i != 0 && math::dot(quat, quat_prev) < 0.0f) {
|
||||
quat = -quat;
|
||||
}
|
||||
quat_prev = quat;
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, quat.w);
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, quat.x);
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, quat.y);
|
||||
set_curve_sample(curves[rot_index + 3], i, tf, quat.z);
|
||||
break;
|
||||
case ROT_MODE_AXISANGLE: {
|
||||
const math::AxisAngle axis_angle = math::to_axis_angle(quat);
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, axis_angle.angle().radian());
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, axis_angle.axis().x);
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, axis_angle.axis().y);
|
||||
set_curve_sample(curves[rot_index + 3], i, tf, axis_angle.axis().z);
|
||||
} break;
|
||||
default: {
|
||||
math::EulerXYZ euler = math::to_euler(quat);
|
||||
set_curve_sample(curves[rot_index + 0], i, tf, euler.x().radian());
|
||||
set_curve_sample(curves[rot_index + 1], i, tf, euler.y().radian());
|
||||
set_curve_sample(curves[rot_index + 2], i, tf, euler.z().radian());
|
||||
} break;
|
||||
}
|
||||
|
||||
set_curve_sample(curves[scale_index + 0], i, tf, float(xform.scale.x));
|
||||
set_curve_sample(curves[scale_index + 1], i, tf, float(xform.scale.y));
|
||||
set_curve_sample(curves[scale_index + 2], i, tf, float(xform.scale.z));
|
||||
}
|
||||
}
|
||||
|
||||
static void create_camera_curves(const ufbx_metadata &metadata,
|
||||
const ElementAnimations &anim,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
if (anim.target_id == nullptr || GS(anim.target_id->name) != ID_CA) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (anim.prop_focal_length != nullptr) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_focal_length->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(channelbag, {"lens", 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
float val = float(fkey.value);
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
|
||||
if (anim.prop_focus_dist != nullptr) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_focus_dist->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(
|
||||
channelbag, {"dof.focus_distance", 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
/* Animation curves containing camera focus distance have values multiplied by 1000.0 */
|
||||
float val = float(fkey.value / 1000.0 * metadata.geometry_scale * metadata.root_scale);
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
}
|
||||
|
||||
static void create_material_curves(const ElementAnimations &anim,
|
||||
bAction *action,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
if (anim.target_id == nullptr || GS(anim.target_id->name) != ID_MA) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *rna_path_1 = "diffuse_color";
|
||||
const char *rna_path_2 = "nodes[\"Principled BSDF\"].inputs[0].default_value";
|
||||
|
||||
/* Also create animation curves for the node tree diffuse color input. */
|
||||
Material *target_mat = id_cast<Material *>(anim.target_id);
|
||||
ID *target_ntree = reinterpret_cast<ID *>(target_mat->nodetree);
|
||||
animrig::Action &act = action->wrap();
|
||||
const animrig::Slot *slot = animrig::assign_action_ensure_slot_for_keying(act, *target_ntree);
|
||||
BLI_assert(slot != nullptr);
|
||||
UNUSED_VARS_NDEBUG(slot);
|
||||
animrig::Channelbag &chbag_node = animrig::action_channelbag_ensure(*action, *target_ntree);
|
||||
|
||||
if (anim.prop_mat_diffuse != nullptr) {
|
||||
for (int ch = 0; ch < 3; ch++) {
|
||||
const ufbx_anim_curve *input_curve = anim.prop_mat_diffuse->anim_value->curves[ch];
|
||||
FCurve *curve_1 = create_fcurve(channelbag, {rna_path_1, ch}, input_curve->keyframes.count);
|
||||
FCurve *curve_2 = create_fcurve(chbag_node, {rna_path_2, ch}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve_1->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
float tf = float(fkey.time * fps + anim_offset);
|
||||
float val = float(fkey.value);
|
||||
set_curve_sample(curve_1, i, tf, val);
|
||||
set_curve_sample(curve_2, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve_1);
|
||||
finalize_curve(curve_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void create_blend_shape_curves(const ElementAnimations &anim,
|
||||
animrig::Channelbag &channelbag,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
const ufbx_blend_channel *fchan = ufbx_as_blend_channel(anim.prop_blend_shape->element);
|
||||
BLI_assert(fchan != nullptr);
|
||||
std::string rna_path = std::string("key_blocks[\"") + fchan->target_shape->name.data +
|
||||
"\"].value";
|
||||
const ufbx_anim_curve *input_curve = anim.prop_blend_shape->anim_value->curves[0];
|
||||
FCurve *curve = create_fcurve(channelbag, {rna_path, 0}, input_curve->keyframes.count);
|
||||
for (int64_t i = 0; i < curve->totvert; i++) {
|
||||
const ufbx_keyframe &fkey = input_curve->keyframes[i];
|
||||
double t = fkey.time;
|
||||
float tf = float(t * fps + anim_offset);
|
||||
float val = float(fkey.value / 100.0); /* FBX shape weights are 0..100 range. */
|
||||
set_curve_sample(curve, i, tf, val);
|
||||
}
|
||||
finalize_curve(curve);
|
||||
}
|
||||
|
||||
void import_animations(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
const FbxElementMapping &mapping,
|
||||
const double fps,
|
||||
const float anim_offset)
|
||||
{
|
||||
/* Note: mixing is completely ignored for now, each layer results in an independent set of
|
||||
* actions. */
|
||||
for (const ufbx_anim_stack *fstack : fbx.anim_stacks) {
|
||||
for (const ufbx_anim_layer *flayer : fstack->layers) {
|
||||
Vector<ElementAnimations> animations = gather_animated_properties(mapping, *flayer);
|
||||
if (animations.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Create action for this layer. */
|
||||
std::string action_name = fstack->name.data;
|
||||
if (!STREQ(fstack->name.data, flayer->name.data) && fstack->layers.count != 1) {
|
||||
action_name += '|';
|
||||
action_name += flayer->name.data;
|
||||
}
|
||||
animrig::Action &action = animrig::action_add(bmain, action_name);
|
||||
id_fake_user_set(&action.id);
|
||||
action.layer_keystrip_ensure();
|
||||
animrig::StripKeyframeData &strip_data =
|
||||
action.layer(0)->strip(0)->data<animrig::StripKeyframeData>(action);
|
||||
|
||||
/* Figure out the set of IDs that are animated. We want to preserve the order
|
||||
* of this set to match order of animations inside the FBX file. */
|
||||
VectorSet<ID *> animated_ids;
|
||||
Map<ID *, Vector<const ElementAnimations *>> id_to_anims;
|
||||
for (const ElementAnimations &anim : animations) {
|
||||
animated_ids.add(anim.target_id);
|
||||
Vector<const ElementAnimations *> &anims = id_to_anims.lookup_or_add_default(
|
||||
anim.target_id);
|
||||
anims.append(&anim);
|
||||
}
|
||||
|
||||
/* Create action slots for each animated ID. */
|
||||
for (ID *id : animated_ids) {
|
||||
/* Create a slot for this ID. */
|
||||
BLI_assert(id != nullptr);
|
||||
const std::string slot_name = id->name;
|
||||
animrig::Slot &slot = action.slot_add_for_id_type(GS(id->name));
|
||||
action.slot_identifier_define(slot, slot_name);
|
||||
|
||||
/* Assign this action & slot to ID. */
|
||||
const AnimData *adt = BKE_animdata_ensure_id(id);
|
||||
BLI_assert_msg(adt != nullptr, "fbx: could not create animation data for an ID");
|
||||
if (adt->action == nullptr) {
|
||||
bool ok = animrig::assign_action(&action, *id);
|
||||
BLI_assert_msg(ok, "fbx: could not assign action to ID");
|
||||
UNUSED_VARS_NDEBUG(ok);
|
||||
}
|
||||
if (adt->slot_handle == animrig::Slot::unassigned) {
|
||||
animrig::ActionSlotAssignmentResult res = animrig::assign_action_slot(&slot, *id);
|
||||
BLI_assert_msg(res == animrig::ActionSlotAssignmentResult::OK,
|
||||
"fbx: failed to assign slot to ID");
|
||||
UNUSED_VARS_NDEBUG(res);
|
||||
}
|
||||
animrig::Channelbag &channelbag = strip_data.channelbag_for_slot_ensure(slot);
|
||||
|
||||
/* Create animation curves for this ID. */
|
||||
Vector<const ElementAnimations *> id_anims = id_to_anims.lookup(id);
|
||||
/* Batch create the transform curves: creating them one by one is not very fast,
|
||||
* especially for armatures where many bones often are animated. So first create
|
||||
* their descriptors, then create the f-curves in one step, and finally fill their data. */
|
||||
Vector<animrig::FCurveDescriptor> curve_desc;
|
||||
Vector<int64_t> anim_transform_curve_index(id_anims.size());
|
||||
LinearAllocator name_alloc;
|
||||
for (const int64_t index : id_anims.index_range()) {
|
||||
const ElementAnimations *anim = id_anims[index];
|
||||
if (anim->prop_position || anim->prop_rotation || anim->prop_scale) {
|
||||
anim_transform_curve_index[index] = curve_desc.size();
|
||||
create_transform_curve_desc(mapping, *anim, name_alloc, curve_desc);
|
||||
}
|
||||
else {
|
||||
anim_transform_curve_index[index] = -1;
|
||||
}
|
||||
}
|
||||
Vector<FCurve *> transform_curves;
|
||||
if (!curve_desc.is_empty()) {
|
||||
transform_curves = channelbag.fcurve_create_many(nullptr, curve_desc.as_span());
|
||||
}
|
||||
|
||||
for (const int64_t index : id_anims.index_range()) {
|
||||
const ElementAnimations *anim = id_anims[index];
|
||||
if (anim->prop_position || anim->prop_rotation || anim->prop_scale) {
|
||||
create_transform_curve_data(mapping,
|
||||
flayer->anim,
|
||||
*anim,
|
||||
fps,
|
||||
anim_offset,
|
||||
transform_curves.data() +
|
||||
anim_transform_curve_index[index]);
|
||||
}
|
||||
if (anim->prop_focal_length || anim->prop_focus_dist) {
|
||||
create_camera_curves(fbx.metadata, *anim, channelbag, fps, anim_offset);
|
||||
}
|
||||
if (anim->prop_mat_diffuse) {
|
||||
create_material_curves(*anim, &action, channelbag, fps, anim_offset);
|
||||
}
|
||||
if (anim->prop_blend_shape) {
|
||||
create_blend_shape_curves(*anim, channelbag, fps, anim_offset);
|
||||
}
|
||||
}
|
||||
|
||||
for (FCurve *curve : transform_curves) {
|
||||
finalize_curve(curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_animations(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
const FbxElementMapping &mapping,
|
||||
const double fps,
|
||||
const float anim_offset);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,433 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_armature.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "BLI_math_vector.hh"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "ED_armature.hh"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
|
||||
#include "fbx_import_armature.hh"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
struct ArmatureImportContext {
|
||||
Main &bmain;
|
||||
const ufbx_scene &fbx;
|
||||
const FBXImportParams ¶ms;
|
||||
FbxElementMapping &mapping;
|
||||
|
||||
ArmatureImportContext(Main &main,
|
||||
const ufbx_scene &fbx,
|
||||
const FBXImportParams ¶ms,
|
||||
FbxElementMapping &mapping)
|
||||
: bmain(main), fbx(fbx), params(params), mapping(mapping)
|
||||
{
|
||||
}
|
||||
|
||||
Object *create_armature_for_node(const ufbx_node *node);
|
||||
void create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
const float parent_bone_size);
|
||||
void find_armatures(const ufbx_node *node);
|
||||
void calc_bone_bind_matrices();
|
||||
};
|
||||
|
||||
Object *ArmatureImportContext::create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
BLI_assert_msg(node != nullptr, "fbx: node for armature creation should not be null");
|
||||
|
||||
const char *arm_name = get_fbx_name(node->name, "Armature");
|
||||
const char *obj_name = get_fbx_name(node->name, "Armature");
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "create ARMATURE %s\n", arm_name);
|
||||
#endif
|
||||
|
||||
bArmature *arm = BKE_armature_add(&this->bmain, arm_name);
|
||||
Object *obj = BKE_object_add_only_object(&this->bmain, OB_ARMATURE, obj_name);
|
||||
obj->dtx |= OB_DRAW_IN_FRONT;
|
||||
obj->data = id_cast<ID *>(arm);
|
||||
this->mapping.imported_objects.add(obj);
|
||||
if (!node->is_root) {
|
||||
this->mapping.el_to_object.add(&node->element, obj);
|
||||
if (this->params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, this->params.props_enum_as_string);
|
||||
}
|
||||
node_matrix_to_obj(node, obj, this->mapping);
|
||||
|
||||
/* Record world to fbx node matrix for the armature object. */
|
||||
ufbx_matrix world_to_arm = ufbx_matrix_invert(&node->node_to_world);
|
||||
this->mapping.armature_world_to_arm_node_matrix.add(obj, world_to_arm);
|
||||
|
||||
/* Record world to posed root node matrix. */
|
||||
if (node->bind_pose && node->bind_pose->is_bind_pose) {
|
||||
for (const ufbx_bone_pose &pose : node->bind_pose->bone_poses) {
|
||||
if (pose.bone_node == node) {
|
||||
world_to_arm = ufbx_matrix_invert(&pose.bone_to_world);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->mapping.armature_world_to_arm_pose_matrix.add(obj, world_to_arm);
|
||||
}
|
||||
else {
|
||||
/* For armatures created at root, make them have the same rotation/scale
|
||||
* as done by ufbx for all regular nodes. */
|
||||
ufbx_matrix_to_obj(this->mapping.global_conv_matrix, obj);
|
||||
ufbx_matrix world_to_arm = ufbx_matrix_invert(&this->mapping.global_conv_matrix);
|
||||
this->mapping.armature_world_to_arm_pose_matrix.add(obj, world_to_arm);
|
||||
this->mapping.armature_world_to_arm_node_matrix.add(obj, world_to_arm);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
void ArmatureImportContext::create_armature_bones(const ufbx_node *node,
|
||||
Object *arm_obj,
|
||||
const Set<const ufbx_node *> &bone_nodes,
|
||||
EditBone *parent_bone,
|
||||
const ufbx_matrix &parent_mtx,
|
||||
const ufbx_matrix &world_to_arm,
|
||||
const float parent_bone_size)
|
||||
{
|
||||
BLI_assert(node != nullptr && !node->is_root);
|
||||
bArmature *arm = id_cast<bArmature *>(arm_obj->data);
|
||||
|
||||
/* Create an EditBone. */
|
||||
std::string name;
|
||||
if (node->is_geometry_transform_helper) {
|
||||
/* Name geometry transform adjustment helpers with parent name and _GeomAdjust suffix. */
|
||||
name = get_fbx_name(node->parent->name, "Bone") + std::string("_GeomAdjust");
|
||||
}
|
||||
else {
|
||||
name = get_fbx_name(node->name, "Bone");
|
||||
}
|
||||
EditBone *bone = ED_armature_ebone_add(arm, name.c_str());
|
||||
this->mapping.node_to_name.add(node, bone->name);
|
||||
this->mapping.node_is_blender_bone.add(node);
|
||||
this->mapping.bone_to_armature.add(node, arm_obj);
|
||||
bone->flag |= BONE_SELECTED;
|
||||
bone->parent = parent_bone;
|
||||
if (node->inherit_mode == UFBX_INHERIT_MODE_IGNORE_PARENT_SCALE) {
|
||||
bone->inherit_scale_mode = BONE_INHERIT_SCALE_NONE;
|
||||
}
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
"create BONE %s (parent %s) parent_mtx:\n",
|
||||
node->name.data,
|
||||
parent_bone ? parent_bone->name : "");
|
||||
print_matrix(parent_mtx);
|
||||
#endif
|
||||
|
||||
ufbx_matrix bone_mtx = this->mapping.get_node_bind_matrix(node);
|
||||
bone_mtx = ufbx_matrix_mul(&world_to_arm, &bone_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
bone_mtx.cols[1] = ufbx_vec3_normalize(bone_mtx.cols[1]);
|
||||
bone_mtx.cols[2] = ufbx_vec3_normalize(bone_mtx.cols[2]);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, " bone_mtx:\n");
|
||||
print_matrix(bone_mtx);
|
||||
#endif
|
||||
|
||||
/* Calculate bone tail position. */
|
||||
float bone_size = 0.0f;
|
||||
int child_bone_count = 0;
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Estimate child position from local transform, but if the child
|
||||
* is skinned/posed then use the posed transform instead. */
|
||||
ufbx_vec3 pos = fchild->local_transform.translation;
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fchild)) {
|
||||
ufbx_matrix local_mtx = this->mapping.calc_local_bind_matrix(fchild, world_to_arm);
|
||||
pos = local_mtx.cols[3];
|
||||
}
|
||||
bone_size += math::length(float3(pos.x, pos.y, pos.z));
|
||||
child_bone_count++;
|
||||
}
|
||||
if (child_bone_count > 0) {
|
||||
bone_size /= child_bone_count;
|
||||
}
|
||||
else {
|
||||
/* This is leaf bone, set length to parent bone length. */
|
||||
bone_size = parent_bone_size;
|
||||
/* If we do not have actual pose/skin matrix for this bone, apply local transform onto parent
|
||||
* matrix. */
|
||||
if (!this->mapping.bone_to_bind_matrix.contains(node)) {
|
||||
ufbx_matrix offset_mtx = ufbx_transform_to_matrix(&node->local_transform);
|
||||
bone_mtx = ufbx_matrix_mul(&parent_mtx, &offset_mtx);
|
||||
bone_mtx.cols[0] = ufbx_vec3_normalize(bone_mtx.cols[0]);
|
||||
bone_mtx.cols[1] = ufbx_vec3_normalize(bone_mtx.cols[1]);
|
||||
bone_mtx.cols[2] = ufbx_vec3_normalize(bone_mtx.cols[2]);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, " bone_mtx adj for non-posed bones:\n");
|
||||
print_matrix(bone_mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
/* Zero length bones are automatically collapsed into their parent when you leave edit mode,
|
||||
* so enforce a minimum length. */
|
||||
bone_size = math::max(bone_size, 0.01f);
|
||||
this->mapping.bone_to_length.add(node, bone_size);
|
||||
|
||||
bone->tail[0] = 0.0f;
|
||||
bone->tail[1] = bone_size;
|
||||
bone->tail[2] = 0.0f;
|
||||
/* Set bone matrix. */
|
||||
float bone_matrix[4][4];
|
||||
matrix_to_m44(bone_mtx, bone_matrix);
|
||||
ED_armature_ebone_from_mat4(bone, bone_matrix);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
" length %.3f head (%.3f %.3f %.3f) tail (%.3f %.3f %.3f)\n",
|
||||
adjf(bone_size),
|
||||
adjf(bone->head[0]),
|
||||
adjf(bone->head[1]),
|
||||
adjf(bone->head[2]),
|
||||
adjf(bone->tail[0]),
|
||||
adjf(bone->tail[1]),
|
||||
adjf(bone->tail[2]));
|
||||
#endif
|
||||
|
||||
/* Mark bone as connected to parent if head approximately in the same place as parent tail, in
|
||||
* both rest pose and current pose. */
|
||||
if (parent_bone != nullptr) {
|
||||
float3 self_head_rest(bone->head);
|
||||
float3 par_tail_rest(parent_bone->tail);
|
||||
const float connect_dist = 1.0e-4f;
|
||||
const float connect_dist_sq = connect_dist * connect_dist;
|
||||
float dist_sq_rest = math::distance_squared(self_head_rest, par_tail_rest);
|
||||
if (dist_sq_rest < connect_dist_sq) {
|
||||
/* Bones seem connected in rest pose, now check their current transforms. */
|
||||
ufbx_vec3 self_head_cur_u = node->node_to_world.cols[3];
|
||||
ufbx_vec3 par_tail;
|
||||
par_tail.x = 0;
|
||||
par_tail.y = parent_bone_size;
|
||||
par_tail.z = 0;
|
||||
ufbx_vec3 par_tail_cur_u = ufbx_transform_position(&node->parent->node_to_world, par_tail);
|
||||
float3 self_head_cur(self_head_cur_u.x, self_head_cur_u.y, self_head_cur_u.z);
|
||||
float3 par_tail_cur(par_tail_cur_u.x, par_tail_cur_u.y, par_tail_cur_u.z);
|
||||
float dist_sq_cur = math::distance_squared(self_head_cur, par_tail_cur);
|
||||
|
||||
if (dist_sq_cur < connect_dist_sq) {
|
||||
/* Connected in both cases. */
|
||||
bone->flag |= BONE_CONNECTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recurse into child bones. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!bone_nodes.contains(fchild)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool skip_child = false;
|
||||
if (this->params.ignore_leaf_bones) {
|
||||
if (node->children.count == 1 && fchild->children.count == 0 &&
|
||||
!mapping.bone_is_skinned.contains(fchild))
|
||||
{
|
||||
skip_child = true;
|
||||
/* We are skipping this bone, but still record it --
|
||||
* so that later code does not try to create an empty for it. */
|
||||
this->mapping.node_is_blender_bone.add(fchild);
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip_child) {
|
||||
create_armature_bones(fchild, arm_obj, bone_nodes, bone, bone_mtx, world_to_arm, bone_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Need to create armature if we are root bone, or any child is a non-root bone. */
|
||||
static bool need_create_armature_for_node(const ufbx_node *node)
|
||||
{
|
||||
if (node->bone && node->bone->is_root) {
|
||||
return true;
|
||||
}
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (fchild->bone && !fchild->bone->is_root) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void find_bones(const ufbx_node *node, Set<const ufbx_node *> &r_bones)
|
||||
{
|
||||
if (node->bone != nullptr) {
|
||||
r_bones.add(node);
|
||||
}
|
||||
for (const ufbx_node *child : node->children) {
|
||||
find_bones(child, r_bones);
|
||||
}
|
||||
}
|
||||
|
||||
static void find_fake_bones(const ufbx_node *root_node,
|
||||
const Set<const ufbx_node *> &bones,
|
||||
Set<const ufbx_node *> &r_fake_bones)
|
||||
{
|
||||
for (const ufbx_node *bone_node : bones) {
|
||||
const ufbx_node *node = bone_node->parent;
|
||||
while (!ELEM(node, nullptr, root_node)) {
|
||||
if (node->bone == nullptr) {
|
||||
r_fake_bones.add(node);
|
||||
}
|
||||
node = node->parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Set<const ufbx_node *> find_all_bones(const ufbx_node *root_node)
|
||||
{
|
||||
/* Find regular FBX bones nodes anywhere under our root armature node. */
|
||||
Set<const ufbx_node *> bones;
|
||||
find_bones(root_node, bones);
|
||||
|
||||
/* There might be non-bone nodes in between, e.g. FBX structure being like:
|
||||
* BoneA -> MeshB -> BoneC -> MeshD. Blender Armature can only contain
|
||||
* bones, so in this case "MeshB" has to have a bone created for it as well.
|
||||
* "Fake bones" are any non-bone FBX nodes in between root armature node
|
||||
* and the actual bone node. */
|
||||
Set<const ufbx_node *> fake_bones;
|
||||
find_fake_bones(root_node, bones, fake_bones);
|
||||
for (const ufbx_node *b : fake_bones) {
|
||||
bones.add(b);
|
||||
}
|
||||
return bones;
|
||||
}
|
||||
|
||||
void ArmatureImportContext::find_armatures(const ufbx_node *node)
|
||||
{
|
||||
const bool needs_arm = need_create_armature_for_node(node);
|
||||
if (needs_arm) {
|
||||
/* Create armature. */
|
||||
Object *arm_obj = this->create_armature_for_node(node);
|
||||
ufbx_matrix world_to_arm = this->mapping.armature_world_to_arm_pose_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
|
||||
Set<const ufbx_node *> bone_nodes = find_all_bones(node);
|
||||
|
||||
/* Create bones in edit mode. */
|
||||
bArmature *arm = id_cast<bArmature *>(arm_obj->data);
|
||||
ED_armature_to_edit(arm);
|
||||
this->mapping.node_to_name.add(node, BKE_id_name(arm_obj->id));
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (bone_nodes.contains(fchild)) {
|
||||
create_armature_bones(
|
||||
fchild, arm_obj, bone_nodes, nullptr, ufbx_identity_matrix, world_to_arm, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
ED_armature_from_edit(&this->bmain, arm);
|
||||
ED_armature_edit_free(arm);
|
||||
|
||||
/* Setup pose on the object, and custom properties on the bone pose channels. */
|
||||
for (const ufbx_node *fbone : bone_nodes) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fbone)) {
|
||||
continue; /* Blender bone was not created for it (e.g. root bone in some cases). */
|
||||
}
|
||||
bPoseChannel *pchan = BKE_pose_channel_find_name(
|
||||
arm_obj->pose, this->mapping.node_to_name.lookup_default(fbone, "").c_str());
|
||||
if (pchan == nullptr) {
|
||||
continue;
|
||||
}
|
||||
read_custom_properties(fbone->props, *pchan, this->params.props_enum_as_string);
|
||||
|
||||
/* For bones that have rest/bind information, put their current transform into
|
||||
* the current pose. */
|
||||
if (this->mapping.bone_to_bind_matrix.contains(fbone)) {
|
||||
ufbx_matrix bind_local_mtx = this->mapping.calc_local_bind_matrix(fbone, world_to_arm);
|
||||
ufbx_matrix bind_local_mtx_inv = ufbx_matrix_invert(&bind_local_mtx);
|
||||
ufbx_transform xform = fbone->local_transform;
|
||||
if (fbone->node_depth <= 1) {
|
||||
ufbx_matrix matrix = ufbx_matrix_mul(&world_to_arm, &fbone->node_to_world);
|
||||
xform = ufbx_matrix_to_transform(&matrix);
|
||||
}
|
||||
ufbx_matrix pose_mtx = calc_bone_pose_matrix(xform, *fbone, bind_local_mtx_inv);
|
||||
|
||||
float pchan_matrix[4][4];
|
||||
matrix_to_m44(pose_mtx, pchan_matrix);
|
||||
BKE_pchan_apply_mat4(pchan, pchan_matrix, false);
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "set POSE matrix of %s matrix_basis:\n", fbone->name.data);
|
||||
print_matrix(pose_mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recurse into children that have not been turned into bones yet. */
|
||||
for (const ufbx_node *fchild : node->children) {
|
||||
if (!this->mapping.node_is_blender_bone.contains(fchild)) {
|
||||
this->find_armatures(fchild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ArmatureImportContext::calc_bone_bind_matrices()
|
||||
{
|
||||
/* Figure out bind matrices for bone nodes:
|
||||
* - Get them from "pose" objects in FBX that are marked as "bind pose",
|
||||
* - From all "skin deformer" objects in FBX; these override the ones from "poses".
|
||||
* - For all the bone nodes that do not have a matrix yet, record their world matrix
|
||||
* as bind matrix. */
|
||||
for (const ufbx_pose *fpose : this->fbx.poses) {
|
||||
if (!fpose->is_bind_pose) {
|
||||
continue;
|
||||
}
|
||||
for (const ufbx_bone_pose &bone_pose : fpose->bone_poses) {
|
||||
const ufbx_matrix &bind_matrix = bone_pose.bone_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(bone_pose.bone_node, bind_matrix);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone POSE matrix %s\n", bone_pose.bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
for (const ufbx_skin_deformer *fskin : this->fbx.skin_deformers) {
|
||||
for (const ufbx_skin_cluster *fbone : fskin->clusters) {
|
||||
const ufbx_matrix &bind_matrix = fbone->bind_to_world;
|
||||
this->mapping.bone_to_bind_matrix.add_overwrite(fbone->bone_node, bind_matrix);
|
||||
this->mapping.bone_is_skinned.add(fbone->bone_node);
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "bone SKIN matrix %s\n", fbone->bone_node->name.data);
|
||||
print_matrix(bind_matrix);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void import_armatures(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms)
|
||||
{
|
||||
ArmatureImportContext context(bmain, fbx, params, mapping);
|
||||
context.calc_bone_bind_matrices();
|
||||
context.find_armatures(fbx.root_node);
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_armatures(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,467 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_image.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_node_legacy_types.hh"
|
||||
#include "BKE_node_runtime.hh"
|
||||
#include "BKE_node_tree_update.hh"
|
||||
|
||||
#include "BLI_math_vector.hh"
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "DNA_material_types.h"
|
||||
|
||||
#include "NOD_shader.h"
|
||||
|
||||
#include "IMB_colormanagement.hh"
|
||||
#include "IMB_imbuf_types.hh"
|
||||
|
||||
#include "fbx_import_material.hh"
|
||||
|
||||
#include "ufbx.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
/* Nodes are arranged in columns by type, with manually placed x coordinates
|
||||
* based on node widths. */
|
||||
static constexpr float node_locx_texcoord = -880.0f;
|
||||
static constexpr float node_locx_mapping = -680.0f;
|
||||
static constexpr float node_locx_image = -480.0f;
|
||||
static constexpr float node_locx_normalmap = -200.0f;
|
||||
static constexpr float node_locx_bsdf = 0.0f;
|
||||
static constexpr float node_locx_output = 280.0f;
|
||||
|
||||
/* Nodes are arranged in rows; one row for each image being used. */
|
||||
static constexpr float node_locy_top = 300.0f;
|
||||
static constexpr float node_locy_step = 300.0f;
|
||||
|
||||
/* Add a node of the given type at the given location. */
|
||||
static bNode *add_node(bNodeTree *ntree, int type, float x, float y)
|
||||
{
|
||||
bNode *node = bke::node_add_static_node(nullptr, *ntree, type);
|
||||
node->location[0] = x;
|
||||
node->location[1] = y;
|
||||
return node;
|
||||
}
|
||||
|
||||
static void link_sockets(bNodeTree *ntree,
|
||||
bNode *from_node,
|
||||
const char *from_node_id,
|
||||
bNode *to_node,
|
||||
const char *to_node_id)
|
||||
{
|
||||
bNodeSocket *from_sock{bke::node_find_socket(*from_node, SOCK_OUT, UString(from_node_id))};
|
||||
bNodeSocket *to_sock{bke::node_find_socket(*to_node, SOCK_IN, UString(to_node_id))};
|
||||
BLI_assert(from_sock && to_sock);
|
||||
bke::node_add_link(*ntree, *from_node, *from_sock, *to_node, *to_sock);
|
||||
}
|
||||
|
||||
static void set_socket_float(const char *socket_id, const float value, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_FLOAT);
|
||||
bNodeSocketValueFloat *dst = socket->default_value_typed<bNodeSocketValueFloat>();
|
||||
dst->value = value;
|
||||
}
|
||||
|
||||
static void set_socket_rgb(const char *socket_id, float vr, float vg, float vb, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_RGBA);
|
||||
bNodeSocketValueRGBA *dst = socket->default_value_typed<bNodeSocketValueRGBA>();
|
||||
dst->value[0] = vr;
|
||||
dst->value[1] = vg;
|
||||
dst->value[2] = vb;
|
||||
dst->value[3] = 1.0f;
|
||||
}
|
||||
|
||||
static void set_socket_vector(const char *socket_id, float vx, float vy, float vz, bNode *node)
|
||||
{
|
||||
bNodeSocket *socket{bke::node_find_socket(*node, SOCK_IN, UString(socket_id))};
|
||||
BLI_assert(socket && socket->type == SOCK_VECTOR);
|
||||
bNodeSocketValueVector *dst = socket->default_value_typed<bNodeSocketValueVector>();
|
||||
dst->value[0] = vx;
|
||||
dst->value[1] = vy;
|
||||
dst->value[2] = vz;
|
||||
}
|
||||
|
||||
static float set_bsdf_float_param(bNode *bsdf,
|
||||
const ufbx_material_map &umap,
|
||||
const char *socket,
|
||||
float def,
|
||||
float min = 0.0f,
|
||||
float max = 1.0f,
|
||||
float multiplier = 1.0f)
|
||||
{
|
||||
if (!umap.has_value) {
|
||||
return def * multiplier;
|
||||
}
|
||||
float value = umap.value_real * multiplier;
|
||||
value = math::clamp(value, min, max);
|
||||
set_socket_float(socket, value, bsdf);
|
||||
return value;
|
||||
}
|
||||
|
||||
static float3 set_bsdf_color_param(bNode *bsdf,
|
||||
const ufbx_material_map &umap,
|
||||
const char *socket,
|
||||
float3 def,
|
||||
float3 min = float3(0.0f),
|
||||
float3 max = float3(1.0f))
|
||||
{
|
||||
if (!umap.has_value || umap.value_components < 3) {
|
||||
return def;
|
||||
}
|
||||
float3 value = float3(umap.value_vec3.x, umap.value_vec3.y, umap.value_vec3.z);
|
||||
value = math::clamp(value, min, max);
|
||||
set_socket_rgb(socket, value.x, value.y, value.z, bsdf);
|
||||
return value;
|
||||
}
|
||||
|
||||
static void set_bsdf_socket_values(bNode *bsdf, Material *mat, const ufbx_material &fmat)
|
||||
{
|
||||
float3 base_color = set_bsdf_color_param(bsdf, fmat.pbr.base_color, "Base Color", float3(0.8f));
|
||||
mat->r = base_color.x;
|
||||
mat->g = base_color.y;
|
||||
mat->b = base_color.z;
|
||||
|
||||
float roughness = set_bsdf_float_param(bsdf, fmat.pbr.roughness, "Roughness", 0.5f);
|
||||
mat->roughness = roughness;
|
||||
|
||||
float metallic = set_bsdf_float_param(bsdf, fmat.pbr.metalness, "Metallic", 0.0f);
|
||||
mat->metallic = metallic;
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_ior, "IOR", 1.5f, 1.0f, 1000.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.opacity, "Alpha", 1.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.diffuse_roughness, "Diffuse Roughness", 0.0f);
|
||||
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_factor, "Subsurface Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_scale, "Subsurface Scale", 0.05f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.subsurface_anisotropy, "Subsurface Anisotropy", 0.0f);
|
||||
|
||||
if (fmat.features.specular.enabled) {
|
||||
float spec = set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.specular_factor, "Specular IOR Level", 0.25f, 0.0f, 1.0f, 2.0f);
|
||||
mat->spec = spec;
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.specular_color, "Specular Tint", float3(1.0f));
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_anisotropy, "Anisotropic", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.specular_rotation, "Anisotropic Rotation", 0.0f);
|
||||
}
|
||||
|
||||
if (ELEM(fmat.shader_type,
|
||||
UFBX_SHADER_OSL_STANDARD_SURFACE,
|
||||
UFBX_SHADER_ARNOLD_STANDARD_SURFACE,
|
||||
UFBX_SHADER_3DS_MAX_PHYSICAL_MATERIAL,
|
||||
UFBX_SHADER_3DS_MAX_PBR_METAL_ROUGH,
|
||||
UFBX_SHADER_3DS_MAX_PBR_SPEC_GLOSS,
|
||||
UFBX_SHADER_GLTF_MATERIAL,
|
||||
UFBX_SHADER_BLENDER_PHONG) &&
|
||||
fmat.features.transmission.enabled)
|
||||
{
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.transmission_factor, "Transmission Weight", 0.0f);
|
||||
}
|
||||
|
||||
if (fmat.features.coat.enabled) {
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_factor, "Coat Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_roughness, "Coat Roughness", 0.03f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.coat_ior, "Coat IOR", 1.5f, 1.0f, 4.0f);
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.coat_color, "Coat Tint", float3(1.0f));
|
||||
}
|
||||
|
||||
if (fmat.features.sheen.enabled) {
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.sheen_factor, "Sheen Weight", 0.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.sheen_roughness, "Sheen Roughness", 0.5f);
|
||||
set_bsdf_color_param(bsdf, fmat.pbr.sheen_color, "Sheen Tint", float3(1.0f));
|
||||
}
|
||||
|
||||
set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.emission_factor, "Emission Strength", 0.0f, 0.0f, 1000000.0f);
|
||||
set_bsdf_color_param(bsdf,
|
||||
fmat.pbr.emission_color,
|
||||
"Emission Color",
|
||||
float3(0.0f),
|
||||
float3(0.0f),
|
||||
float3(1000000.0f));
|
||||
|
||||
set_bsdf_float_param(
|
||||
bsdf, fmat.pbr.thin_film_thickness, "Thin Film Thickness", 0.0f, 0.0f, 100000.0f);
|
||||
set_bsdf_float_param(bsdf, fmat.pbr.thin_film_ior, "Thin Film IOR", 1.33f, 1.0f, 1000.0f);
|
||||
}
|
||||
|
||||
static Image *create_placeholder_image(Main *bmain, const std::string &path)
|
||||
{
|
||||
const float color[4] = {0, 0, 0, 1};
|
||||
const char *name = BLI_path_basename(path.c_str());
|
||||
Image *image = BKE_image_add_generated(
|
||||
bmain, 1, 1, name, 24, false, IMA_GENTYPE_BLANK, color, false, false, false);
|
||||
STRNCPY(image->filepath, path.c_str());
|
||||
|
||||
/* Ensure that we are not marked as a generated image and clear any buffers created so far. */
|
||||
image->source = IMA_SRC_FILE;
|
||||
image->type = IMA_TYPE_IMAGE;
|
||||
BKE_image_free_buffers(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
static Image *load_texture_image(Main *bmain, const std::string &file_dir, const ufbx_texture &tex)
|
||||
{
|
||||
/* Check with filename directly. */
|
||||
Image *image = BKE_image_load_exists(bmain, tex.filename.data);
|
||||
/* Try loading as a relative path. */
|
||||
if (image == nullptr) {
|
||||
std::string path = file_dir + "/" + tex.filename.data;
|
||||
image = BKE_image_load_exists(bmain, path.c_str());
|
||||
}
|
||||
/* Try loading with absolute path from FBX. */
|
||||
if (image == nullptr) {
|
||||
image = BKE_image_load_exists(bmain, tex.absolute_filename.data);
|
||||
}
|
||||
|
||||
/* If still not found, try taking progressively longer parts of the absolute path,
|
||||
* as relative to the file. */
|
||||
if (image == nullptr) {
|
||||
size_t pos = tex.absolute_filename.length;
|
||||
do {
|
||||
const char *parent_path = BLI_path_parent_dir_end(tex.absolute_filename.data, pos);
|
||||
if (parent_path == nullptr) {
|
||||
break;
|
||||
}
|
||||
char path[FILE_MAX];
|
||||
BLI_path_join(path, sizeof(path), file_dir.c_str(), parent_path);
|
||||
BLI_path_normalize(path);
|
||||
image = BKE_image_load_exists(bmain, path);
|
||||
pos = parent_path - tex.absolute_filename.data;
|
||||
} while (image == nullptr);
|
||||
}
|
||||
|
||||
/* Create dummy/placeholder image. */
|
||||
if (image == nullptr) {
|
||||
image = create_placeholder_image(bmain, tex.filename.data);
|
||||
}
|
||||
|
||||
/* Use embedded data for this image, if we haven't done that yet. */
|
||||
if (tex.content.size > 0 && (image == nullptr || !BKE_image_has_packedfile(image))) {
|
||||
BKE_image_free_buffers(image); /* Free cached placeholder images. */
|
||||
char *data_dup = MEM_new_array_uninitialized<char>(tex.content.size, __func__);
|
||||
memcpy(data_dup, tex.content.data, tex.content.size);
|
||||
BKE_image_packfiles_from_mem(nullptr, image, data_dup, tex.content.size);
|
||||
|
||||
/* Make sure the image is not marked as "generated". */
|
||||
image->source = IMA_SRC_FILE;
|
||||
image->type = IMA_TYPE_IMAGE;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
struct FbxPbrTextureToSocket {
|
||||
ufbx_material_pbr_map slot;
|
||||
const char *socket;
|
||||
};
|
||||
static const FbxPbrTextureToSocket fbx_pbr_to_socket[] = {
|
||||
{UFBX_MATERIAL_PBR_BASE_COLOR, "Base Color"},
|
||||
{UFBX_MATERIAL_PBR_ROUGHNESS, "Roughness"},
|
||||
{UFBX_MATERIAL_PBR_METALNESS, "Metallic"},
|
||||
{UFBX_MATERIAL_PBR_DIFFUSE_ROUGHNESS, "Diffuse Roughness"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_FACTOR, "Specular IOR Level"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_COLOR, "Specular Tint"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_IOR, "IOR"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_ANISOTROPY, "Anisotropic"},
|
||||
{UFBX_MATERIAL_PBR_SPECULAR_ROTATION, "Anisotropic Rotation"},
|
||||
{UFBX_MATERIAL_PBR_TRANSMISSION_FACTOR, "Transmission Weight"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_FACTOR, "Subsurface Weight"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_SCALE, "Subsurface Scale"},
|
||||
{UFBX_MATERIAL_PBR_SUBSURFACE_ANISOTROPY, "Subsurface Anisotropy"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_FACTOR, "Sheen Weight"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_COLOR, "Sheen Tint"},
|
||||
{UFBX_MATERIAL_PBR_SHEEN_ROUGHNESS, "Sheen Roughness"},
|
||||
{UFBX_MATERIAL_PBR_COAT_FACTOR, "Coat Weight"},
|
||||
{UFBX_MATERIAL_PBR_COAT_COLOR, "Coat Tint"},
|
||||
{UFBX_MATERIAL_PBR_COAT_ROUGHNESS, "Coat Roughness"},
|
||||
{UFBX_MATERIAL_PBR_COAT_IOR, "Coat IOR"},
|
||||
{UFBX_MATERIAL_PBR_COAT_NORMAL, "Coat Normal"},
|
||||
{UFBX_MATERIAL_PBR_THIN_FILM_THICKNESS, "Thin Film Thickness"},
|
||||
{UFBX_MATERIAL_PBR_THIN_FILM_IOR, "Thin Film IOR"},
|
||||
{UFBX_MATERIAL_PBR_EMISSION_FACTOR, "Emission Strength"},
|
||||
{UFBX_MATERIAL_PBR_EMISSION_COLOR, "Emission Color"},
|
||||
{UFBX_MATERIAL_PBR_OPACITY, "Alpha"},
|
||||
{UFBX_MATERIAL_PBR_NORMAL_MAP, "Normal"},
|
||||
{UFBX_MATERIAL_PBR_TANGENT_MAP, "Tangent"},
|
||||
};
|
||||
|
||||
struct FbxStdTextureToSocket {
|
||||
ufbx_material_fbx_map slot;
|
||||
const char *socket;
|
||||
};
|
||||
static const FbxStdTextureToSocket fbx_std_to_socket[] = {
|
||||
{UFBX_MATERIAL_FBX_TRANSPARENCY_FACTOR, "Alpha"},
|
||||
{UFBX_MATERIAL_FBX_TRANSPARENCY_COLOR, "Alpha"},
|
||||
{UFBX_MATERIAL_FBX_BUMP, "Normal"},
|
||||
};
|
||||
|
||||
static void add_image_texture(Main *bmain,
|
||||
const std::string &file_dir,
|
||||
bNodeTree *ntree,
|
||||
bNode *bsdf,
|
||||
const ufbx_material &fmat,
|
||||
const ufbx_texture *ftex,
|
||||
const char *socket_name,
|
||||
float node_locy,
|
||||
Set<StringRefNull> &done_bsdf_inputs)
|
||||
{
|
||||
Image *image = load_texture_image(bmain, file_dir, *ftex);
|
||||
BLI_assert(image != nullptr);
|
||||
|
||||
/* Set "non-color" color space for all "data" textures. */
|
||||
if (!STR_ELEM(
|
||||
socket_name, "Base Color", "Specular Tint", "Sheen Tint", "Coat Tint", "Emission Color"))
|
||||
{
|
||||
STRNCPY_UTF8(image->colorspace_settings.name,
|
||||
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_DATA));
|
||||
}
|
||||
|
||||
/* Add texture node and any UV transformations if needed. */
|
||||
bNode *image_node = add_node(ntree, SH_NODE_TEX_IMAGE, node_locx_image, node_locy);
|
||||
BLI_assert(image_node);
|
||||
image_node->id = &image->id;
|
||||
NodeTexImage *tex_image = static_cast<NodeTexImage *>(image_node->storage);
|
||||
|
||||
/* Wrap mode. */
|
||||
tex_image->extension = SHD_IMAGE_EXTENSION_REPEAT;
|
||||
if (ftex->wrap_u == UFBX_WRAP_CLAMP || ftex->wrap_v == UFBX_WRAP_CLAMP) {
|
||||
tex_image->extension = SHD_IMAGE_EXTENSION_EXTEND;
|
||||
}
|
||||
|
||||
/* UV transform. */
|
||||
if (ftex->has_uv_transform) {
|
||||
/* TODO: which UV set to use. */
|
||||
bNode *uvmap = add_node(ntree, SH_NODE_UVMAP, node_locx_texcoord, node_locy);
|
||||
bNode *mapping = add_node(ntree, SH_NODE_MAPPING, node_locx_mapping, node_locy);
|
||||
mapping->custom1 = TEXMAP_TYPE_TEXTURE;
|
||||
set_socket_vector("Location",
|
||||
ftex->uv_transform.translation.x,
|
||||
ftex->uv_transform.translation.y,
|
||||
ftex->uv_transform.translation.z,
|
||||
mapping);
|
||||
ufbx_vec3 rot = ufbx_quat_to_euler(ftex->uv_transform.rotation, UFBX_ROTATION_ORDER_XYZ);
|
||||
set_socket_vector("Rotation", -rot.x, -rot.y, -rot.z, mapping);
|
||||
set_socket_vector("Scale",
|
||||
1.0f / ftex->uv_transform.scale.x,
|
||||
1.0f / ftex->uv_transform.scale.y,
|
||||
1.0f / ftex->uv_transform.scale.z,
|
||||
mapping);
|
||||
|
||||
link_sockets(ntree, uvmap, "UV", mapping, "Vector");
|
||||
link_sockets(ntree, mapping, "Vector", image_node, "Vector");
|
||||
}
|
||||
|
||||
done_bsdf_inputs.add(socket_name);
|
||||
if (STREQ(socket_name, "Normal")) {
|
||||
bNode *normal_node = add_node(ntree, SH_NODE_NORMAL_MAP, node_locx_normalmap, node_locy);
|
||||
link_sockets(ntree, image_node, "Color", normal_node, "Color");
|
||||
link_sockets(ntree, normal_node, "Normal", bsdf, "Normal");
|
||||
|
||||
/* Normal strength: Blender exports it as BumpFactor in FBX built-in properties. */
|
||||
float normal_strength = 1.0f;
|
||||
if (fmat.fbx.bump_factor.has_value) {
|
||||
normal_strength = fmat.fbx.bump_factor.value_real;
|
||||
}
|
||||
set_socket_float("Strength", normal_strength, normal_node);
|
||||
}
|
||||
else {
|
||||
link_sockets(ntree, image_node, "Color", bsdf, socket_name);
|
||||
|
||||
if (STREQ(socket_name, "Base Color") && !done_bsdf_inputs.contains("Alpha")) {
|
||||
/* Link base color alpha (if we have one) to output alpha. */
|
||||
void *lock;
|
||||
ImBuf *ibuf = BKE_image_acquire_ibuf(image, nullptr, &lock);
|
||||
bool has_alpha = ibuf != nullptr && ibuf->can_contain_alpha();
|
||||
BKE_image_release_ibuf(image, ibuf, lock);
|
||||
|
||||
if (has_alpha) {
|
||||
link_sockets(ntree, image_node, "Alpha", bsdf, "Alpha");
|
||||
done_bsdf_inputs.add("Alpha");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void add_image_textures(Main *bmain,
|
||||
const std::string &file_dir,
|
||||
bNodeTree *ntree,
|
||||
bNode *bsdf,
|
||||
const ufbx_material &fmat)
|
||||
{
|
||||
float node_locy = node_locy_top;
|
||||
Set<StringRefNull> done_bsdf_inputs;
|
||||
|
||||
/* We primarily use images from "PBR" FBX mapping. */
|
||||
for (const FbxPbrTextureToSocket &entry : fbx_pbr_to_socket) {
|
||||
BLI_assert(entry.socket != nullptr);
|
||||
if (done_bsdf_inputs.contains(entry.socket)) {
|
||||
continue; /* Already connected. */
|
||||
}
|
||||
|
||||
const ufbx_texture *ftex = fmat.pbr.maps[entry.slot].texture;
|
||||
if (ftex == nullptr || !fmat.pbr.maps[entry.slot].texture_enabled) {
|
||||
/* No texture used for this slot. */
|
||||
continue;
|
||||
}
|
||||
|
||||
add_image_texture(
|
||||
bmain, file_dir, ntree, bsdf, fmat, ftex, entry.socket, node_locy, done_bsdf_inputs);
|
||||
node_locy -= node_locy_step;
|
||||
}
|
||||
|
||||
/* But also support several from the legacy/standard "FBX" material model,
|
||||
* mostly to match behavior of python importer. */
|
||||
for (const FbxStdTextureToSocket &entry : fbx_std_to_socket) {
|
||||
BLI_assert(entry.socket != nullptr);
|
||||
if (done_bsdf_inputs.contains(entry.socket)) {
|
||||
continue; /* Already connected. */
|
||||
}
|
||||
|
||||
const ufbx_texture *ftex = fmat.fbx.maps[entry.slot].texture;
|
||||
if (ftex == nullptr || !fmat.fbx.maps[entry.slot].texture_enabled) {
|
||||
/* No texture used for this slot. */
|
||||
continue;
|
||||
}
|
||||
|
||||
add_image_texture(
|
||||
bmain, file_dir, ntree, bsdf, fmat, ftex, entry.socket, node_locy, done_bsdf_inputs);
|
||||
node_locy -= node_locy_step;
|
||||
}
|
||||
}
|
||||
|
||||
Material *import_material(Main *bmain, const std::string &base_dir, const ufbx_material &fmat)
|
||||
{
|
||||
Material *mat = BKE_material_add(bmain, fmat.name.data);
|
||||
id_us_min(&mat->id);
|
||||
|
||||
bNodeTree *ntree = mat->nodetree;
|
||||
bNode *bsdf = add_node(ntree, SH_NODE_BSDF_PRINCIPLED, node_locx_bsdf, node_locy_top);
|
||||
bNode *output = add_node(ntree, SH_NODE_OUTPUT_MATERIAL, node_locx_output, node_locy_top);
|
||||
set_bsdf_socket_values(bsdf, mat, fmat);
|
||||
add_image_textures(bmain, base_dir, ntree, bsdf, fmat);
|
||||
link_sockets(ntree, bsdf, "BSDF", output, "Surface");
|
||||
bke::node_set_active(*ntree, *output);
|
||||
|
||||
mat->nodetree = ntree;
|
||||
|
||||
BKE_ntree_update_after_single_tree_change(*bmain, *mat->nodetree);
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,22 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
struct ufbx_material;
|
||||
namespace blender {
|
||||
|
||||
struct Main;
|
||||
struct Material;
|
||||
namespace io::fbx {
|
||||
|
||||
Material *import_material(Main *bmain, const std::string &base_dir, const ufbx_material &fmat);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
663
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_mesh.cc
Normal file
663
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_mesh.cc
Normal file
@@ -0,0 +1,663 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_attribute.hh"
|
||||
#include "BKE_deform.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_object_deform.h"
|
||||
|
||||
#include "BLI_color_types.hh"
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_math_color.h"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
#include "BLI_task.hh"
|
||||
#include "BLI_vector_set.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_meshdata_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "IO_fbx.hh"
|
||||
#include "IO_validate.hh"
|
||||
|
||||
#include "fbx_import_mesh.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
static CLG_LogRef LOG = {"io.fbx"};
|
||||
|
||||
static constexpr const char *temp_custom_normals_name = "fbx_temp_custom_normals";
|
||||
|
||||
static bool is_skin_deformer_usable(const ufbx_mesh *mesh, const ufbx_skin_deformer *skin)
|
||||
{
|
||||
return mesh != nullptr && skin != nullptr && skin->clusters.count > 0 &&
|
||||
mesh->num_vertices > 0 && skin->vertices.count == mesh->num_vertices;
|
||||
}
|
||||
|
||||
static void import_vertex_positions(const ufbx_mesh *fmesh, Mesh *mesh)
|
||||
{
|
||||
MutableSpan<float3> positions = mesh->vert_positions_for_write();
|
||||
#if 0 // @TODO: "bake" skinned meshes
|
||||
if (skin != nullptr) {
|
||||
/* For a skinned mesh, transform the vertices into bind pose position, in local space. */
|
||||
const ufbx_matrix &geom_to_world = fmesh->instances[0]->geometry_to_world;
|
||||
ufbx_matrix world_to_geom = ufbx_matrix_invert(&geom_to_world);
|
||||
for (size_t i = 0; i < fmesh->vertex_position.values.count; i++) {
|
||||
ufbx_matrix skin_mat = ufbx_get_skin_vertex_matrix(skin, i, &geom_to_world);
|
||||
skin_mat = ufbx_matrix_mul(&world_to_geom, &skin_mat);
|
||||
ufbx_vec3 val = ufbx_transform_position(&skin_mat, fmesh->vertex_position.values[i]);
|
||||
positions[i] = float3(val.x, val.y, val.z);
|
||||
//@TODO: skin normals
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
BLI_assert(positions.size() == fmesh->vertex_position.values.count);
|
||||
for (const int64_t i : positions.index_range()) {
|
||||
ufbx_vec3 val = fmesh->vertex_position.values[i];
|
||||
positions[i] = float3(val.x, val.y, val.z);
|
||||
}
|
||||
}
|
||||
|
||||
static void import_faces(const ufbx_mesh *fmesh, Mesh *mesh)
|
||||
{
|
||||
MutableSpan<int> face_offsets = mesh->face_offsets_for_write();
|
||||
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
|
||||
BLI_assert((face_offsets.size() == fmesh->num_faces + 1) ||
|
||||
(face_offsets.is_empty() && fmesh->num_faces == 0));
|
||||
for (size_t face_idx = 0; face_idx < fmesh->num_faces; face_idx++) {
|
||||
//@TODO: skip < 3 vertex faces?
|
||||
const ufbx_face &fface = fmesh->faces[face_idx];
|
||||
face_offsets[face_idx] = fface.index_begin;
|
||||
for (uint32_t i = 0; i < fface.num_indices; i++) {
|
||||
const uint32_t corner_idx = fface.index_begin + i;
|
||||
corner_verts[corner_idx] = fmesh->vertex_indices[corner_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void import_face_material_indices(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (fmesh->face_material.count == fmesh->num_faces) {
|
||||
bke::SpanAttributeWriter<int> materials = attributes.lookup_or_add_for_write_only_span<int>(
|
||||
"material_index", bke::AttrDomain::Face);
|
||||
for (const int64_t i : materials.span.index_range()) {
|
||||
materials.span[i] = fmesh->face_material[i];
|
||||
}
|
||||
materials.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_face_smoothing(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (fmesh->face_smoothing.count > 0 && fmesh->face_smoothing.count == fmesh->num_faces) {
|
||||
bke::SpanAttributeWriter<bool> smooth = attributes.lookup_or_add_for_write_only_span<bool>(
|
||||
"sharp_face", bke::AttrDomain::Face);
|
||||
for (const int64_t i : smooth.span.index_range()) {
|
||||
smooth.span[i] = !fmesh->face_smoothing[i];
|
||||
}
|
||||
smooth.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_edges(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
MutableSpan<int2> edges = mesh->edges_for_write();
|
||||
BLI_assert(edges.size() == fmesh->num_edges);
|
||||
for (size_t i = 0; i < fmesh->num_edges; i++) {
|
||||
const ufbx_edge &fedge = fmesh->edges[i];
|
||||
const int va = fmesh->vertex_indices[fedge.a];
|
||||
const int vb = fmesh->vertex_indices[fedge.b];
|
||||
edges[i] = int2(va, vb);
|
||||
}
|
||||
|
||||
/* Edge attributes are written here in the same order as the FBX edges. Mesh validation
|
||||
* preserves edge attributes when removing degenerate edges or computing missing ones. */
|
||||
if (fmesh->edge_crease.count > 0 && fmesh->edge_crease.count == fmesh->num_edges) {
|
||||
bke::SpanAttributeWriter<float> creases = attributes.lookup_or_add_for_write_only_span<float>(
|
||||
"crease_edge", bke::AttrDomain::Edge);
|
||||
for (int64_t i = 0; i < fmesh->num_edges; i++) {
|
||||
/* Python fbx importer was squaring the incoming crease values. */
|
||||
creases.span[i] = sqrtf(fmesh->edge_crease[i]);
|
||||
}
|
||||
creases.finish();
|
||||
}
|
||||
|
||||
if (fmesh->edge_smoothing.count > 0 && fmesh->edge_smoothing.count == fmesh->num_edges) {
|
||||
bke::SpanAttributeWriter<bool> sharp = attributes.lookup_or_add_for_write_only_span<bool>(
|
||||
"sharp_edge", bke::AttrDomain::Edge);
|
||||
for (int64_t i = 0; i < fmesh->num_edges; i++) {
|
||||
sharp.span[i] = !fmesh->edge_smoothing[i];
|
||||
}
|
||||
sharp.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_uvs(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes,
|
||||
AttributeOwner attr_owner)
|
||||
{
|
||||
bool set_active_uv = true;
|
||||
for (const ufbx_uv_set &fuv_set : fmesh->uv_sets) {
|
||||
std::string attr_name = BKE_attribute_calc_unique_name(attr_owner, fuv_set.name.data);
|
||||
if (set_active_uv) {
|
||||
mesh->uv_maps_active_set(attr_name);
|
||||
mesh->uv_maps_default_set(attr_name);
|
||||
set_active_uv = false;
|
||||
}
|
||||
bke::SpanAttributeWriter<float2> uvs = attributes.lookup_or_add_for_write_only_span<float2>(
|
||||
attr_name, bke::AttrDomain::Corner);
|
||||
BLI_assert(fuv_set.vertex_uv.indices.count == uvs.span.size());
|
||||
for (const int64_t i : uvs.span.index_range()) {
|
||||
const int val_idx = fuv_set.vertex_uv.indices[i];
|
||||
const ufbx_vec2 &uv = fuv_set.vertex_uv.values[val_idx];
|
||||
uvs.span[i] = float2(uv.x, uv.y);
|
||||
}
|
||||
uvs.finish();
|
||||
}
|
||||
}
|
||||
|
||||
static void import_colors(const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
bke::MutableAttributeAccessor &attributes,
|
||||
AttributeOwner attr_owner,
|
||||
eFBXVertexColorMode color_mode)
|
||||
{
|
||||
std::string first_color_name;
|
||||
for (const ufbx_color_set &fcol_set : fmesh->color_sets) {
|
||||
std::string attr_name = BKE_attribute_calc_unique_name(attr_owner, fcol_set.name.data);
|
||||
if (first_color_name.empty()) {
|
||||
first_color_name = attr_name;
|
||||
}
|
||||
if (color_mode == eFBXVertexColorMode::sRGB) {
|
||||
/* sRGB colors, use 4 bytes per color. */
|
||||
bke::SpanAttributeWriter<ColorGeometry4b> cols =
|
||||
attributes.lookup_or_add_for_write_only_span<ColorGeometry4b>(attr_name,
|
||||
bke::AttrDomain::Corner);
|
||||
BLI_assert(fcol_set.vertex_color.indices.count == cols.span.size());
|
||||
for (const int64_t i : cols.span.index_range()) {
|
||||
const int val_idx = fcol_set.vertex_color.indices[i];
|
||||
const ufbx_vec4 &col = fcol_set.vertex_color.values[val_idx];
|
||||
/* Note: color values are expected to already be in sRGB space. */
|
||||
float4 fcol = float4(col.x, col.y, col.z, col.w);
|
||||
uchar4 bcol;
|
||||
rgba_float_to_uchar(bcol, fcol);
|
||||
cols.span[i] = ColorGeometry4b(bcol);
|
||||
}
|
||||
cols.finish();
|
||||
}
|
||||
else if (color_mode == eFBXVertexColorMode::Linear) {
|
||||
/* Linear colors, use 4 floats per color. */
|
||||
bke::SpanAttributeWriter<ColorGeometry4f> cols =
|
||||
attributes.lookup_or_add_for_write_only_span<ColorGeometry4f>(attr_name,
|
||||
bke::AttrDomain::Corner);
|
||||
BLI_assert(fcol_set.vertex_color.indices.count == cols.span.size());
|
||||
for (const int64_t i : cols.span.index_range()) {
|
||||
const int val_idx = fcol_set.vertex_color.indices[i];
|
||||
const ufbx_vec4 &col = fcol_set.vertex_color.values[val_idx];
|
||||
cols.span[i] = ColorGeometry4f(col.x, col.y, col.z, col.w);
|
||||
}
|
||||
cols.finish();
|
||||
}
|
||||
else {
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
}
|
||||
if (!first_color_name.empty()) {
|
||||
mesh->active_color_attribute = BLI_strdup(first_color_name.c_str());
|
||||
mesh->default_color_attribute = BLI_strdup(first_color_name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static bool import_normals_into_temp_attribute(const ufbx_mesh *fmesh,
|
||||
bke::MutableAttributeAccessor &attributes)
|
||||
{
|
||||
if (!fmesh->vertex_normal.exists) {
|
||||
return false;
|
||||
}
|
||||
bke::SpanAttributeWriter<float3> normals = attributes.lookup_or_add_for_write_only_span<float3>(
|
||||
temp_custom_normals_name, bke::AttrDomain::Corner);
|
||||
BLI_assert(fmesh->vertex_normal.indices.count == normals.span.size());
|
||||
for (const int64_t i : normals.span.index_range()) {
|
||||
const int val_idx = fmesh->vertex_normal.indices[i];
|
||||
const ufbx_vec3 &normal = fmesh->vertex_normal.values[val_idx];
|
||||
normals.span[i] = float3(normal.x, normal.y, normal.z);
|
||||
}
|
||||
normals.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
static VectorSet<std::string> get_skin_bone_name_set(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh)
|
||||
{
|
||||
VectorSet<std::string> name_set;
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
name_set.add(bone_name);
|
||||
}
|
||||
}
|
||||
return name_set;
|
||||
}
|
||||
|
||||
static void import_skin_vertex_groups(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh)
|
||||
{
|
||||
if (fmesh->skin_deformers.count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A single mesh can be skinned by several armatures, so we need to build bone (vertex group)
|
||||
* name set, taking all skin deformers into account. */
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
if (bone_set.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MutableSpan<MDeformVert> dverts = mesh->deform_verts_for_write();
|
||||
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string bone_name = mapping.node_to_name.lookup_default(cluster->bone_node, "");
|
||||
const int group_index = bone_set.index_of_try(bone_name);
|
||||
if (group_index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int64_t i = 0; i < cluster->num_weights; i++) {
|
||||
const int vertex = cluster->vertices[i];
|
||||
if (validate::index_in_range(vertex, dverts.size())) {
|
||||
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[vertex], group_index);
|
||||
dw->weight = cluster->weights[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool import_blend_shapes(Main &bmain,
|
||||
FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh)
|
||||
{
|
||||
Key *mesh_key = nullptr;
|
||||
for (const ufbx_blend_deformer *fdeformer : fmesh->blend_deformers) {
|
||||
for (const ufbx_blend_channel *fchan : fdeformer->channels) {
|
||||
/* In theory fbx supports multiple keyframes within one blend shape
|
||||
* channel; we only take the final target keyframe. */
|
||||
if (fchan->target_shape == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mesh_key == nullptr) {
|
||||
mesh_key = BKE_key_add(&bmain, &mesh->id);
|
||||
mesh_key->type = KEY_RELATIVE;
|
||||
mesh->key = mesh_key;
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_add(mesh_key, nullptr);
|
||||
BKE_keyblock_convert_from_mesh(mesh, mesh_key, kb);
|
||||
}
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_add(mesh_key, fchan->target_shape->name.data);
|
||||
kb->curval = fchan->weight;
|
||||
BKE_keyblock_convert_from_mesh(mesh, mesh_key, kb);
|
||||
if (!kb->data) {
|
||||
/* Nothing to do. This can happen if the mesh has no vertices. */
|
||||
continue;
|
||||
}
|
||||
float3 *kb_data = static_cast<float3 *>(kb->data);
|
||||
for (size_t i = 0; i < fchan->target_shape->num_offsets; i++) {
|
||||
const int idx = fchan->target_shape->offset_vertices[i];
|
||||
if (!validate::index_in_range(idx, mesh->verts_num)) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_vec3 &delta = fchan->target_shape->position_offsets[i];
|
||||
kb_data[idx] += float3(delta.x, delta.y, delta.z);
|
||||
}
|
||||
mapping.el_to_shape_key.add(&fchan->element, mesh_key);
|
||||
}
|
||||
}
|
||||
return mesh_key != nullptr;
|
||||
}
|
||||
|
||||
/* Handle Blender-specific "FullWeights" that for each blend shape also create
|
||||
* a weighted vertex group for itself. */
|
||||
static void import_blend_shape_full_weights(const FbxElementMapping &mapping,
|
||||
const ufbx_mesh *fmesh,
|
||||
Mesh *mesh,
|
||||
Object *obj)
|
||||
{
|
||||
for (const ufbx_blend_deformer *fdeformer : fmesh->blend_deformers) {
|
||||
for (const ufbx_blend_channel *fchan : fdeformer->channels) {
|
||||
Key *key = mapping.el_to_shape_key.lookup_default(&fchan->element, nullptr);
|
||||
if (fchan->target_shape == nullptr || key == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (fchan->target_shape->offset_weights.count != fchan->target_shape->num_offsets) {
|
||||
continue;
|
||||
}
|
||||
|
||||
KeyBlock *kb = BKE_keyblock_find_name(key, fchan->target_shape->name.data);
|
||||
if (kb == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Ignore cases where all weights are 1.0 (group has no effect),
|
||||
* and cases where any weights are outside of 0..1 range (apparently some files have
|
||||
* invalid negative weights and should be ignored). */
|
||||
bool all_one = true;
|
||||
bool all_unorm = true;
|
||||
for (ufbx_real w : fchan->target_shape->offset_weights) {
|
||||
if (w != 1.0) {
|
||||
all_one = false;
|
||||
}
|
||||
if (w < 0.0 || w > 1.0) {
|
||||
all_unorm = false;
|
||||
}
|
||||
}
|
||||
if (all_one || !all_unorm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int group_index = BKE_defgroup_name_index(&mesh->vertex_group_names, kb->name);
|
||||
if (group_index < 0) {
|
||||
BKE_object_defgroup_add_name(obj, kb->name);
|
||||
group_index = BKE_defgroup_name_index(&mesh->vertex_group_names, kb->name);
|
||||
if (group_index < 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
MutableSpan<MDeformVert> dverts = mesh->deform_verts_for_write();
|
||||
for (size_t i = 0; i < fchan->target_shape->num_offsets; i++) {
|
||||
const int idx = fchan->target_shape->offset_vertices[i];
|
||||
if (validate::index_in_range(idx, dverts.size())) {
|
||||
const float w = fchan->target_shape->offset_weights[i];
|
||||
MDeformWeight *dw = BKE_defvert_ensure_index(&dverts[idx], group_index);
|
||||
dw->weight = w;
|
||||
}
|
||||
}
|
||||
|
||||
STRNCPY_UTF8(kb->vgroup, kb->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void import_meshes(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms)
|
||||
{
|
||||
/* Create Mesh objects outside of Main, in parallel. */
|
||||
Vector<Mesh *> meshes(fbx.meshes.count);
|
||||
threading::parallel_for_each(IndexRange(fbx.meshes.count), [&](const int64_t index) {
|
||||
const ufbx_mesh *fmesh = fbx.meshes.data[index];
|
||||
if (fmesh->instances.count == 0) {
|
||||
meshes[index] = nullptr; /* Ignore if not used by any objects. */
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validate::size_fits_in_int(fmesh->num_vertices) ||
|
||||
!validate::size_fits_in_int(fmesh->num_edges) ||
|
||||
!validate::size_fits_in_int(fmesh->num_faces) ||
|
||||
!validate::size_fits_in_int(fmesh->num_indices))
|
||||
{
|
||||
CLOG_WARN(&LOG, "Mesh '%s' too large to import, exceeds max int size", fmesh->name.data);
|
||||
meshes[index] = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Create Mesh outside of main. */
|
||||
Mesh *mesh = BKE_mesh_new_nomain(
|
||||
fmesh->num_vertices, fmesh->num_edges, fmesh->num_faces, fmesh->num_indices);
|
||||
bke::MutableAttributeAccessor attributes = mesh->attributes_for_write();
|
||||
AttributeOwner attr_owner = AttributeOwner::from_id(&mesh->id);
|
||||
|
||||
import_vertex_positions(fmesh, mesh);
|
||||
import_faces(fmesh, mesh);
|
||||
import_face_material_indices(fmesh, attributes);
|
||||
import_face_smoothing(fmesh, attributes);
|
||||
import_edges(fmesh, mesh, attributes);
|
||||
import_uvs(fmesh, mesh, attributes, attr_owner);
|
||||
if (params.vertex_colors != eFBXVertexColorMode::None) {
|
||||
import_colors(fmesh, mesh, attributes, attr_owner, params.vertex_colors);
|
||||
}
|
||||
bool has_custom_normals = false;
|
||||
if (params.use_custom_normals) {
|
||||
/* Mesh validation below can alter the mesh, so we first write custom normals
|
||||
* into a temporary custom corner domain attribute, and then re-apply that
|
||||
* data as custom normals after the validation. */
|
||||
has_custom_normals = import_normals_into_temp_attribute(fmesh, attributes);
|
||||
}
|
||||
import_skin_vertex_groups(mapping, fmesh, mesh);
|
||||
|
||||
/* Add vertex groups to the object. */
|
||||
VectorSet<std::string> bone_set = get_skin_bone_name_set(mapping, fmesh);
|
||||
for (const std::string &name : bone_set) {
|
||||
bDeformGroup *defgroup = MEM_new<bDeformGroup>("bDeformGroup");
|
||||
StringRef(name).copy_utf8_truncated(defgroup->name);
|
||||
BLI_addtail(&mesh->vertex_group_names, defgroup);
|
||||
}
|
||||
|
||||
/* FBX files may not contain all edges, so missing edges must be added here.
|
||||
* Validation will do this, and otherwise calculate them explicitly. */
|
||||
if (params.validate_meshes) {
|
||||
const bool allow_missing_edges = true;
|
||||
#ifndef NDEBUG
|
||||
const bool verbose_validate = true;
|
||||
#else
|
||||
const bool verbose_validate = false;
|
||||
#endif
|
||||
bke::mesh_validate(*mesh, verbose_validate, allow_missing_edges);
|
||||
}
|
||||
else {
|
||||
bke::mesh_calc_edges(*mesh, true, false);
|
||||
}
|
||||
|
||||
if (has_custom_normals) {
|
||||
/* Actually set custom normals after the validation. */
|
||||
bke::SpanAttributeWriter<float3> normals =
|
||||
attributes.lookup_or_add_for_write_only_span<float3>(temp_custom_normals_name,
|
||||
bke::AttrDomain::Corner);
|
||||
bke::mesh_set_custom_normals(*mesh, normals.span);
|
||||
normals.finish();
|
||||
attributes.remove(temp_custom_normals_name);
|
||||
}
|
||||
|
||||
meshes[index] = mesh;
|
||||
});
|
||||
|
||||
/* Create final mesh objects in Main, serially. And do steps that need to be done on the final
|
||||
* objects. */
|
||||
for (int64_t index : meshes.index_range()) {
|
||||
Mesh *mesh = meshes[index];
|
||||
if (mesh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const ufbx_mesh *fmesh = fbx.meshes[index];
|
||||
BLI_assert(fmesh != nullptr);
|
||||
|
||||
Mesh *mesh_main = static_cast<Mesh *>(
|
||||
BKE_object_obdata_add_from_type(&bmain, OB_MESH, get_fbx_name(fmesh->name, "Mesh")));
|
||||
BKE_mesh_nomain_to_mesh(mesh, mesh_main, nullptr);
|
||||
meshes[index] = mesh_main;
|
||||
mesh = mesh_main;
|
||||
if (params.use_custom_props) {
|
||||
read_custom_properties(fmesh->props, mesh->id, params.props_enum_as_string);
|
||||
}
|
||||
|
||||
const bool any_shapes = import_blend_shapes(bmain, mapping, fmesh, mesh);
|
||||
|
||||
/* Create objects that use this mesh. */
|
||||
for (const ufbx_node *node : fmesh->instances) {
|
||||
std::string name;
|
||||
if (node->is_geometry_transform_helper) {
|
||||
/* Name geometry transform adjustment helpers with parent name and _GeomAdjust suffix. */
|
||||
name = get_fbx_name(node->parent->name) + std::string("_GeomAdjust");
|
||||
}
|
||||
else {
|
||||
name = get_fbx_name(node->name);
|
||||
}
|
||||
Object *obj = BKE_object_add_only_object(&bmain, OB_MESH, name.c_str());
|
||||
obj->data = id_cast<ID *>(mesh_main);
|
||||
if (!node->visible) {
|
||||
obj->visibility_flag |= OB_HIDE_VIEWPORT;
|
||||
}
|
||||
|
||||
if (any_shapes) {
|
||||
obj->shapenr = 1;
|
||||
}
|
||||
|
||||
bool matrix_already_set = false;
|
||||
|
||||
/* Skinned mesh. */
|
||||
if (fmesh->skin_deformers.count > 0) {
|
||||
/* Add armature modifiers for each skin deformer. */
|
||||
for (const ufbx_skin_deformer *skin : fmesh->skin_deformers) {
|
||||
if (!is_skin_deformer_usable(fmesh, skin)) {
|
||||
continue;
|
||||
}
|
||||
Object *arm_obj = nullptr;
|
||||
for (const ufbx_skin_cluster *cluster : skin->clusters) {
|
||||
if (cluster->num_weights == 0) {
|
||||
continue;
|
||||
}
|
||||
arm_obj = mapping.bone_to_armature.lookup_default(cluster->bone_node, nullptr);
|
||||
if (arm_obj != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Add armature modifier. */
|
||||
if (arm_obj != nullptr) {
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Armature);
|
||||
STRNCPY_UTF8(md->name, BKE_id_name(arm_obj->id));
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
ArmatureModifierData *ad = reinterpret_cast<ArmatureModifierData *>(md);
|
||||
ad->object = arm_obj;
|
||||
|
||||
if (!matrix_already_set) {
|
||||
matrix_already_set = true;
|
||||
obj->parent = arm_obj;
|
||||
|
||||
/* We are setting mesh parent to the armature, so set the matrix that is
|
||||
* armature-local. Note that the matrix needs to be relative to the FBX
|
||||
* node matrix (not the root bone pose matrix). */
|
||||
ufbx_matrix world_to_arm = mapping.armature_world_to_arm_node_matrix.lookup_default(
|
||||
arm_obj, ufbx_identity_matrix);
|
||||
ufbx_matrix world_to_arm_pose = mapping.armature_world_to_arm_pose_matrix
|
||||
.lookup_default(arm_obj, ufbx_identity_matrix);
|
||||
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(&world_to_arm, &node->geometry_to_world);
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
|
||||
/* Setup parent inverse matrix of the mesh, to account for the mesh possibly being in
|
||||
* different bind pose than what the node is at. */
|
||||
ufbx_matrix mtx_inv = ufbx_matrix_invert(&mtx);
|
||||
ufbx_matrix mtx_world = mapping.get_node_bind_matrix(node);
|
||||
ufbx_matrix mtx_parent_inverse = ufbx_matrix_mul(&mtx_world, &mtx_inv);
|
||||
mtx_parent_inverse = ufbx_matrix_mul(&world_to_arm_pose, &mtx_parent_inverse);
|
||||
matrix_to_m44(mtx_parent_inverse, obj->parentinv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (any_shapes) {
|
||||
import_blend_shape_full_weights(mapping, fmesh, mesh, obj);
|
||||
}
|
||||
|
||||
/* Assign materials. */
|
||||
if (fmesh->materials.count > 0 && node->materials.count == fmesh->materials.count) {
|
||||
int mat_index = 0;
|
||||
for (size_t mi = 0; mi < fmesh->materials.count; mi++) {
|
||||
const ufbx_material *mesh_fmat = fmesh->materials[mi];
|
||||
const ufbx_material *node_fmat = node->materials[mi];
|
||||
Material *mesh_mat = mapping.mat_to_material.lookup_default(mesh_fmat, nullptr);
|
||||
Material *node_mat = mapping.mat_to_material.lookup_default(node_fmat, nullptr);
|
||||
if (mesh_mat != nullptr) {
|
||||
mat_index++;
|
||||
/* Assign material to the data block. */
|
||||
BKE_object_material_assign_single_obdata(&bmain, obj, mesh_mat, mat_index);
|
||||
|
||||
/* If object material is different, assign that to object. */
|
||||
if (!ELEM(node_mat, nullptr, mesh_mat)) {
|
||||
BKE_object_material_assign(&bmain, obj, node_mat, mat_index, BKE_MAT_ASSIGN_OBJECT);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mat_index > 0) {
|
||||
obj->actcol = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Subdivision. */
|
||||
if (params.import_subdivision &&
|
||||
fmesh->subdivision_display_mode != UFBX_SUBDIVISION_DISPLAY_DISABLED &&
|
||||
(fmesh->subdivision_preview_levels > 0 || fmesh->subdivision_render_levels > 0))
|
||||
{
|
||||
ModifierData *md = BKE_modifier_new(eModifierType_Subsurf);
|
||||
BLI_addtail(&obj->modifiers, md);
|
||||
BKE_modifiers_persistent_uid_init(*obj, *md);
|
||||
|
||||
SubsurfModifierData *ssd = reinterpret_cast<SubsurfModifierData *>(md);
|
||||
ssd->subdivType = SUBSURF_TYPE_CATMULL_CLARK;
|
||||
ssd->levels = fmesh->subdivision_preview_levels;
|
||||
ssd->renderLevels = fmesh->subdivision_render_levels;
|
||||
ssd->boundary_smooth = fmesh->subdivision_boundary ==
|
||||
UFBX_SUBDIVISION_BOUNDARY_SHARP_CORNERS ?
|
||||
SUBSURF_BOUNDARY_SMOOTH_PRESERVE_CORNERS :
|
||||
SUBSURF_BOUNDARY_SMOOTH_ALL;
|
||||
}
|
||||
|
||||
if (params.use_custom_props) {
|
||||
read_custom_properties(node->props, obj->id, params.props_enum_as_string);
|
||||
}
|
||||
if (!matrix_already_set) {
|
||||
node_matrix_to_obj(node, obj, mapping);
|
||||
}
|
||||
mapping.el_to_object.add(&node->element, obj);
|
||||
mapping.imported_objects.add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FBXImportParams;
|
||||
struct Main;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
void import_meshes(Main &bmain,
|
||||
const ufbx_scene &fbx,
|
||||
FbxElementMapping &mapping,
|
||||
const FBXImportParams ¶ms);
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
309
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.cc
Normal file
309
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.cc
Normal file
@@ -0,0 +1,309 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#include "BKE_idprop.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_object_types.hh"
|
||||
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "fbx_import_util.hh"
|
||||
|
||||
namespace blender::io::fbx {
|
||||
|
||||
const char *get_fbx_name(const ufbx_string &name, const char *def)
|
||||
{
|
||||
return name.length > 0 ? name.data : def;
|
||||
}
|
||||
|
||||
void matrix_to_m44(const ufbx_matrix &src, float dst[4][4])
|
||||
{
|
||||
dst[0][0] = src.m00;
|
||||
dst[1][0] = src.m01;
|
||||
dst[2][0] = src.m02;
|
||||
dst[3][0] = src.m03;
|
||||
dst[0][1] = src.m10;
|
||||
dst[1][1] = src.m11;
|
||||
dst[2][1] = src.m12;
|
||||
dst[3][1] = src.m13;
|
||||
dst[0][2] = src.m20;
|
||||
dst[1][2] = src.m21;
|
||||
dst[2][2] = src.m22;
|
||||
dst[3][2] = src.m23;
|
||||
dst[0][3] = 0.0f;
|
||||
dst[1][3] = 0.0f;
|
||||
dst[2][3] = 0.0f;
|
||||
dst[3][3] = 1.0f;
|
||||
}
|
||||
|
||||
ufbx_matrix calc_bone_pose_matrix(const ufbx_transform &local_xform,
|
||||
const ufbx_node &node,
|
||||
const ufbx_matrix &local_bind_inv_matrix)
|
||||
{
|
||||
ufbx_transform xform = local_xform;
|
||||
|
||||
/* For bones that have "ignore parent scale" on them, ufbx helpfully applies global scale to
|
||||
* the evaluated transform. However we really need to get local transform without global
|
||||
* scale, so undo that. */
|
||||
if (node.adjust_post_scale != 1.0) {
|
||||
xform.scale.x /= node.adjust_post_scale;
|
||||
xform.scale.y /= node.adjust_post_scale;
|
||||
xform.scale.z /= node.adjust_post_scale;
|
||||
}
|
||||
|
||||
/* Transformed to the bind transform in joint-local space. */
|
||||
ufbx_matrix matrix = ufbx_transform_to_matrix(&xform);
|
||||
matrix = ufbx_matrix_mul(&local_bind_inv_matrix, &matrix);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
void ufbx_matrix_to_obj(const ufbx_matrix &mtx, Object *obj)
|
||||
{
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file, "init NODE %s self.matrix:\n", obj->id.name + 2);
|
||||
print_matrix(mtx);
|
||||
#endif
|
||||
|
||||
float obmat[4][4];
|
||||
matrix_to_m44(mtx, obmat);
|
||||
BKE_object_apply_mat4(obj, obmat, true, false);
|
||||
BKE_object_to_mat4(obj, obj->runtime->object_to_world.ptr());
|
||||
}
|
||||
|
||||
void node_matrix_to_obj(const ufbx_node *node, Object *obj, const FbxElementMapping &mapping)
|
||||
{
|
||||
ufbx_matrix mtx = ufbx_matrix_mul(node->node_depth < 2 ? &node->node_to_world :
|
||||
&node->node_to_parent,
|
||||
&node->geometry_to_node);
|
||||
|
||||
/* Handle case of an object parented to a bone: need to set
|
||||
* bone as parent, and make transform be at the end of the bone. */
|
||||
const ufbx_node *parbone = node->parent;
|
||||
if (obj->parent == nullptr && parbone && mapping.node_is_blender_bone.contains(parbone)) {
|
||||
Object *arm = mapping.bone_to_armature.lookup_default(parbone, nullptr);
|
||||
if (arm != nullptr) {
|
||||
ufbx_matrix offset_mtx = ufbx_identity_matrix;
|
||||
offset_mtx.cols[3].y = -mapping.bone_to_length.lookup_default(parbone, 0.0);
|
||||
if (mapping.node_is_blender_bone.contains(node)) {
|
||||
/* The node itself is a "fake bone", in which case parent it to the matching
|
||||
* fake bone, and matrix is just what puts transform at the bone tail. */
|
||||
parbone = node;
|
||||
mtx = offset_mtx;
|
||||
}
|
||||
else {
|
||||
mtx = ufbx_matrix_mul(&offset_mtx, &mtx);
|
||||
}
|
||||
|
||||
obj->parent = arm;
|
||||
obj->partype = PARBONE;
|
||||
STRNCPY_UTF8(obj->parsubstr, mapping.node_to_name.lookup_default(parbone, "").c_str());
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
fprintf(g_debug_file,
|
||||
"parent CHILD %s to ARM %s BONE %s bone_child_mtx:\n",
|
||||
node->name.data,
|
||||
arm->id.name + 2,
|
||||
parbone->name.data);
|
||||
print_matrix(offset_mtx);
|
||||
fprintf(g_debug_file, "- child matrix:\n");
|
||||
print_matrix(mtx);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ufbx_matrix_to_obj(mtx, obj);
|
||||
}
|
||||
|
||||
static void read_ufbx_property(const ufbx_prop &prop, IDProperty *idgroup, bool enums_as_strings)
|
||||
{
|
||||
IDProperty *idprop = nullptr;
|
||||
IDPropertyTemplate val = {0};
|
||||
//@TODO: validate_blend_names on the property name
|
||||
const char *name = prop.name.data;
|
||||
|
||||
switch (prop.type) {
|
||||
case UFBX_PROP_BOOLEAN:
|
||||
val.i = prop.value_int;
|
||||
idprop = IDP_New(IDP_BOOLEAN, &val, name);
|
||||
break;
|
||||
case UFBX_PROP_INTEGER: {
|
||||
bool parsed_as_enum = false;
|
||||
if (enums_as_strings && (prop.flags & UFBX_PROP_FLAG_VALUE_STR) &&
|
||||
(prop.value_str.length > 0))
|
||||
{
|
||||
/* "Enum" property with integer value, and enum names as `~` separated string. */
|
||||
const char *tilde = prop.value_str.data;
|
||||
int enum_index = -1;
|
||||
while (true) {
|
||||
const char *tilde_start = tilde;
|
||||
tilde = BLI_strchr_or_end(tilde_start, '~');
|
||||
if (tilde == tilde_start) {
|
||||
break;
|
||||
}
|
||||
/* We have an enum value string. */
|
||||
enum_index++;
|
||||
if (enum_index == prop.value_int) {
|
||||
/* Found the needed one. */
|
||||
parsed_as_enum = true;
|
||||
std::string str_val = StringRef(tilde_start, tilde).trim();
|
||||
val.string.str = str_val.c_str();
|
||||
val.string.len = str_val.size() + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
idprop = IDP_New(IDP_STRING, &val, name);
|
||||
break;
|
||||
}
|
||||
if (tilde[0] == 0) {
|
||||
break;
|
||||
}
|
||||
tilde++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed_as_enum) {
|
||||
val.i = prop.value_int;
|
||||
idprop = IDP_New(IDP_INT, &val, name);
|
||||
}
|
||||
|
||||
} break;
|
||||
case UFBX_PROP_NUMBER:
|
||||
val.d = prop.value_real;
|
||||
idprop = IDP_New(IDP_DOUBLE, &val, name);
|
||||
break;
|
||||
case UFBX_PROP_STRING:
|
||||
if (STREQ(name, "UDP3DSMAX")) {
|
||||
/* 3dsmax user properties are coming as `UDP3DSMAX` property. Parse them
|
||||
* as multi-line text, splitting across `=` within each line. */
|
||||
const char *line = prop.value_str.data;
|
||||
while (true) {
|
||||
const char *line_start = line;
|
||||
line = BLI_strchr_or_end(line_start, '\n');
|
||||
if (line == line_start) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* We have a line, split it by '=' and trim name/value. */
|
||||
const char *eq_pos = line_start;
|
||||
while (eq_pos != line && eq_pos[0] != '=') {
|
||||
eq_pos++;
|
||||
}
|
||||
if (eq_pos[0] == '=') {
|
||||
std::string str_name = StringRef(line_start, eq_pos).trim();
|
||||
std::string str_val = StringRef(eq_pos + 1, line).trim();
|
||||
//@TODO validate_blend_names on str_name
|
||||
val.string.str = str_val.c_str();
|
||||
val.string.len = str_val.size() + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
IDProperty *str_prop = IDP_New(IDP_STRING, &val, str_name.c_str());
|
||||
IDP_AddToGroup(idgroup, str_prop);
|
||||
}
|
||||
|
||||
if (line[0] == 0) {
|
||||
break;
|
||||
}
|
||||
line++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
val.string.str = prop.value_str.data;
|
||||
val.string.len = prop.value_str.length + 1; /* .len needs to include null terminator. */
|
||||
val.string.subtype = IDP_STRING_SUB_UTF8;
|
||||
idprop = IDP_New(IDP_STRING, &val, name);
|
||||
}
|
||||
break;
|
||||
case UFBX_PROP_VECTOR:
|
||||
case UFBX_PROP_COLOR:
|
||||
val.array.len = 3;
|
||||
val.array.type = IDP_DOUBLE;
|
||||
idprop = IDP_New(IDP_ARRAY, &val, name);
|
||||
{
|
||||
double *dst = static_cast<double *>(idprop->data.pointer);
|
||||
dst[0] = prop.value_vec3.x;
|
||||
dst[1] = prop.value_vec3.y;
|
||||
dst[2] = prop.value_vec3.z;
|
||||
}
|
||||
break;
|
||||
case UFBX_PROP_COLOR_WITH_ALPHA:
|
||||
val.array.len = 4;
|
||||
val.array.type = IDP_DOUBLE;
|
||||
idprop = IDP_New(IDP_ARRAY, &val, name);
|
||||
{
|
||||
double *dst = static_cast<double *>(idprop->data.pointer);
|
||||
dst[0] = prop.value_vec4.x;
|
||||
dst[1] = prop.value_vec4.y;
|
||||
dst[2] = prop.value_vec4.z;
|
||||
dst[3] = prop.value_vec4.z;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (idprop != nullptr) {
|
||||
IDP_AddToGroup(idgroup, idprop);
|
||||
}
|
||||
}
|
||||
|
||||
void read_custom_properties(const ufbx_props &props, ID &id, bool enums_as_strings)
|
||||
{
|
||||
for (const ufbx_prop &prop : props.props) {
|
||||
if ((prop.flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) {
|
||||
continue;
|
||||
}
|
||||
IDProperty *idgroup = IDP_EnsureProperties(&id);
|
||||
read_ufbx_property(prop, idgroup, enums_as_strings);
|
||||
}
|
||||
}
|
||||
|
||||
static IDProperty *pchan_EnsureProperties(bPoseChannel &pchan)
|
||||
{
|
||||
if (pchan.prop == nullptr) {
|
||||
pchan.prop = bke::idprop::create_group("").release();
|
||||
}
|
||||
return pchan.prop;
|
||||
}
|
||||
|
||||
void read_custom_properties(const ufbx_props &props, bPoseChannel &pchan, bool enums_as_strings)
|
||||
{
|
||||
for (const ufbx_prop &prop : props.props) {
|
||||
if ((prop.flags & UFBX_PROP_FLAG_USER_DEFINED) == 0) {
|
||||
continue;
|
||||
}
|
||||
IDProperty *idgroup = pchan_EnsureProperties(pchan);
|
||||
read_ufbx_property(prop, idgroup, enums_as_strings);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
FILE *g_debug_file;
|
||||
void print_matrix(const ufbx_matrix &m)
|
||||
{
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].x),
|
||||
adjf(m.cols[1].x),
|
||||
adjf(m.cols[2].x),
|
||||
adjf(m.cols[3].x));
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].y),
|
||||
adjf(m.cols[1].y),
|
||||
adjf(m.cols[2].y),
|
||||
adjf(m.cols[3].y));
|
||||
fprintf(g_debug_file,
|
||||
" (%.3f %.3f %.3f %.3f)\n",
|
||||
adjf(m.cols[0].z),
|
||||
adjf(m.cols[1].z),
|
||||
adjf(m.cols[2].z),
|
||||
adjf(m.cols[3].z));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace blender::io::fbx
|
||||
106
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.hh
Normal file
106
blender-5.2.0/source/blender/io/fbx/importer/fbx_import_util.hh
Normal file
@@ -0,0 +1,106 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup fbx
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "ufbx.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct Object;
|
||||
struct Key;
|
||||
struct Material;
|
||||
struct bPoseChannel;
|
||||
|
||||
namespace io::fbx {
|
||||
|
||||
const char *get_fbx_name(const ufbx_string &name, const char *def = "Untitled");
|
||||
|
||||
struct FbxElementMapping {
|
||||
Set<Object *> imported_objects;
|
||||
Map<const ufbx_element *, Object *> el_to_object;
|
||||
Map<const ufbx_element *, Key *> el_to_shape_key;
|
||||
Map<const ufbx_material *, Material *> mat_to_material;
|
||||
Map<const ufbx_node *, Object *> bone_to_armature;
|
||||
|
||||
/* For the armatures we create, for different use cases we need transform
|
||||
* from world space to the root bone, either in posed transform or in
|
||||
* node transform. */
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_pose_matrix;
|
||||
Map<const Object *, ufbx_matrix> armature_world_to_arm_node_matrix;
|
||||
|
||||
/* Which FBX bone nodes got turned into actual armature bones (not all of them
|
||||
* always are; in some cases root bone is the armature object itself). */
|
||||
Set<const ufbx_node *> node_is_blender_bone;
|
||||
|
||||
/* Mapping of ufbx node to object name used within blender. If names are too long
|
||||
* or duplicate, they might not match what was in FBX file. */
|
||||
Map<const ufbx_node *, std::string> node_to_name;
|
||||
/* Bone node to "bind matrix", i.e. matrix that transforms from bone (in skin bind pose) local
|
||||
* space to world space. This records bone pose or skin cluster bind matrix (skin cluster taking
|
||||
* precedence if it exists). */
|
||||
Map<const ufbx_node *, ufbx_matrix> bone_to_bind_matrix;
|
||||
Map<const ufbx_node *, ufbx_real> bone_to_length;
|
||||
Set<const ufbx_node *> bone_is_skinned;
|
||||
ufbx_matrix global_conv_matrix;
|
||||
|
||||
ufbx_matrix get_node_bind_matrix(const ufbx_node *node) const
|
||||
{
|
||||
return this->bone_to_bind_matrix.lookup_default(node, node->geometry_to_world);
|
||||
}
|
||||
|
||||
ufbx_matrix calc_local_bind_matrix(const ufbx_node *bone_node,
|
||||
const ufbx_matrix &world_to_arm) const
|
||||
{
|
||||
ufbx_matrix res = this->get_node_bind_matrix(bone_node);
|
||||
ufbx_matrix parent_inv_mtx;
|
||||
if (bone_node->parent != nullptr && !bone_node->parent->is_root) {
|
||||
ufbx_matrix parent_mtx = this->get_node_bind_matrix(bone_node->parent);
|
||||
parent_inv_mtx = ufbx_matrix_invert(&parent_mtx);
|
||||
}
|
||||
else {
|
||||
parent_inv_mtx = world_to_arm;
|
||||
}
|
||||
res = ufbx_matrix_mul(&parent_inv_mtx, &res);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
void matrix_to_m44(const ufbx_matrix &src, float dst[4][4]);
|
||||
void ufbx_matrix_to_obj(const ufbx_matrix &mtx, Object *obj);
|
||||
void node_matrix_to_obj(const ufbx_node *node, Object *obj, const FbxElementMapping &mapping);
|
||||
void read_custom_properties(const ufbx_props &props, ID &id, bool enums_as_strings);
|
||||
void read_custom_properties(const ufbx_props &props, bPoseChannel &pchan, bool enums_as_strings);
|
||||
|
||||
ufbx_matrix calc_bone_pose_matrix(const ufbx_transform &local_xform,
|
||||
const ufbx_node &node,
|
||||
const ufbx_matrix &local_bind_inv_matrix);
|
||||
|
||||
//@TODO remove debug file print once things are working properly
|
||||
// #define FBX_DEBUG_PRINT
|
||||
|
||||
#ifdef FBX_DEBUG_PRINT
|
||||
extern FILE *g_debug_file;
|
||||
|
||||
inline double adjf(double f)
|
||||
{
|
||||
if (fabs(f) < 0.0005) {
|
||||
return 0.0;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
void print_matrix(const ufbx_matrix &m);
|
||||
#endif
|
||||
|
||||
} // namespace io::fbx
|
||||
} // namespace blender
|
||||
56
blender-5.2.0/source/blender/io/grease_pencil/CMakeLists.txt
Normal file
56
blender-5.2.0/source/blender/io/grease_pencil/CMakeLists.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
.
|
||||
../common
|
||||
../../editors/include
|
||||
../../makesdna
|
||||
../../makesrna
|
||||
../../../../intern/guardedalloc
|
||||
../../../../intern/utfconv
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
intern/grease_pencil_io.cc
|
||||
intern/grease_pencil_io_import_svg.cc
|
||||
|
||||
grease_pencil_io.hh
|
||||
intern/grease_pencil_io_intern.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::blenloader
|
||||
PRIVATE bf::bmesh
|
||||
PRIVATE bf::depsgraph
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::extern::nanosvg
|
||||
PRIVATE bf::functions
|
||||
PRIVATE bf::geometry
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
PRIVATE bf::windowmanager
|
||||
PRIVATE bf::dependencies::optional::pugixml
|
||||
PRIVATE bf::dependencies::optional::haru
|
||||
bf_io_common
|
||||
)
|
||||
|
||||
if(WITH_PUGIXML)
|
||||
list(APPEND SRC
|
||||
intern/grease_pencil_io_export_svg.cc
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WITH_HARU)
|
||||
list(APPEND SRC
|
||||
intern/grease_pencil_io_export_pdf.cc
|
||||
)
|
||||
endif()
|
||||
|
||||
blender_add_lib(bf_io_grease_pencil "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user