Merged master@e12c3110024dc4b62ffefa739288218b04e39f71 into blender2.8
This doesn't merge entire master into blender2.8, but at least solves one conflict.
This commit is contained in:
@@ -205,6 +205,7 @@ void AbcCurveReader::readObjectData(Main *bmain, float time)
|
|||||||
|
|
||||||
cu->flag |= CU_DEFORM_FILL | CU_3D;
|
cu->flag |= CU_DEFORM_FILL | CU_3D;
|
||||||
cu->actvert = CU_ACT_NONE;
|
cu->actvert = CU_ACT_NONE;
|
||||||
|
cu->resolu = 1;
|
||||||
|
|
||||||
m_object = BKE_object_add_only_object(bmain, OB_CURVE, m_object_name.c_str());
|
m_object = BKE_object_add_only_object(bmain, OB_CURVE, m_object_name.c_str());
|
||||||
m_object->data = cu;
|
m_object->data = cu;
|
||||||
@@ -250,13 +251,18 @@ void read_curve_sample(Curve *cu, const ICurvesSchema &schema, const float time)
|
|||||||
nu->pntsv = 1;
|
nu->pntsv = 1;
|
||||||
nu->flag |= CU_SMOOTH;
|
nu->flag |= CU_SMOOTH;
|
||||||
|
|
||||||
nu->orderu = num_verts;
|
switch (smp.getType()) {
|
||||||
|
case Alembic::AbcGeom::kCubic:
|
||||||
if (smp.getType() == Alembic::AbcGeom::kCubic) {
|
nu->orderu = 4;
|
||||||
nu->orderu = 3;
|
break;
|
||||||
}
|
case Alembic::AbcGeom::kVariableOrder:
|
||||||
else if (orders && orders->size() > i) {
|
if (orders && orders->size() > i) {
|
||||||
nu->orderu = static_cast<short>((*orders)[i] - 1);
|
nu->orderu = static_cast<short>((*orders)[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Alembic::AbcGeom::kLinear:
|
||||||
|
default:
|
||||||
|
nu->orderu = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (periodicity == Alembic::AbcGeom::kNonPeriodic) {
|
if (periodicity == Alembic::AbcGeom::kNonPeriodic) {
|
||||||
|
@@ -443,4 +443,24 @@ if(WITH_CYCLES)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(WITH_ALEMBIC)
|
||||||
|
if(MSVC)
|
||||||
|
add_test(NAME cycles_${subject}_test
|
||||||
|
COMMAND
|
||||||
|
"$<TARGET_FILE_DIR:blender>/${BLENDER_VERSION_MAJOR}.${BLENDER_VERSION_MINOR}/python/bin/python$<$<CONFIG:Debug>:_d>"
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/alembic_tests.py
|
||||||
|
--blender "${TEST_BLENDER_EXE_BARE}"
|
||||||
|
--testdir "${TEST_SRC_DIR}/alembic"
|
||||||
|
--alembic-root "${ALEMBIC_ROOT_DIR}"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
add_test(alembic_tests
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/alembic_tests.py
|
||||||
|
--blender "${TEST_BLENDER_EXE_BARE}"
|
||||||
|
--testdir "${TEST_SRC_DIR}/alembic"
|
||||||
|
--alembic-root "${ALEMBIC_ROOT_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(render_layer)
|
add_subdirectory(render_layer)
|
||||||
|
225
tests/python/alembic_tests.py
Executable file
225
tests/python/alembic_tests.py
Executable file
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation; either version 2
|
||||||
|
# of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
# <pep8 compliant>
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import functools
|
||||||
|
import shutil
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
def with_tempdir(wrapped):
|
||||||
|
"""Creates a temporary directory for the function, cleaning up after it returns normally.
|
||||||
|
|
||||||
|
When the wrapped function raises an exception, the contents of the temporary directory
|
||||||
|
remain available for manual inspection.
|
||||||
|
|
||||||
|
The wrapped function is called with an extra positional argument containing
|
||||||
|
the pathlib.Path() of the temporary directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(wrapped)
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
dirname = tempfile.mkdtemp(prefix='blender-alembic-test')
|
||||||
|
try:
|
||||||
|
retval = wrapped(*args, pathlib.Path(dirname), **kwargs)
|
||||||
|
except:
|
||||||
|
print('Exception in %s, not cleaning up temporary directory %s' % (wrapped, dirname))
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
shutil.rmtree(dirname)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractAlembicTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
import re
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--blender', required=True)
|
||||||
|
parser.add_argument('--testdir', required=True)
|
||||||
|
parser.add_argument('--alembic-root', required=True)
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
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+)\])?$')
|
||||||
|
|
||||||
|
def run_blender(self, filepath: str, python_script: str, timeout: int=300) -> str:
|
||||||
|
"""Runs Blender by opening a blendfile and executing a script.
|
||||||
|
|
||||||
|
Returns Blender's stdout + stderr combined into one string.
|
||||||
|
|
||||||
|
:param filepath: taken relative to self.testdir.
|
||||||
|
:param timeout: in seconds
|
||||||
|
"""
|
||||||
|
|
||||||
|
blendfile = self.testdir / filepath
|
||||||
|
|
||||||
|
command = (
|
||||||
|
self.blender,
|
||||||
|
'--background',
|
||||||
|
'-noaudio',
|
||||||
|
'--factory-startup',
|
||||||
|
'--enable-autoexec',
|
||||||
|
str(blendfile),
|
||||||
|
'-E', 'CYCLES',
|
||||||
|
'--python-exit-code', '47',
|
||||||
|
'--python-expr', python_script,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
timeout=timeout)
|
||||||
|
output = proc.stdout.decode('utf8')
|
||||||
|
if proc.returncode:
|
||||||
|
self.fail('Error %d running Blender:\n%s' % (proc.returncode, output))
|
||||||
|
|
||||||
|
return 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 just strings
|
||||||
|
as returned by abcls.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
abcls = self.alembic_root / 'bin' / 'abcls'
|
||||||
|
|
||||||
|
command = (str(abcls), '-vl', '%s%s' % (filepath, proppath))
|
||||||
|
proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
timeout=30)
|
||||||
|
|
||||||
|
coloured_output = proc.stdout
|
||||||
|
output = self.ansi_remove_re.sub(b'', coloured_output).decode('utf8')
|
||||||
|
|
||||||
|
if proc.returncode:
|
||||||
|
self.fail('Error %d running abcls:\n%s' % (proc.returncode, output))
|
||||||
|
|
||||||
|
# Mapping from value type to callable that can convert a string to Python values.
|
||||||
|
converters = {
|
||||||
|
'bool_t': int,
|
||||||
|
'uint8_t': int,
|
||||||
|
'float64_t': float,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Ideally we'd get abcls to output JSON, see https://github.com/alembic/alembic/issues/121
|
||||||
|
lines = output.split('\n')
|
||||||
|
for info, value in zip(lines[0::2], lines[1::2]):
|
||||||
|
proptype, valtype_and_arrsize, name_and_extent = info.split()
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
# Parse type and convert values
|
||||||
|
m = self.abcls_array.match(valtype_and_arrsize)
|
||||||
|
if not m:
|
||||||
|
self.fail('Unparsable value type from abcls: %s' % valtype_and_arrsize)
|
||||||
|
valtype, arraysize = m.group('name'), m.group('arraysize')
|
||||||
|
|
||||||
|
try:
|
||||||
|
conv = converters[valtype]
|
||||||
|
except KeyError:
|
||||||
|
self.fail('Unsupported type %s for property %s/%s' % (valtype, proppath, name))
|
||||||
|
|
||||||
|
if arraysize is None:
|
||||||
|
result[name] = conv(value)
|
||||||
|
else:
|
||||||
|
values = [conv(v.strip()) for v in value.split(',')]
|
||||||
|
result[name] = values
|
||||||
|
|
||||||
|
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, " \
|
||||||
|
"renderable_only=True, visible_layers_only=True, flatten=False)" % abc
|
||||||
|
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, " \
|
||||||
|
"renderable_only=True, visible_layers_only=True, flatten=True)" % abc
|
||||||
|
self.run_blender('cubes-hierarchy.blend', script)
|
||||||
|
|
||||||
|
# Now check the resulting Alembic file.
|
||||||
|
xform = self.abcprop(abc, '/Cube_012/.xform')
|
||||||
|
self.assertEqual(0, xform['.inherits'])
|
||||||
|
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(argv=sys.argv[0:1])
|
Reference in New Issue
Block a user