2017-04-14 12:54:20 +02:00
|
|
|
#!/usr/bin/env python3
|
2023-08-16 00:20:26 +10:00
|
|
|
# SPDX-FileCopyrightText: 2017-2023 Blender Authors
|
2023-06-15 13:09:04 +10:00
|
|
|
#
|
2022-02-11 09:07:11 +11:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
2017-04-14 12:54:20 +02:00
|
|
|
|
2020-08-17 12:57:15 +02:00
|
|
|
"""
|
|
|
|
Alembic Export Tests
|
|
|
|
|
|
|
|
This test suite runs outside of Blender. Tests run Blender to call the exporter,
|
|
|
|
and then use the Alembic CLI tools to inspect the exported Alembic files.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2017-04-14 12:54:20 +02:00
|
|
|
import argparse
|
|
|
|
import pathlib
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import unittest
|
|
|
|
|
2018-11-26 09:26:15 +11:00
|
|
|
from modules.test_utils import (
|
|
|
|
with_tempdir,
|
|
|
|
AbstractBlenderRunnerTest,
|
|
|
|
)
|
2017-04-14 12:54:20 +02:00
|
|
|
|
|
|
|
|
2017-04-19 13:03:52 +02:00
|
|
|
class AbcPropError(Exception):
|
|
|
|
"""Raised when AbstractAlembicTest.abcprop() finds an error."""
|
|
|
|
|
|
|
|
|
2018-03-01 17:39:57 +01:00
|
|
|
class AbstractAlembicTest(AbstractBlenderRunnerTest):
|
2017-04-14 12:54:20 +02:00
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
import re
|
|
|
|
|
|
|
|
cls.blender = args.blender
|
|
|
|
cls.testdir = pathlib.Path(args.testdir)
|
|
|
|
cls.alembic_root = pathlib.Path(args.alembic_root)
|
|
|
|
|
|
|
|
# 'abcls' outputs ANSI colour codes, even when stdout is not a terminal.
|
|
|
|
# See https://github.com/alembic/alembic/issues/120
|
|
|
|
cls.ansi_remove_re = re.compile(rb'\x1b[^m]*m')
|
|
|
|
|
|
|
|
# 'abcls' array notation, like "name[16]"
|
|
|
|
cls.abcls_array = re.compile(r'^(?P<name>[^\[]+)(\[(?P<arraysize>\d+)\])?$')
|
|
|
|
|
2024-10-23 12:48:09 +11:00
|
|
|
def abcls(self, *arguments) -> tuple[int, str]:
|
2020-08-17 16:58:09 +02:00
|
|
|
"""Uses abcls and return its output.
|
2017-04-14 12:54:20 +02:00
|
|
|
|
2020-08-17 16:58:09 +02:00
|
|
|
:return: tuple (process exit status code, stdout)
|
2017-04-14 12:54:20 +02:00
|
|
|
"""
|
|
|
|
|
2020-08-17 16:58:09 +02:00
|
|
|
command = (self.alembic_root / 'bin' / 'abcls', *arguments)
|
2020-08-17 20:07:45 +02:00
|
|
|
# Convert Path to str; Path works fine on Linux, but not on Windows.
|
|
|
|
command_str = [str(arg) for arg in command]
|
|
|
|
proc = subprocess.run(command_str, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
2017-04-14 12:54:20 +02:00
|
|
|
timeout=30)
|
|
|
|
|
|
|
|
coloured_output = proc.stdout
|
|
|
|
output = self.ansi_remove_re.sub(b'', coloured_output).decode('utf8')
|
|
|
|
|
2017-04-28 15:28:41 +02:00
|
|
|
# Because of the ANSI colour codes, we need to remove those first before
|
|
|
|
# decoding to text. This means that we cannot use the universal_newlines
|
|
|
|
# parameter to subprocess.run(), and have to do the conversion ourselves
|
|
|
|
output = output.replace('\r\n', '\n').replace('\r', '\n')
|
|
|
|
|
2017-04-14 12:54:20 +02:00
|
|
|
if proc.returncode:
|
2020-08-17 16:58:09 +02:00
|
|
|
str_command = " ".join(str(c) for c in command)
|
|
|
|
print(f'command {str_command} failed with status {proc.returncode}')
|
|
|
|
|
|
|
|
return (proc.returncode, output)
|
|
|
|
|
|
|
|
def abcprop(self, filepath: pathlib.Path, proppath: str) -> dict:
|
|
|
|
"""Uses abcls to obtain compound property values from an Alembic object.
|
|
|
|
|
|
|
|
A dict of subproperties is returned, where the values are Python values.
|
|
|
|
|
|
|
|
The Python bindings for Alembic are old, and only compatible with Python 2.x,
|
|
|
|
so that's why we can't use them here, and have to rely on other tooling.
|
|
|
|
"""
|
|
|
|
import collections
|
|
|
|
|
|
|
|
command = ('-vl', '%s%s' % (filepath, proppath))
|
|
|
|
returncode, output = self.abcls(*command)
|
|
|
|
if returncode:
|
|
|
|
raise AbcPropError('Error %d running abcls:\n%s' % (returncode, output))
|
2017-04-14 12:54:20 +02:00
|
|
|
|
|
|
|
# Mapping from value type to callable that can convert a string to Python values.
|
|
|
|
converters = {
|
|
|
|
'bool_t': int,
|
|
|
|
'uint8_t': int,
|
2017-04-18 16:36:33 +02:00
|
|
|
'int16_t': int,
|
2017-04-18 12:17:07 +02:00
|
|
|
'int32_t': int,
|
2020-05-26 16:38:47 +02:00
|
|
|
'uint32_t': int,
|
2017-04-19 13:03:52 +02:00
|
|
|
'uint64_t': int,
|
2017-04-14 12:54:20 +02:00
|
|
|
'float64_t': float,
|
2017-04-18 12:17:07 +02:00
|
|
|
'float32_t': float,
|
Alembic export: write custom properties
Write custom properties (aka ID properties) to Alembic, to the
`.userProperties` compound property.
Manifest Task: https://developer.blender.org/T50725
Scalar properties (so single-value/non-array properties) are written as
single-element array properties to Alembic. This is also what's done by
Houdini and Maya exporters, so it seems to be the standard way of doing
things. It also simplifies the implementation.
Two-dimensional arrays are flattened by concatenating all the numbers
into a single array. This is because ID properties have a limited type
system. This means that a 3x3 "matrix" could just as well be a list of
three 3D vectors.
Alembic has two container properties to store custom data:
- `.userProperties`, which is meant for properties that aren't
necessarily understood by other software packages, and
- `.arbGeomParams`, which can contain the same kind of data as
`.userProperties`, but can also specify that these vary per face of a
mesh. This property is mostly intended for renderers.
Most industry packages write their custom data to `.arbGeomParams`.
However, given their goals I feel that `.userProperties` is the more
appropriate one for Blender's ID Properties.
The code is a bit more involved than I would have liked. An
`ABCAbstractWriter` has a `uniqueptr` to its `CustomPropertiesExporter`,
but the `CustomPropertiesExporter` also has a pointer back to its owning
`ABCAbstractWriter`. It's the latter pointer that I'm not too happy
with, but it has a reason. Getting the aforementioned `.userProperties`
from the Alembic library will automatically create it if it doesn't
exist already. If it's not used to actually add custom properties to, it
will crash the Alembic CLI tools (and maybe others too). This is what
the pointer back to the `ABCAbstractWriter` is used for: to get the
`.userProperties` at the last moment, when it's 100% sure at least one
custom property will be written.
Differential Revision: https://developer.blender.org/D8869
Reviewed by: sergey, dbystedt
2020-09-11 14:06:13 +02:00
|
|
|
'string': str,
|
2017-04-14 12:54:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
|
|
|
|
# Ideally we'd get abcls to output JSON, see https://github.com/alembic/alembic/issues/121
|
2017-04-18 12:17:07 +02:00
|
|
|
lines = collections.deque(output.split('\n'))
|
|
|
|
while lines:
|
|
|
|
info = lines.popleft()
|
|
|
|
if not info:
|
|
|
|
continue
|
2017-04-18 16:36:33 +02:00
|
|
|
parts = info.split()
|
|
|
|
proptype = parts[0]
|
|
|
|
|
|
|
|
if proptype == 'CompoundProperty':
|
|
|
|
# To read those, call self.abcprop() on it.
|
|
|
|
continue
|
2020-08-17 14:03:11 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
valtype_and_arrsize, name_and_extent = parts[1:]
|
|
|
|
except ValueError as ex:
|
2020-08-17 15:16:06 +02:00
|
|
|
raise ValueError(f'Error parsing result from abcprop "{info.strip()}": {ex}') from ex
|
2017-04-14 12:54:20 +02:00
|
|
|
|
|
|
|
# Parse name and extent
|
|
|
|
m = self.abcls_array.match(name_and_extent)
|
|
|
|
if not m:
|
|
|
|
self.fail('Unparsable name/extent from abcls: %s' % name_and_extent)
|
|
|
|
name, extent = m.group('name'), m.group('arraysize')
|
|
|
|
|
|
|
|
if extent != '1':
|
|
|
|
self.fail('Unsupported extent %s for property %s/%s' % (extent, proppath, name))
|
|
|
|
|
2017-04-18 12:17:07 +02:00
|
|
|
# Parse type
|
2017-04-14 12:54:20 +02:00
|
|
|
m = self.abcls_array.match(valtype_and_arrsize)
|
|
|
|
if not m:
|
|
|
|
self.fail('Unparsable value type from abcls: %s' % valtype_and_arrsize)
|
2017-04-18 12:17:07 +02:00
|
|
|
valtype, scalarsize = m.group('name'), m.group('arraysize')
|
2017-04-14 12:54:20 +02:00
|
|
|
|
2017-04-18 12:17:07 +02:00
|
|
|
# Convert values
|
2017-04-14 12:54:20 +02:00
|
|
|
try:
|
|
|
|
conv = converters[valtype]
|
|
|
|
except KeyError:
|
|
|
|
self.fail('Unsupported type %s for property %s/%s' % (valtype, proppath, name))
|
|
|
|
|
2017-04-18 12:17:07 +02:00
|
|
|
def convert_single_line(linevalue):
|
|
|
|
try:
|
|
|
|
if scalarsize is None:
|
|
|
|
return conv(linevalue)
|
|
|
|
else:
|
|
|
|
return [conv(v.strip()) for v in linevalue.split(',')]
|
|
|
|
except ValueError as ex:
|
|
|
|
return str(ex)
|
|
|
|
|
|
|
|
if proptype == 'ScalarProperty':
|
|
|
|
value = lines.popleft()
|
|
|
|
result[name] = convert_single_line(value)
|
|
|
|
elif proptype == 'ArrayProperty':
|
|
|
|
arrayvalue = []
|
|
|
|
# Arrays consist of a variable number of items, and end in a blank line.
|
|
|
|
while True:
|
|
|
|
linevalue = lines.popleft()
|
|
|
|
if not linevalue:
|
|
|
|
break
|
|
|
|
arrayvalue.append(convert_single_line(linevalue))
|
|
|
|
result[name] = arrayvalue
|
2017-04-14 12:54:20 +02:00
|
|
|
else:
|
2017-04-18 12:17:07 +02:00
|
|
|
self.fail('Unsupported type %s for property %s/%s' % (proptype, proppath, name))
|
2017-04-14 12:54:20 +02:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def assertAlmostEqualFloatArray(self, actual, expect, places=6, delta=None):
|
|
|
|
"""Asserts that the arrays of floats are almost equal."""
|
|
|
|
|
|
|
|
self.assertEqual(len(actual), len(expect),
|
|
|
|
'Actual array has %d items, expected %d' % (len(actual), len(expect)))
|
|
|
|
|
|
|
|
for idx, (act, exp) in enumerate(zip(actual, expect)):
|
|
|
|
self.assertAlmostEqual(act, exp, places=places, delta=delta,
|
|
|
|
msg='%f != %f at index %d' % (act, exp, idx))
|
|
|
|
|
|
|
|
|
|
|
|
class HierarchicalAndFlatExportTest(AbstractAlembicTest):
|
|
|
|
@with_tempdir
|
|
|
|
def test_hierarchical_export(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'cubes_hierarchical.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=False)" % abc.as_posix()
|
2017-04-14 12:54:20 +02:00
|
|
|
self.run_blender('cubes-hierarchy.blend', script)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
|
|
|
xform = self.abcprop(abc, '/Cube/Cube_002/Cube_012/.xform')
|
|
|
|
self.assertEqual(1, xform['.inherits'])
|
|
|
|
self.assertAlmostEqualFloatArray(
|
|
|
|
xform['.vals'],
|
|
|
|
[1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0,
|
|
|
|
3.07484, -2.92265, 0.0586434, 1.0]
|
|
|
|
)
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_flat_export(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'cubes_flat.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=True)" % abc.as_posix()
|
2017-04-14 12:54:20 +02:00
|
|
|
self.run_blender('cubes-hierarchy.blend', script)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
|
|
|
xform = self.abcprop(abc, '/Cube_012/.xform')
|
2020-06-19 10:24:57 +02:00
|
|
|
self.assertEqual(1, xform['.inherits'], "Blender transforms always inherit")
|
2017-04-14 12:54:20 +02:00
|
|
|
|
|
|
|
self.assertAlmostEqualFloatArray(
|
|
|
|
xform['.vals'],
|
|
|
|
[0.343134, 0.485243, 0.804238, 0,
|
|
|
|
0.0, 0.856222, -0.516608, 0,
|
|
|
|
-0.939287, 0.177266, 0.293799, 0,
|
|
|
|
1, 3, 4, 1],
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2017-04-14 18:20:24 +02:00
|
|
|
class DupliGroupExportTest(AbstractAlembicTest):
|
|
|
|
@with_tempdir
|
|
|
|
def test_hierarchical_export(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'dupligroup_hierarchical.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=False)" % abc.as_posix()
|
2017-04-14 18:20:24 +02:00
|
|
|
self.run_blender('dupligroup-scene.blend', script)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
Alembic: new exporter based on the USD exporter structure
The Alembic exporter has been restructured by leverages the
`AbstractHierarchyIterator` introduced by the USD exporter. The produced
Alembic files have not changed much (details below), as the Alembic
writing code has simply been moved from the old exporter to the new. How
the export hierarchy is handled changed a lot, though, and also the way
in which transforms are computed. As a result, T71395 is fixed.
Differences between the old and new exporter, in terms of the produced
Alembic file:
- Duplicated objects now have a unique numerical suffix.
- Matrices are computed differently, namely by simply computing the
evaluated transform of the object relative to the evaluated transform
of its export-parent. This fixes {T71395}, but otherwise should
produce the same result as before (but with simpler code).
Compared to the old Alembic exporter, Subdivision modifiers are now
disabled in a cleaner, more efficient way (they are disabled when
exporting with the "Apply Subdivisions" option is unchecked). Previously
the exporter would move to a new frame, disable the modifier, evaluate
the object, and enable the modifier again. This is now done before
exporting starts, and modifiers are only restored when exporting ends.
Some issues with the old Alembic exporter that have NOT been fixed in
this patch:
- Exporting NURBS patches and curves (see T49114 for example).
- Exporting flattened hierarchy in combination with dupli-objects. This
seems to be broken in the old Alembic exporter as well, but nobody
reported this yet.
Differential Revision: https://developer.blender.org/D7664
Reviewed By: Sergey
2020-06-19 16:36:10 +02:00
|
|
|
xform = self.abcprop(abc, '/Real_Cube/Linked_Suzanne/Cylinder-0/Suzanne-1/.xform')
|
2017-04-14 18:20:24 +02:00
|
|
|
self.assertEqual(1, xform['.inherits'])
|
|
|
|
self.assertAlmostEqualFloatArray(
|
|
|
|
xform['.vals'],
|
|
|
|
[1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0,
|
|
|
|
0.0, 2.0, 0.0, 1.0]
|
|
|
|
)
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_flat_export(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'dupligroup_hierarchical.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=True)" % abc.as_posix()
|
2017-04-14 18:20:24 +02:00
|
|
|
self.run_blender('dupligroup-scene.blend', script)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
Alembic: new exporter based on the USD exporter structure
The Alembic exporter has been restructured by leverages the
`AbstractHierarchyIterator` introduced by the USD exporter. The produced
Alembic files have not changed much (details below), as the Alembic
writing code has simply been moved from the old exporter to the new. How
the export hierarchy is handled changed a lot, though, and also the way
in which transforms are computed. As a result, T71395 is fixed.
Differences between the old and new exporter, in terms of the produced
Alembic file:
- Duplicated objects now have a unique numerical suffix.
- Matrices are computed differently, namely by simply computing the
evaluated transform of the object relative to the evaluated transform
of its export-parent. This fixes {T71395}, but otherwise should
produce the same result as before (but with simpler code).
Compared to the old Alembic exporter, Subdivision modifiers are now
disabled in a cleaner, more efficient way (they are disabled when
exporting with the "Apply Subdivisions" option is unchecked). Previously
the exporter would move to a new frame, disable the modifier, evaluate
the object, and enable the modifier again. This is now done before
exporting starts, and modifiers are only restored when exporting ends.
Some issues with the old Alembic exporter that have NOT been fixed in
this patch:
- Exporting NURBS patches and curves (see T49114 for example).
- Exporting flattened hierarchy in combination with dupli-objects. This
seems to be broken in the old Alembic exporter as well, but nobody
reported this yet.
Differential Revision: https://developer.blender.org/D7664
Reviewed By: Sergey
2020-06-19 16:36:10 +02:00
|
|
|
xform = self.abcprop(abc, '/Suzanne-1/.xform')
|
2020-06-19 10:24:57 +02:00
|
|
|
self.assertEqual(1, xform['.inherits'])
|
2017-04-14 18:20:24 +02:00
|
|
|
|
|
|
|
self.assertAlmostEqualFloatArray(
|
|
|
|
xform['.vals'],
|
|
|
|
[1.5, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.5, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.5, 0.0,
|
|
|
|
2.0, 3.0, 0.0, 1.0]
|
|
|
|
)
|
|
|
|
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
@with_tempdir
|
|
|
|
def test_multiple_duplicated_hierarchies(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / "multiple-duplicated-hierarchies.abc"
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1)" % abc.as_posix()
|
|
|
|
self.run_blender('multiple-duplicated-hierarchies.blend', script)
|
|
|
|
|
|
|
|
# This is the expected hierarchy:
|
|
|
|
# ABC
|
|
|
|
# `--Triangle
|
|
|
|
# |--Triangle
|
|
|
|
# |--Empty-1
|
2020-07-07 14:30:55 +02:00
|
|
|
# | `--Pole-1-0
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
# | |--Pole
|
|
|
|
# | `--Block-1-1
|
|
|
|
# | `--Block
|
|
|
|
# |--Empty
|
|
|
|
# | `--Pole-0
|
|
|
|
# | |--Pole
|
|
|
|
# | `--Block-1
|
|
|
|
# | `--Block
|
|
|
|
# |--Empty-2
|
2020-07-07 14:30:55 +02:00
|
|
|
# | `--Pole-2-0
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
# | |--Pole
|
2020-07-07 14:30:55 +02:00
|
|
|
# | `--Block-2-1
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
# | `--Block
|
|
|
|
# `--Empty-0
|
|
|
|
# `--Pole-0-0
|
|
|
|
# |--Pole
|
2020-07-07 14:30:55 +02:00
|
|
|
# `--Block-0-1
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
# `--Block
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
2020-07-07 14:30:55 +02:00
|
|
|
xform = self.abcprop(abc, '/Triangle/Empty-1/Pole-1-0/Block-1-1/.xform')
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
self.assertEqual(1, xform['.inherits'])
|
|
|
|
self.assertAlmostEqualFloatArray(
|
|
|
|
xform['.vals'],
|
|
|
|
[1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0,
|
|
|
|
0.0, 2.0, 0.0, 1.0]
|
|
|
|
)
|
|
|
|
|
|
|
|
# If the property can be gotten, the hierarchy is okay. No need to actually check each xform.
|
|
|
|
self.abcprop(abc, '/Triangle/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty-1/.xform')
|
2020-07-07 14:30:55 +02:00
|
|
|
self.abcprop(abc, '/Triangle/Empty-1/Pole-1-0/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty-1/Pole-1-0/Block-1-1/.xform')
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
self.abcprop(abc, '/Triangle/Empty/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty/Pole-0/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty/Pole-0/Block-1/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty-2/.xform')
|
2020-07-07 14:30:55 +02:00
|
|
|
self.abcprop(abc, '/Triangle/Empty-2/Pole-2-0/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty-2/Pole-2-0/Block-2-1/.xform')
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
self.abcprop(abc, '/Triangle/Empty-0/.xform')
|
|
|
|
self.abcprop(abc, '/Triangle/Empty-0/Pole-0-0/.xform')
|
2020-07-07 14:30:55 +02:00
|
|
|
self.abcprop(abc, '/Triangle/Empty-0/Pole-0-0/Block-0-1/.xform')
|
IO: Fix bug exporting dupli parent/child relations
Exporting a scene to USD or Alembic would fail when there are multiple
duplicates of parent & child objects, duplicated by the same object. For
example, this happens when such a hierarchy of objects is contained in a
collection, and that collection is instanced multiple times by mesh
vertices. The problem here is that the 'parent' pointer of each
duplicated object points to the real parent; Blender would not figure
out properly which duplicated parent should be used.
This is now resolved by keeping track of the persistent ID of each
duplicated instance, which makes it possible to reconstruct the
parent-child relations of duplicated objects. This does use up some
memory for each dupli, so it could be heavy to export a Spring scene
(with all the pebbles and leaves), but it's only a small addition on top
of the USD/Alembic writer objects that have to be created anyway. At
least with this patch, they're created correctly.
Code-wise, the following changes are made:
- The export graph (that maps export parent to its export children) used
to have as its key (Object, Duplicator). This is insufficient to
correctly distinguish between multiple duplis of the same object by
the same duplicator, so this is now extended to (Object, Duplicator,
Persistent ID). To make this possible, new classes `ObjectIdentifier`
and `PersistentID` are introduced.
- Finding the parent of a duplicated object is done via its persistent
ID. In Python notation, the code first tries to find the parent
instance where `child_persistent_id[1:] == parent_persistent_id[1:]`.
If that fails, the dupli with persistent ID `child_persistent_id[1:]`
is used as parent.
Reviewed By: sergey
Differential Revision: https://developer.blender.org/D8233
2020-07-07 12:45:30 +02:00
|
|
|
|
2017-04-14 18:20:24 +02:00
|
|
|
|
2017-04-18 12:17:07 +02:00
|
|
|
class CurveExportTest(AbstractAlembicTest):
|
|
|
|
@with_tempdir
|
|
|
|
def test_export_single_curve(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'single-curve.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=False)" % abc.as_posix()
|
2017-04-18 12:17:07 +02:00
|
|
|
self.run_blender('single-curve.blend', script)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/NurbsCurve/CurveData/.geom')
|
2017-04-18 12:17:07 +02:00
|
|
|
self.assertEqual(abcprop['.orders'], [4])
|
2017-04-14 18:20:24 +02:00
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/NurbsCurve/CurveData/.geom/.userProperties')
|
2017-04-18 16:36:33 +02:00
|
|
|
self.assertEqual(abcprop['blender:resolution'], 10)
|
|
|
|
|
2017-04-14 18:20:24 +02:00
|
|
|
|
2017-04-19 13:03:52 +02:00
|
|
|
class HairParticlesExportTest(AbstractAlembicTest):
|
|
|
|
"""Tests exporting with/without hair/particles.
|
|
|
|
|
|
|
|
Just a basic test to ensure that the enabling/disabling works, and that export
|
|
|
|
works at all. NOT testing the quality/contents of the exported file.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _do_test(self, tempdir: pathlib.Path, export_hair: bool, export_particles: bool) -> pathlib.Path:
|
|
|
|
abc = tempdir / 'hair-particles.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=True, flatten=False, " \
|
2017-04-19 14:40:57 +02:00
|
|
|
"export_hair=%r, export_particles=%r, as_background_job=False)" \
|
2017-04-28 15:28:41 +02:00
|
|
|
% (abc.as_posix(), export_hair, export_particles)
|
2017-04-19 13:03:52 +02:00
|
|
|
self.run_blender('hair-particles.blend', script)
|
|
|
|
return abc
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_with_both(self, tempdir: pathlib.Path):
|
|
|
|
abc = self._do_test(tempdir, True, True)
|
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/Hair_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('nVertices', abcprop)
|
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/Non-hair_particle_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.velocities', abcprop)
|
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/MonkeyMesh/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.faceIndices', abcprop)
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_with_hair_only(self, tempdir: pathlib.Path):
|
|
|
|
abc = self._do_test(tempdir, True, False)
|
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/Hair_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('nVertices', abcprop)
|
|
|
|
|
|
|
|
self.assertRaises(AbcPropError, self.abcprop, abc,
|
2020-05-08 13:42:39 +02:00
|
|
|
'/Suzanne/Non-hair_particle_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/MonkeyMesh/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.faceIndices', abcprop)
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_with_particles_only(self, tempdir: pathlib.Path):
|
|
|
|
abc = self._do_test(tempdir, False, True)
|
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
self.assertRaises(AbcPropError, self.abcprop, abc, '/Suzanne/Hair_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/Non-hair_particle_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.velocities', abcprop)
|
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/MonkeyMesh/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.faceIndices', abcprop)
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_with_neither(self, tempdir: pathlib.Path):
|
|
|
|
abc = self._do_test(tempdir, False, False)
|
|
|
|
|
2020-05-08 13:42:39 +02:00
|
|
|
self.assertRaises(AbcPropError, self.abcprop, abc, '/Suzanne/Hair_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertRaises(AbcPropError, self.abcprop, abc,
|
2020-05-08 13:42:39 +02:00
|
|
|
'/Suzanne/Non-hair_particle_system/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Suzanne/MonkeyMesh/.geom')
|
2017-04-19 13:03:52 +02:00
|
|
|
self.assertIn('.faceIndices', abcprop)
|
|
|
|
|
|
|
|
|
2020-05-26 16:38:47 +02:00
|
|
|
class UVMapExportTest(AbstractAlembicTest):
|
|
|
|
@with_tempdir
|
|
|
|
def test_uvmap_export(self, tempdir: pathlib.Path):
|
|
|
|
"""Minimal test for exporting multiple UV maps on an animated mesh.
|
|
|
|
|
2023-02-12 14:37:16 +11:00
|
|
|
This covers the issue reported in #77021.
|
2020-05-26 16:38:47 +02:00
|
|
|
"""
|
|
|
|
basename = 'T77021-multiple-uvmaps-animated-mesh'
|
|
|
|
abc = tempdir / f'{basename}.abc'
|
|
|
|
script = f"import bpy; bpy.ops.wm.alembic_export(filepath='{abc.as_posix()}', start=1, end=1, " \
|
2024-10-23 12:48:09 +11:00
|
|
|
f"visible_objects_only=True, flatten=False)"
|
2020-05-26 16:38:47 +02:00
|
|
|
self.run_blender(f'{basename}.blend', script)
|
|
|
|
|
|
|
|
self.maxDiff = 1000
|
|
|
|
|
|
|
|
# The main UV map should be written to .geom
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Cube/Cube/.geom/uv')
|
2020-05-26 16:38:47 +02:00
|
|
|
self.assertEqual(abcprop['.vals'], [
|
|
|
|
[0.625, 0.75],
|
|
|
|
[0.875, 0.75],
|
|
|
|
[0.875, 0.5],
|
|
|
|
[0.625, 0.5],
|
|
|
|
[0.375, 1.0],
|
|
|
|
[0.625, 1.0],
|
|
|
|
[0.375, 0.75],
|
|
|
|
[0.375, 0.25],
|
|
|
|
[0.625, 0.25],
|
|
|
|
[0.625, 0.0],
|
|
|
|
[0.375, 0.0],
|
|
|
|
[0.125, 0.75],
|
|
|
|
[0.375, 0.5],
|
|
|
|
[0.125, 0.5],
|
|
|
|
])
|
|
|
|
|
|
|
|
# The second UV map should be written to .arbGeomParams
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '/Cube/Cube/.geom/.arbGeomParams/Secondary')
|
2020-05-26 16:38:47 +02:00
|
|
|
self.assertEqual(abcprop['.vals'], [
|
|
|
|
[0.75, 0.375],
|
|
|
|
[0.75, 0.125],
|
|
|
|
[0.5, 0.125],
|
|
|
|
[0.5, 0.375],
|
|
|
|
[1.0, 0.625],
|
|
|
|
[1.0, 0.375],
|
|
|
|
[0.75, 0.625],
|
|
|
|
[0.25, 0.625],
|
|
|
|
[0.25, 0.375],
|
|
|
|
[0.0, 0.375],
|
|
|
|
[0.0, 0.625],
|
|
|
|
[0.75, 0.875],
|
|
|
|
[0.5, 0.625],
|
|
|
|
[0.5, 0.875],
|
|
|
|
])
|
|
|
|
|
|
|
|
|
2017-04-25 12:05:55 +02:00
|
|
|
class LongNamesExportTest(AbstractAlembicTest):
|
|
|
|
@with_tempdir
|
|
|
|
def test_export_long_names(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'long-names.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=False, flatten=False)" % abc.as_posix()
|
2017-04-25 12:05:55 +02:00
|
|
|
self.run_blender('long-names.blend', script)
|
|
|
|
|
|
|
|
name_parts = [
|
|
|
|
'foG9aeLahgoh5goacee1dah6Hethaghohjaich5pasizairuWigee1ahPeekiGh',
|
|
|
|
'yoNgoisheedah2ua0eigh2AeCaiTee5bo0uphoo7Aixephah9racahvaingeeH4',
|
|
|
|
'zuthohnoi1thooS3eezoo8seuph2Boo5aefacaethuvee1aequoonoox1sookie',
|
|
|
|
'wugh4ciTh3dipiepeequait5uug7thiseek5ca7Eijei5ietaizokohhaecieto',
|
|
|
|
'up9aeheenein9oteiX6fohP3thiez6Ahvah0oohah1ep2Eesho4Beboechaipoh',
|
|
|
|
'coh4aehiacheTh0ue0eegho9oku1lohl4loht9ohPoongoow7dasiego6yimuis',
|
|
|
|
'lohtho8eigahfeipohviepajaix4it2peeQu6Iefee1nevihaes4cee2soh4noy',
|
|
|
|
'kaht9ahv0ieXaiyih7ohxe8bah7eeyicahjoa2ohbu7Choxua7oongah6sei4bu',
|
|
|
|
'deif0iPaechohkee5nahx6oi2uJeeN7ze3seunohJibe4shai0mah5Iesh3Quai',
|
|
|
|
'ChohDahshooNee0NeNohthah0eiDeese3Vu6ohShil1Iey9ja0uebi2quiShae6',
|
|
|
|
'Dee1kai7eiph2ahh2nufah3zai3eexeengohQue1caj0eeW0xeghi3eshuadoot',
|
|
|
|
'aeshiup3aengajoog0AhCoo5tiu3ieghaeGhie4Tu1ohh1thee8aepheingah1E',
|
|
|
|
'ooRa6ahciolohshaifoopeo9ZeiGhae2aech4raisheiWah9AaNga0uas9ahquo',
|
|
|
|
'thaepheip2aip6shief4EaXopei8ohPo0ighuiXah2ashowai9nohp4uach6Mei',
|
|
|
|
'ohph4yaev3quieji3phophiem3OoNuisheepahng4waithae3Naichai7aw3noo',
|
|
|
|
'aibeawaneBahmieyuph8ieng8iopheereeD2uu9Uyee5bei2phahXeir8eeJ8oo',
|
|
|
|
'ooshahphei2hoh3uth5chaen7ohsai6uutiesucheichai8ungah9Gie1Aiphie',
|
|
|
|
'eiwohchoo7ere2iebohn4Aapheichaelooriiyaoxaik7ooqua7aezahx0aeJei',
|
|
|
|
'Vah0ohgohphiefohTheshieghichaichahch5moshoo0zai5eeva7eisi4yae8T',
|
|
|
|
'EibeeN0fee0Gohnguz8iec6yeigh7shuNg4eingu3siph9joucahpeidoom4ree',
|
|
|
|
'iejiu3shohheeZahHusheimeefaihoh5eecachu5eeZie9ceisugu9taidohT3U',
|
|
|
|
'eex6dilakaix5Eetai7xiCh5Jaa8aiD4Ag3tuij1aijohv5fo0heevah8hohs3m',
|
|
|
|
'ohqueeNgahraew6uraemohtoo5qua3oojiex6ohqu6Aideibaithaiphuriquie',
|
|
|
|
'cei0eiN4Shiey7Aeluy3unohboo5choiphahc2mahbei5paephaiKeso1thoog1',
|
|
|
|
'ieghif4ohKequ7ong0jah5ooBah0eiGh1caechahnahThae9Shoo0phopashoo4',
|
|
|
|
'roh9er3thohwi5am8iequeequuSh3aic0voocai3ihi5nie2abahphupiegh7vu',
|
|
|
|
'uv3Quei7wujoo5beingei2aish5op4VaiX0aebai7iwoaPee5pei8ko9IepaPig',
|
|
|
|
'co7aegh5beitheesi9lu7jeeQu3johgeiphee9cheichi8aithuDehu2gaeNein',
|
|
|
|
'thai3Tiewoo4nuir1ohy4aithiuZ7shae1luuwei5phibohriepe2paeci1Ach8',
|
|
|
|
'phoi3ribah7ufuvoh8eigh1oB6deeBaiPohphaghiPieshahfah5EiCi3toogoo',
|
|
|
|
'aiM8geil7ooreinee4Cheiwea4yeec8eeshi7Sei4Shoo3wu6ohkaNgooQu1mai',
|
|
|
|
'agoo3faciewah9ZeesiXeereek7am0eigaeShie3Tisu8haReeNgoo0ci2Hae5u',
|
|
|
|
'Aesatheewiedohshaephaenohbooshee8eu7EiJ8isal1laech2eiHo0noaV3ta',
|
|
|
|
'liunguep3ooChoo4eir8ahSie8eenee0oo1TooXu8Cais8Aimo4eir6Phoo3xei',
|
|
|
|
'toe9heepeobein3teequachemei0Cejoomef9ujie3ohwae9AiNgiephi3ep0de',
|
|
|
|
'ua6xooY9uzaeB3of6sheiyaedohoiS5Eev0Aequ9ahm1zoa5Aegh3ooz9ChahDa',
|
|
|
|
'eevasah6Bu9wi7EiwiequumahkaeCheegh6lui8xoh4eeY4ieneavah8phaibun',
|
|
|
|
'AhNgei2sioZeeng6phaecheemeehiShie5eFeiTh6ooV8iiphabud0die4siep4',
|
|
|
|
'kushe6Xieg6ahQuoo9aex3aipheefiec1esa7OhBuG0ueziep9phai5eegh1vie',
|
|
|
|
'Jie5yu8aafuQuoh9shaep3moboh3Pooy7och8oC6obeik6jaew2aiLooweib3ch',
|
|
|
|
'ohohjajaivaiRail3odaimei6aekohVaicheip2wu7phieg5Gohsaing2ahxaiy',
|
|
|
|
'hahzaht6yaiYu9re9jah9loisiit4ahtoh2quoh9xohishioz4oo4phofu3ogha',
|
|
|
|
'pu4oorea0uh2tahB8aiZoonge1aophaes6ogaiK9ailaigeej4zoVou8ielotee',
|
|
|
|
'cae2thei3Luphuqu0zeeG8leeZuchahxaicai4ui4Eedohte9uW6gae8Geeh0ea',
|
|
|
|
'air7tuy7ohw5sho2Tahpai8aep4so5ria7eaShus5weaqu0Naquei2xaeyoo2ae',
|
|
|
|
'vohge4aeCh7ahwoo7Jaex6sohl0Koong4Iejisei8Coir0iemeiz9uru9Iebaep',
|
|
|
|
'aepeidie8aiw6waish9gie4Woolae2thuj5phae4phexux7gishaeph4Deu7ooS',
|
|
|
|
'vahc5ia0xohHooViT0uyuxookiaquu2ogueth0ahquoudeefohshai8aeThahba',
|
|
|
|
'mun3oagah2eequaenohfoo8DaigeghoozaV2eiveeQuee7kah0quaa6tiesheet',
|
|
|
|
'ooSet4IdieC4ugow3za0die4ohGoh1oopoh6luaPhaeng4Eechea1hae0eimie5',
|
|
|
|
'iedeimadaefu2NeiPaey2jooloov5iehiegeakoo4ueso7aeK9ahqu2Thahkaes',
|
|
|
|
'nahquah9Quuu2uuf0aJah7eishi2siegh8ue5eiJa2EeVu8ebohkepoh4dahNgo',
|
|
|
|
'io1bie7chioPiej5ae2oohe2fee6ooP2thaeJohjohb9Se8tang3eipaifeimai',
|
|
|
|
'oungoqu6dieneejiechez1xeD2Zi9iox2Ahchaiy9ithah3ohVoolu2euQuuawo',
|
|
|
|
'thaew0veigei4neishohd8mecaixuqu7eeshiex1chaigohmoThoghoitoTa0Eo',
|
|
|
|
'ahroob2phohvaiz0Ohteik2ohtakie6Iu1vitho8IyiyeeleeShae9defaiw9ki',
|
|
|
|
'DohHoothohzeaxolai3Toh5eJie7ahlah9reF0ohn1chaipoogain2aibahw4no',
|
|
|
|
'aif8lo5she4aich5cho2rie8ieJaujeem2Joongeedae4vie3tah1Leequaix1O',
|
|
|
|
'Aang0Shaih6chahthie1ahZ7aewei9thiethee7iuThah3yoongi8ahngiobaa5',
|
|
|
|
'iephoBuayoothah0Ru6aichai4aiw8deg1umongauvaixai3ohy6oowohlee8ei',
|
|
|
|
'ohn5shigoameer0aejohgoh8oChohlaecho9jie6shu0ahg9Bohngau6paevei9',
|
|
|
|
'edahghaishak0paigh1eecuich3aad7yeB0ieD6akeeliem2beifufaekee6eat',
|
|
|
|
'hiechahgheloh2zo7Ieghaiph0phahhu8aeyuiKie1xeipheech9zai4aeme0ee',
|
|
|
|
'Cube'
|
|
|
|
]
|
|
|
|
name = '/' + '/'.join(name_parts)
|
|
|
|
|
|
|
|
# Now check the resulting Alembic file.
|
|
|
|
abcprop = self.abcprop(abc, '%s/.xform' % name)
|
|
|
|
self.assertEqual(abcprop['.vals'], [
|
|
|
|
1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0,
|
|
|
|
0.0, 3.0, 0.0, 1.0,
|
|
|
|
])
|
|
|
|
|
2020-06-18 18:10:19 +02:00
|
|
|
abcprop = self.abcprop(abc, '%s/Cube/.geom' % name)
|
2017-04-25 12:05:55 +02:00
|
|
|
self.assertIn('.faceCounts', abcprop)
|
|
|
|
|
|
|
|
|
2020-08-17 16:58:09 +02:00
|
|
|
class InvisibleObjectExportTest(AbstractAlembicTest):
|
|
|
|
"""Export an object which is invisible.
|
|
|
|
|
|
|
|
This test only tests a small subset of the functionality that is required to
|
|
|
|
export invisible objects. It just tests that the visibility property is
|
|
|
|
written, and that it has the correct initial value. This is a limitation
|
2024-11-03 19:18:34 +11:00
|
|
|
caused by these tests relying on ``abcls``.
|
2020-08-17 16:58:09 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_hierarchical_export(self, tempdir: pathlib.Path):
|
|
|
|
abc = tempdir / 'visibility.abc'
|
|
|
|
script = "import bpy; bpy.ops.wm.alembic_export(filepath='%s', start=1, end=2, " \
|
2021-07-05 11:27:17 +02:00
|
|
|
"visible_objects_only=False)" % abc.as_posix()
|
2020-08-17 16:58:09 +02:00
|
|
|
self.run_blender('visibility.blend', script)
|
|
|
|
|
|
|
|
def test(cube_name: str, expect_visible: bool):
|
|
|
|
returncode, output = self.abcls('-va', f'{abc}/{cube_name}')
|
|
|
|
if returncode:
|
|
|
|
self.fail(f"abcls failed: {output}")
|
|
|
|
output = output.strip()
|
|
|
|
self.assertEqual(f'Cube .xform visible {int(expect_visible)}', output)
|
|
|
|
|
|
|
|
# This cube is always visible.
|
|
|
|
test('VisibleCube', True)
|
|
|
|
|
|
|
|
# This cube is never visible, and thus will not be pulled into the
|
|
|
|
# depsgraph by the standard builder, only by the all-objects builder.
|
|
|
|
test('InvisibleCube', False)
|
|
|
|
|
|
|
|
# This cube has animated visibility, and thus will be pulled into the
|
|
|
|
# depsgraph by the standard builder as well as the all-objects builder.
|
|
|
|
test('InvisibleAnimatedCube', False)
|
|
|
|
|
|
|
|
|
Alembic export: write custom properties
Write custom properties (aka ID properties) to Alembic, to the
`.userProperties` compound property.
Manifest Task: https://developer.blender.org/T50725
Scalar properties (so single-value/non-array properties) are written as
single-element array properties to Alembic. This is also what's done by
Houdini and Maya exporters, so it seems to be the standard way of doing
things. It also simplifies the implementation.
Two-dimensional arrays are flattened by concatenating all the numbers
into a single array. This is because ID properties have a limited type
system. This means that a 3x3 "matrix" could just as well be a list of
three 3D vectors.
Alembic has two container properties to store custom data:
- `.userProperties`, which is meant for properties that aren't
necessarily understood by other software packages, and
- `.arbGeomParams`, which can contain the same kind of data as
`.userProperties`, but can also specify that these vary per face of a
mesh. This property is mostly intended for renderers.
Most industry packages write their custom data to `.arbGeomParams`.
However, given their goals I feel that `.userProperties` is the more
appropriate one for Blender's ID Properties.
The code is a bit more involved than I would have liked. An
`ABCAbstractWriter` has a `uniqueptr` to its `CustomPropertiesExporter`,
but the `CustomPropertiesExporter` also has a pointer back to its owning
`ABCAbstractWriter`. It's the latter pointer that I'm not too happy
with, but it has a reason. Getting the aforementioned `.userProperties`
from the Alembic library will automatically create it if it doesn't
exist already. If it's not used to actually add custom properties to, it
will crash the Alembic CLI tools (and maybe others too). This is what
the pointer back to the `ABCAbstractWriter` is used for: to get the
`.userProperties` at the last moment, when it's 100% sure at least one
custom property will be written.
Differential Revision: https://developer.blender.org/D8869
Reviewed by: sergey, dbystedt
2020-09-11 14:06:13 +02:00
|
|
|
class CustomPropertiesExportTest(AbstractAlembicTest):
|
|
|
|
"""Test export of custom properties."""
|
|
|
|
|
|
|
|
def _run_export(self, tempdir: pathlib.Path) -> pathlib.Path:
|
|
|
|
abc = tempdir / 'custom-properties.abc'
|
|
|
|
script = (
|
|
|
|
"import bpy; bpy.context.scene.frame_set(1); "
|
|
|
|
"bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1)" % abc.as_posix()
|
2020-10-02 10:15:51 +10:00
|
|
|
)
|
Alembic export: write custom properties
Write custom properties (aka ID properties) to Alembic, to the
`.userProperties` compound property.
Manifest Task: https://developer.blender.org/T50725
Scalar properties (so single-value/non-array properties) are written as
single-element array properties to Alembic. This is also what's done by
Houdini and Maya exporters, so it seems to be the standard way of doing
things. It also simplifies the implementation.
Two-dimensional arrays are flattened by concatenating all the numbers
into a single array. This is because ID properties have a limited type
system. This means that a 3x3 "matrix" could just as well be a list of
three 3D vectors.
Alembic has two container properties to store custom data:
- `.userProperties`, which is meant for properties that aren't
necessarily understood by other software packages, and
- `.arbGeomParams`, which can contain the same kind of data as
`.userProperties`, but can also specify that these vary per face of a
mesh. This property is mostly intended for renderers.
Most industry packages write their custom data to `.arbGeomParams`.
However, given their goals I feel that `.userProperties` is the more
appropriate one for Blender's ID Properties.
The code is a bit more involved than I would have liked. An
`ABCAbstractWriter` has a `uniqueptr` to its `CustomPropertiesExporter`,
but the `CustomPropertiesExporter` also has a pointer back to its owning
`ABCAbstractWriter`. It's the latter pointer that I'm not too happy
with, but it has a reason. Getting the aforementioned `.userProperties`
from the Alembic library will automatically create it if it doesn't
exist already. If it's not used to actually add custom properties to, it
will crash the Alembic CLI tools (and maybe others too). This is what
the pointer back to the `ABCAbstractWriter` is used for: to get the
`.userProperties` at the last moment, when it's 100% sure at least one
custom property will be written.
Differential Revision: https://developer.blender.org/D8869
Reviewed by: sergey, dbystedt
2020-09-11 14:06:13 +02:00
|
|
|
self.run_blender('custom-properties.blend', script)
|
|
|
|
return abc
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_xform_props(self, tempdir: pathlib.Path) -> None:
|
|
|
|
abc = self._run_export(tempdir)
|
|
|
|
abcprop = self.abcprop(abc, '/Cube/.xform/.userProperties')
|
|
|
|
|
|
|
|
# Simple, single values.
|
|
|
|
self.assertEqual(abcprop['static_int'], [327])
|
|
|
|
self.assertEqual(abcprop['static_float'], [47.01])
|
|
|
|
self.assertEqual(abcprop['static_string'], ['Agents'])
|
|
|
|
self.assertEqual(abcprop['keyed_float'], [-1])
|
|
|
|
self.assertEqual(abcprop['keyed_int'], [-47])
|
|
|
|
|
|
|
|
# Arrays.
|
|
|
|
self.assertEqual(abcprop['keyed_array_float'], [-1.000, 0.000, 1.000])
|
|
|
|
self.assertEqual(abcprop['keyed_array_int'], [42, 47, 327])
|
|
|
|
|
|
|
|
# Multi-dimensional arrays.
|
|
|
|
self.assertEqual(abcprop['array_of_strings'], ['ผัดไทย', 'Pad Thai'])
|
|
|
|
self.assertEqual(
|
|
|
|
abcprop['matrix_tuple'],
|
|
|
|
[1.0, 0.0, 0.0, 3.33333, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
|
|
|
|
self.assertEqual(
|
|
|
|
abcprop['static_matrix'],
|
|
|
|
[1.0, 0.0, 0.0, 3.33333, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
|
|
|
|
self.assertEqual(
|
|
|
|
abcprop['nonuniform_array'],
|
|
|
|
[10, 20, 30, 1, 2, 47])
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_mesh_props(self, tempdir: pathlib.Path) -> None:
|
|
|
|
abc = self._run_export(tempdir)
|
|
|
|
abcprop = self.abcprop(abc, '/Cube/Cube/.geom/.userProperties')
|
|
|
|
self.assertEqual(abcprop['mesh_tags'], ['cube', 'box', 'low-poly-sphere'])
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_camera_props(self, tempdir: pathlib.Path) -> None:
|
|
|
|
abc = self._run_export(tempdir)
|
|
|
|
abcprop = self.abcprop(abc, '/Camera/Hasselblad/.geom/.userProperties')
|
|
|
|
self.assertEqual(abcprop['type'], ['500c/m'])
|
|
|
|
|
|
|
|
@with_tempdir
|
|
|
|
def test_disabled_export_option(self, tempdir: pathlib.Path) -> None:
|
|
|
|
abc = tempdir / 'custom-properties.abc'
|
|
|
|
script = (
|
|
|
|
"import bpy; bpy.context.scene.frame_set(1); "
|
|
|
|
"bpy.ops.wm.alembic_export(filepath='%s', start=1, end=1, export_custom_properties=False)" % abc.as_posix()
|
|
|
|
)
|
|
|
|
self.run_blender('custom-properties.blend', script)
|
|
|
|
|
|
|
|
abcprop = self.abcprop(abc, '/Camera/Hasselblad/.geom/.userProperties')
|
|
|
|
self.assertIn('eyeSeparation', abcprop, 'Regular non-standard properties should still be written')
|
|
|
|
self.assertNotIn('type', abcprop, 'Custom properties should not be written')
|
|
|
|
|
|
|
|
|
2017-04-14 12:54:20 +02:00
|
|
|
if __name__ == '__main__':
|
2017-04-18 12:20:41 +02:00
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument('--blender', required=True)
|
|
|
|
parser.add_argument('--testdir', required=True)
|
|
|
|
parser.add_argument('--alembic-root', required=True)
|
|
|
|
args, remaining = parser.parse_known_args()
|
|
|
|
|
|
|
|
unittest.main(argv=sys.argv[0:1] + remaining)
|