diff options
-rw-r--r-- | source/blender/alembic/intern/abc_object.cc | 46 | ||||
-rw-r--r-- | source/blender/alembic/intern/abc_transform.cc | 31 | ||||
-rw-r--r-- | source/blender/alembic/intern/abc_util.cc | 10 | ||||
-rw-r--r-- | source/blender/alembic/intern/abc_util.h | 4 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/constraint.c | 3 | ||||
-rw-r--r-- | source/blender/blenloader/intern/versioning_280.c | 9 | ||||
-rw-r--r-- | tests/python/bl_alembic_io_test.py | 104 |
7 files changed, 153 insertions, 54 deletions
diff --git a/source/blender/alembic/intern/abc_object.cc b/source/blender/alembic/intern/abc_object.cc index f6f266c808e..4799ed557c8 100644 --- a/source/blender/alembic/intern/abc_object.cc +++ b/source/blender/alembic/intern/abc_object.cc @@ -257,11 +257,19 @@ bool AbcObjectReader::topology_changed(Mesh * /*existing_mesh*/, void AbcObjectReader::setupObjectTransform(const float time) { bool is_constant = false; + float transform_from_alembic[4][4]; - this->read_matrix(m_object->obmat, time, m_settings->scale, is_constant); - invert_m4_m4(m_object->imat, m_object->obmat); + /* 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); - BKE_object_apply_mat4(m_object, m_object->obmat, false, false); + /* 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->obmat); if (!is_constant) { bConstraint *con = BKE_constraint_add_for_object( @@ -311,7 +319,7 @@ Alembic::AbcGeom::IXform AbcObjectReader::xform() return IXform(); } -void AbcObjectReader::read_matrix(float r_mat[4][4], +void AbcObjectReader::read_matrix(float r_mat[4][4] /* local matrix */, const float time, const float scale, bool &is_constant) @@ -331,25 +339,19 @@ void AbcObjectReader::read_matrix(float r_mat[4][4], } const Imath::M44d matrix = get_matrix(schema, time); - convert_matrix(matrix, m_object, r_mat); - - if (m_inherits_xform) { - /* In this case, the matrix in Alembic is in local coordinates, so - * convert to world matrix. To prevent us from reading and accumulating - * all parent matrices in the Alembic file, we assume that the Blender - * parent object is already updated for the current timekey, and use its - * world matrix. */ - if (m_object->parent) { - mul_m4_m4m4(r_mat, m_object->parent->obmat, r_mat); - } - else { - /* This can happen if the user deleted the parent object, but also if the Alembic parent was - * not imported (because of unknown/unsupported schema, for example). In that case just use - * the local matrix as if it is the world matrix. This allows us to import Alembic files from - * MeshRoom, see T61935. */ - } + convert_matrix(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); } - else { + + 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); diff --git a/source/blender/alembic/intern/abc_transform.cc b/source/blender/alembic/intern/abc_transform.cc index 585d4178e41..838e657fee9 100644 --- a/source/blender/alembic/intern/abc_transform.cc +++ b/source/blender/alembic/intern/abc_transform.cc @@ -42,23 +42,6 @@ using Alembic::AbcGeom::OXform; /* ************************************************************************** */ -static bool has_parent_camera(Object *ob) -{ - if (!ob->parent) { - return false; - } - - Object *parent = ob->parent; - - if (parent->type == OB_CAMERA) { - return true; - } - - return has_parent_camera(parent); -} - -/* ************************************************************************** */ - AbcTransformWriter::AbcTransformWriter(Object *ob, const OObject &abc_parent, AbcTransformWriter *parent, @@ -98,14 +81,22 @@ void AbcTransformWriter::do_write() create_transform_matrix( ob_eval, yup_mat, m_inherits_xform ? ABC_MATRIX_LOCAL : ABC_MATRIX_WORLD, m_proxy_from); - /* Only apply rotation to root camera, parenting will propagate it. */ - if (ob_eval->type == OB_CAMERA && (!m_inherits_xform || !has_parent_camera(ob_eval))) { + /* If the parent is a camera, undo its to-Maya rotation (see below). */ + bool is_root_object = !m_inherits_xform || ob_eval->parent == nullptr; + if (!is_root_object && ob_eval->parent->type == OB_CAMERA) { + float rot_mat[4][4]; + axis_angle_to_mat4_single(rot_mat, 'X', M_PI_2); + mul_m4_m4m4(yup_mat, rot_mat, yup_mat); + } + + /* If the object is a camera, apply an extra rotation to Maya camera orientation. */ + if (ob_eval->type == OB_CAMERA) { float rot_mat[4][4]; axis_angle_to_mat4_single(rot_mat, 'X', -M_PI_2); mul_m4_m4m4(yup_mat, yup_mat, rot_mat); } - if (!ob_eval->parent || !m_inherits_xform) { + if (is_root_object) { /* Only apply scaling to root objects, parenting will propagate it. */ float scale_mat[4][4]; scale_m4_fl(scale_mat, m_settings.global_scale); diff --git a/source/blender/alembic/intern/abc_util.cc b/source/blender/alembic/intern/abc_util.cc index b6743c8b363..56978374512 100644 --- a/source/blender/alembic/intern/abc_util.cc +++ b/source/blender/alembic/intern/abc_util.cc @@ -224,21 +224,13 @@ void copy_m44_axis_swap(float dst_mat[4][4], float src_mat[4][4], AbcAxisSwapMod mul_m4_m4m4(dst_mat, dst_mat, dst_scale_mat); } -void convert_matrix(const Imath::M44d &xform, Object *ob, float r_mat[4][4]) +void convert_matrix(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] = static_cast<float>(xform[i][j]); } } - - if (ob->type == OB_CAMERA) { - float cam_to_yup[4][4]; - axis_angle_to_mat4_single(cam_to_yup, 'X', M_PI_2); - mul_m4_m4m4(r_mat, r_mat, cam_to_yup); - } - - copy_m44_axis_swap(r_mat, r_mat, ABC_ZUP_FROM_YUP); } /* Recompute transform matrix of object in new coordinate system diff --git a/source/blender/alembic/intern/abc_util.h b/source/blender/alembic/intern/abc_util.h index 5eb0ed70599..bfeded37c12 100644 --- a/source/blender/alembic/intern/abc_util.h +++ b/source/blender/alembic/intern/abc_util.h @@ -51,6 +51,7 @@ std::string get_id_name(const ID *const id); std::string get_id_name(const Object *const ob); std::string get_object_dag_path_name(const Object *const ob, Object *dupli_parent); +/* Convert from float to Alembic matrix representations. Does NOT convert from Z-up to Y-up. */ Imath::M44d convert_matrix(float mat[4][4]); typedef enum { @@ -69,7 +70,8 @@ template<class TContainer> bool begins_with(const TContainer &input, const TCont return input.size() >= match.size() && std::equal(match.begin(), match.end(), input.begin()); } -void convert_matrix(const Imath::M44d &xform, Object *ob, float r_mat[4][4]); +/* Convert from Alembic to float matrix representations. Does NOT convert from Y-up to Z-up. */ +void convert_matrix(const Imath::M44d &xform, float r_mat[4][4]); template<typename Schema> void get_min_max_time_ex(const Schema &schema, chrono_t &min, chrono_t &max) diff --git a/source/blender/blenkernel/intern/constraint.c b/source/blender/blenkernel/intern/constraint.c index 76acaf5c91c..42efd9a7057 100644 --- a/source/blender/blenkernel/intern/constraint.c +++ b/source/blender/blenkernel/intern/constraint.c @@ -5271,6 +5271,9 @@ static bConstraint *add_new_constraint(Object *ob, } break; } + case CONSTRAINT_TYPE_TRANSFORM_CACHE: + con->ownspace = CONSTRAINT_SPACE_LOCAL; + break; } return con; diff --git a/source/blender/blenloader/intern/versioning_280.c b/source/blender/blenloader/intern/versioning_280.c index f9c89747120..d306049a7e0 100644 --- a/source/blender/blenloader/intern/versioning_280.c +++ b/source/blender/blenloader/intern/versioning_280.c @@ -4473,5 +4473,14 @@ void blo_do_versions_280(FileData *fd, Library *UNUSED(lib), Main *bmain) */ { /* Keep this block, even when empty. */ + + /* Alembic Transform Cache changed from world to local space. */ + LISTBASE_FOREACH (Object *, ob, &bmain->objects) { + LISTBASE_FOREACH (bConstraint *, con, &ob->constraints) { + if (con->type == CONSTRAINT_TYPE_TRANSFORM_CACHE) { + con->ownspace = CONSTRAINT_SPACE_LOCAL; + } + } + } } } diff --git a/tests/python/bl_alembic_io_test.py b/tests/python/bl_alembic_io_test.py index 41b28cb7c33..0f74b773c32 100644 --- a/tests/python/bl_alembic_io_test.py +++ b/tests/python/bl_alembic_io_test.py @@ -22,11 +22,14 @@ ./blender.bin --background -noaudio --factory-startup --python tests/python/bl_alembic_io_test.py -- --testdir /path/to/lib/tests/alembic """ +import math import pathlib import sys +import tempfile import unittest import bpy +from mathutils import Euler, Matrix, Vector args = None @@ -134,8 +137,6 @@ class SimpleImportTest(AbstractAlembicTest): self.assertEqual('Cube' in ob.name, ob.select_get()) def test_change_path_constraint(self): - import math - fname = 'cube-rotating1.abc' abc = self.testdir / fname relpath = bpy.path.relpath(str(abc)) @@ -250,6 +251,105 @@ class VertexColourImportTest(AbstractAlembicTest): self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0)) +class CameraExportImportTest(unittest.TestCase): + names = [ + 'CAM_Unit_Transform', + 'CAM_Look_+Y', + 'CAM_Static_Child_Left', + 'CAM_Static_Child_Right', + 'Static_Child', + 'CAM_Animated', + 'CAM_Animated_Child_Left', + 'CAM_Animated_Child_Right', + 'Animated_Child', + ] + + def setUp(self): + self._tempdir = tempfile.TemporaryDirectory() + self.tempdir = pathlib.Path(self._tempdir.name) + + def tearDown(self): + self._tempdir.cleanup() + + def test_export_hierarchy(self): + self.do_export_import_test(flatten=False) + + # Double-check that the export was hierarchical. + objects = bpy.context.scene.collection.objects + for name in self.names: + if 'Child' in name: + self.assertIsNotNone(objects[name].parent) + else: + self.assertIsNone(objects[name].parent) + + def test_export_flattened(self): + self.do_export_import_test(flatten=True) + + # Double-check that the export was flat. + objects = bpy.context.scene.collection.objects + for name in self.names: + self.assertIsNone(objects[name].parent) + + def do_export_import_test(self, *, flatten: bool): + bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "camera_transforms.blend")) + + abc_path = self.tempdir / "camera_transforms.abc" + self.assertIn('FINISHED', bpy.ops.wm.alembic_export( + filepath=str(abc_path), + renderable_only=False, + flatten=flatten, + )) + + # Re-import what we just exported into an empty file. + bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "empty.blend")) + self.assertIn('FINISHED', bpy.ops.wm.alembic_import(filepath=str(abc_path))) + + # Test that the import was ok. + bpy.context.scene.frame_set(1) + self.loc_rot_scale('CAM_Unit_Transform', (0, 0, 0), (0, 0, 0)) + + self.loc_rot_scale('CAM_Look_+Y', (2, 0, 0), (90, 0, 0)) + self.loc_rot_scale('CAM_Static_Child_Left', (2-0.15, 0, 0), (90, 0, 0)) + self.loc_rot_scale('CAM_Static_Child_Right', (2+0.15, 0, 0), (90, 0, 0)) + self.loc_rot_scale('Static_Child', (2, 0, 1), (90, 0, 0)) + + self.loc_rot_scale('CAM_Animated', (4, 0, 0), (90, 0, 0)) + self.loc_rot_scale('CAM_Animated_Child_Left', (4-0.15, 0, 0), (90, 0, 0)) + self.loc_rot_scale('CAM_Animated_Child_Right', (4+0.15, 0, 0), (90, 0, 0)) + self.loc_rot_scale('Animated_Child', (4, 0, 1), (90, 0, 0)) + + bpy.context.scene.frame_set(10) + + self.loc_rot_scale('CAM_Animated', (4, 1, 2), (90, 0, 25)) + self.loc_rot_scale('CAM_Animated_Child_Left', (3.864053, 0.936607, 2), (90, 0, 25)) + self.loc_rot_scale('CAM_Animated_Child_Right', (4.135946, 1.063392, 2), (90, 0, 25)) + self.loc_rot_scale('Animated_Child', (4, 1, 3), (90, -45, 25)) + + def loc_rot_scale(self, name: str, expect_loc, expect_rot_deg): + """Assert world loc/rot/scale is OK.""" + + objects = bpy.context.scene.collection.objects + depsgraph = bpy.context.evaluated_depsgraph_get() + ob_eval = objects[name].evaluated_get(depsgraph) + + actual_loc = ob_eval.matrix_world.to_translation() + actual_rot = ob_eval.matrix_world.to_euler('XYZ') + actual_scale = ob_eval.matrix_world.to_scale() + + self.assertAlmostEqual(expect_loc[0], actual_loc.x, delta=1e-5) + self.assertAlmostEqual(expect_loc[1], actual_loc.y, delta=1e-5) + self.assertAlmostEqual(expect_loc[2], actual_loc.z, delta=1e-5) + + self.assertAlmostEqual(expect_rot_deg[0], math.degrees(actual_rot.x), delta=1e-5) + self.assertAlmostEqual(expect_rot_deg[1], math.degrees(actual_rot.y), delta=1e-5) + self.assertAlmostEqual(expect_rot_deg[2], math.degrees(actual_rot.z), delta=1e-5) + + # This test doesn't use scale. + self.assertAlmostEqual(1, actual_scale.x, delta=1e-5) + self.assertAlmostEqual(1, actual_scale.y, delta=1e-5) + self.assertAlmostEqual(1, actual_scale.z, delta=1e-5) + + def main(): global args import argparse |