Skip to content

Commit d69f68c

Browse files
committed
Added support for exporting animations
1 parent c0dff13 commit d69f68c

3 files changed

Lines changed: 213 additions & 47 deletions

File tree

__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class ExportDatasmith(bpy.types.Operator, ExportHelper):
4848
description="Exports only the selected objects",
4949
default=False,
5050
)
51+
export_animations: BoolProperty(
52+
name="Export animations",
53+
description="Export object animations (transforms only)",
54+
default=False,
55+
)
5156
apply_modifiers: BoolProperty(
5257
name="Apply modifiers",
5358
description="Applies geometry modifiers when exporting. "

export_datasmith.py

Lines changed: 207 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,12 +1515,14 @@ def node_transform(mat):
15151515
n['sz'] = f(scale.z)
15161516
return n
15171517

1518-
def collect_object(bl_obj,
1518+
def collect_object(
1519+
bl_obj,
15191520
name_override=None,
15201521
instance_matrix=None,
15211522
selected_only=False,
1522-
apply_modifiers=False
1523-
):
1523+
apply_modifiers=False,
1524+
export_animations=False,
1525+
):
15241526

15251527
n = Node('Actor')
15261528

@@ -1531,11 +1533,7 @@ def collect_object(bl_obj,
15311533

15321534
n['layer'] = bl_obj.users_collection[0].name_full
15331535

1534-
mat_basis = bl_obj.matrix_world
1535-
if instance_matrix:
1536-
mat_basis = instance_matrix
15371536

1538-
obj_mat = matrix_datasmith @ mat_basis @ matrix_datasmith.inverted()
15391537

15401538
child_nodes = []
15411539

@@ -1548,19 +1546,25 @@ def collect_object(bl_obj,
15481546
# if this is selected, or if there is any child that needs this
15491547
# object to be placed in hierarchy
15501548
# TODO: collections don't work this way, investigate (export chair from classroom)
1551-
write_all_data = True
1549+
export_empty_because_unselected = False
15521550
if selected_only:
1553-
# by default write minimal data unless selected
1554-
write_all_data = False
1555-
if bl_obj in bpy.context.selected_objects:
1556-
write_all_data = True
1557-
elif not child_nodes:
1558-
# if we don't have children, just skip
1559-
return None
1560-
1561-
if write_all_data:
1562-
# TODO: use instanced static meshes
1551+
is_selected = bl_obj in bpy.context.selected_objects
1552+
if selected_only and not is_selected:
1553+
if len(child_nodes) == 0:
1554+
# We skip this object as it is not selected, and has no children selected
1555+
return None
1556+
else:
1557+
# we aren't selected, but we have selected children, so create minimal object
1558+
export_empty_because_unselected = True
15631559

1560+
# from here, we're absolutely sure that this object should be exported
1561+
1562+
obj_mat = collect_object_transform(bl_obj, instance_matrix)
1563+
transform = node_transform(obj_mat)
1564+
1565+
# if an object is not selected but is in hierarchy, we don't write data for it
1566+
if not export_empty_because_unselected:
1567+
# TODO: use instanced static meshes
15641568
depsgraph = datasmith_context["depsgraph"]
15651569

15661570
if bl_obj.is_instancer:
@@ -1576,11 +1580,36 @@ def collect_object(bl_obj,
15761580
name_override=dup_name,
15771581
selected_only=False, # if is instancer, maybe all child want to be instanced
15781582
apply_modifiers=False, # if is instancer, applying modifiers may end in a lot of meshes
1583+
export_animations=False, # TODO: test how would animation work mixed with instancing
15791584
)
15801585
child_nodes.append(new_obj)
15811586
#dups.append((dup.instance_object.original, dup.matrix_world.copy()))
15821587
dup_idx += 1
15831588

1589+
collect_object_custom_data(bl_obj, n, apply_modifiers, obj_mat, depsgraph)
1590+
1591+
# todo: maybe make some assumptions? like if obj is probe or reflection, don't add to animated objects list
1592+
1593+
if export_animations:
1594+
datasmith_context["anim_objects"].append((bl_obj, n["name"], obj_mat))
1595+
1596+
1597+
collect_object_metadata(n["name"], "Actor", bl_obj)
1598+
# just to make children appear last
1599+
n.push(transform)
1600+
1601+
if len(child_nodes) > 0:
1602+
children_node = Node("children");
1603+
for child in child_nodes:
1604+
if child:
1605+
children_node.push(child)
1606+
n.push(children_node)
1607+
1608+
1609+
return n
1610+
1611+
1612+
def collect_object_custom_data(bl_obj, n, apply_modifiers, obj_mat, depsgraph):
15841613
# I think that these should be ordered by how common they are
15851614
if bl_obj.type == 'EMPTY':
15861615
pass
@@ -1661,7 +1690,6 @@ def collect_object(bl_obj,
16611690

16621691
elif bl_obj.type == 'CAMERA':
16631692

1664-
obj_mat = obj_mat @ matrix_forward
16651693
bl_cam = bl_obj.data
16661694
n.name = 'Camera'
16671695

@@ -1681,7 +1709,6 @@ def collect_object(bl_obj,
16811709
# maybe move up as lights are more common?
16821710
elif bl_obj.type == 'LIGHT':
16831711

1684-
obj_mat = obj_mat @ matrix_forward
16851712
bl_light = bl_obj.data
16861713
n.name = 'Light'
16871714

@@ -1751,15 +1778,8 @@ def collect_object(bl_obj,
17511778
bl_probe = bl_obj.data
17521779
if bl_probe.type == 'PLANAR':
17531780
n["PathName"] = "/DatasmithBlenderContent/Blueprints/BP_BlenderPlanarReflection"
1754-
size = bl_probe.influence_distance * 2.5 # 100 / 40 as the UE4 mirror size is 40m wide
1755-
obj_mat = obj_mat @ Matrix.Scale(size, 4)
1756-
# todo: check what does the falloff do
17571781

17581782
elif bl_probe.type == 'CUBEMAP':
1759-
size = bl_probe.influence_distance * 100
1760-
falloff = bl_probe.falloff # this value is 0..1
1761-
1762-
# we need to have into account current object scale
17631783
## we could also try using min/max if it makes a difference
17641784
_, _, obj_scale = obj_mat.decompose()
17651785
avg_scale = (obj_scale.x + obj_scale.y + obj_scale.z) * 0.333333
@@ -1768,13 +1788,13 @@ def collect_object(bl_obj,
17681788
n["PathName"] = "/DatasmithBlenderContent/Blueprints/BP_BlenderBoxReflection"
17691789

17701790

1771-
transition_distance = falloff * size * avg_scale
1791+
falloff = bl_probe.falloff # this value is 0..1
1792+
transition_distance = falloff * avg_scale
17721793
prop = Node("KeyValueProperty", {"name": "TransitionDistance", "type":"Float", "val": "%.6f"%transition_distance})
17731794
n.push(prop)
1774-
obj_mat = obj_mat @ Matrix.Scale(size, 4)
1775-
else: # if bl_probe.type == 'ELIPSOID'
1795+
else: # if bl_probe.influence_type == 'ELIPSOID'
17761796
n["PathName"] = "/DatasmithBlenderContent/Blueprints/BP_BlenderSphereReflection"
1777-
probe_radius = size * avg_scale
1797+
probe_radius = bl_probe.influence_distance * 100 * avg_scale
17781798
radius = Node("KeyValueProperty", {"name": "Radius", "type":"Float", "val": "%.6f"%probe_radius})
17791799
n.push(radius)
17801800
elif bl_probe.type == 'GRID':
@@ -1786,27 +1806,34 @@ def collect_object(bl_obj,
17861806
# outward_influence would be 1.0 + influence_distance / size maybe?
17871807
# obj_mat = obj_mat @ Matrix.Scale(outward_influence, 4)
17881808

1789-
17901809
else:
17911810
log.error("unhandled light probe")
1811+
elif bl_obj.type == 'ARMATURE':
1812+
pass
17921813
else:
1793-
log.warn("unrecognized object type: %s" % bl_obj.type)
1814+
log.error("unrecognized object type: %s" % bl_obj.type)
17941815

1795-
# set transform at the end, lets the transform be changed depending on its type
1796-
transform = node_transform(obj_mat)
1797-
n.push(transform)
17981816

1799-
collect_object_metadata(n["name"], "Actor", bl_obj)
1800-
# just to make children appear last
18011817

1802-
if len(child_nodes) > 0:
1803-
children_node = Node("children");
1804-
for child in child_nodes:
1805-
if child:
1806-
children_node.push(child)
1807-
n.push(children_node)
1818+
def collect_object_transform(bl_obj, instance_matrix=None):
1819+
mat_basis = instance_matrix or bl_obj.matrix_world
1820+
obj_mat = matrix_datasmith @ mat_basis @ matrix_datasmith.inverted()
1821+
1822+
if bl_obj.type in 'CAMERA' or bl_obj.type == 'LIGHT':
1823+
obj_mat = obj_mat @ matrix_forward
1824+
elif bl_obj.type == 'LIGHT_PROBE':
1825+
bl_probe = bl_obj.data
1826+
if bl_probe.type == 'PLANAR':
1827+
size = bl_probe.influence_distance * 2.5 # 100 / 40 as the UE4 mirror size is 40m wide
1828+
obj_mat = obj_mat @ Matrix.Scale(size, 4)
1829+
elif bl_probe.type == 'CUBEMAP':
1830+
if bl_probe.influence_type == 'BOX':
1831+
size = bl_probe.influence_distance * 100
1832+
obj_mat = obj_mat @ Matrix.Scale(size, 4)
1833+
1834+
obj_mat.freeze() # TODO: check if this is needed
1835+
return obj_mat
18081836

1809-
return n
18101837

18111838
def collect_object_metadata(obj_name, obj_type, obj):
18121839
metadata = None
@@ -2049,6 +2076,7 @@ def collect_and_save(context, args, save_path):
20492076
global datasmith_context
20502077
datasmith_context = {
20512078
"objects": [],
2079+
"anim_objects": [],
20522080
"textures": [],
20532081
"meshes": [],
20542082
"materials": [],
@@ -2067,11 +2095,123 @@ def collect_and_save(context, args, save_path):
20672095
selected_only = args["export_selected"]
20682096
apply_modifiers = args["apply_modifiers"]
20692097
minimal_export = args["minimal_export"]
2098+
export_animations = args["export_animations"]
2099+
2100+
if export_animations:
2101+
frame_at_export_time = context.scene.frame_current
2102+
frame_start = context.scene.frame_start
2103+
frame_end = context.scene.frame_end
2104+
2105+
20702106
for obj in root_objects:
2071-
uobj = collect_object(obj, selected_only=selected_only, apply_modifiers=apply_modifiers)
2107+
uobj = collect_object(obj,
2108+
selected_only=selected_only,
2109+
apply_modifiers=apply_modifiers,
2110+
export_animations=export_animations,
2111+
)
20722112
if uobj:
20732113
objects.append(uobj)
20742114

2115+
log.info("collecting animations")
2116+
anims = []
2117+
if export_animations:
2118+
2119+
# TODO: found a bit late about this: we need to test and profile
2120+
# https://docs.blender.org/api/current/bpy_extras.anim_utils.html
2121+
2122+
anim_objs = datasmith_context["anim_objects"]
2123+
2124+
num_frames = frame_end - frame_start + 1
2125+
num_objects = len(anim_objs)
2126+
object_timelines = [[Matrix() for frame in range(num_frames)] for obj in range(num_objects)]
2127+
object_animates = [False for num in range(num_objects)]
2128+
# collect phase?
2129+
2130+
for arr_idx, frame_idx in enumerate(range(frame_start, frame_end+1)):
2131+
2132+
context.scene.frame_set(frame_idx)
2133+
2134+
for obj_idx, obj in enumerate(anim_objs):
2135+
2136+
obj_mat = collect_object_transform(obj[0])
2137+
object_timelines[obj_idx][arr_idx] = obj_mat
2138+
2139+
if arr_idx == 0:
2140+
continue
2141+
2142+
if not object_animates[obj_idx]:
2143+
changed = obj_mat != object_timelines[obj_idx][arr_idx -1]
2144+
if changed:
2145+
object_animates[obj_idx] = True
2146+
2147+
anims_strings = []
2148+
# write phase:
2149+
to_deg = 360 / math.tau
2150+
rot_fix = np.array((to_deg, -to_deg, to_deg))
2151+
for idx, timeline in enumerate(object_timelines):
2152+
if not object_animates[idx]:
2153+
continue
2154+
log.error(f"writing obj:{idx}")
2155+
2156+
timeline_repr = ['''{
2157+
"actor": "''', anim_objs[idx][1], '",'
2158+
]
2159+
2160+
translations = np.empty((num_frames, 4), dtype=np.float32)
2161+
rotations = np.empty((num_frames, 4), dtype=np.float32)
2162+
scales = np.empty((num_frames, 4), dtype=np.float32)
2163+
translations[:, 0] = np.arange(frame_start, frame_end+1)
2164+
rotations[:, 0] = np.arange(frame_start, frame_end+1)
2165+
scales[:, 0] = np.arange(frame_start, frame_end+1)
2166+
2167+
for frame_idx, frame_mat in enumerate(timeline):
2168+
loc, rot, scale = frame_mat.decompose()
2169+
tx_slice = (frame_idx, slice(1,4))
2170+
translations[frame_idx, 1:4] = loc
2171+
rotations[frame_idx, 1:4] = rot_fix * rot.to_euler('XYZ')
2172+
scales[frame_idx, 1:4] = scale
2173+
2174+
trans_expression = ",".join(
2175+
'{"id":%d,"x":%f,"y":%f,"z":%f}'% tuple(v)
2176+
for v in translations
2177+
)
2178+
timeline_repr.extend(('"trans":[', trans_expression, '],'))
2179+
2180+
rot_expression = ",".join(
2181+
'{"id":%d,"x":%f,"y":%f,"z":%f}'% tuple(v)
2182+
for v in rotations
2183+
)
2184+
timeline_repr.extend(('"rot":[', rot_expression, '],'))
2185+
2186+
scale_expression = ",".join(
2187+
'{"id":%d,"x":%f,"y":%f,"z":%f}'% tuple(v)
2188+
for v in scales
2189+
)
2190+
timeline_repr.extend(('"scl":[', scale_expression, '],'))
2191+
2192+
timeline_repr.append('"type":"transform"}')
2193+
result = "".join(timeline_repr)
2194+
anims_strings.append(result)
2195+
2196+
if anims_strings:
2197+
output = ["""
2198+
{
2199+
"version": "0.1",
2200+
"fps": """,
2201+
str(context.scene.render.fps),
2202+
""",
2203+
"animations": [""",
2204+
",".join(anims_strings),
2205+
"]}"
2206+
]
2207+
2208+
output_text = "".join(output)
2209+
anims.append(output_text)
2210+
2211+
# cleanup
2212+
context.scene.frame_set(frame_at_export_time)
2213+
2214+
20752215
environment = collect_environment(context.scene.world)
20762216

20772217
log.info("Collecting materials")
@@ -2101,6 +2241,22 @@ def collect_and_save(context, args, save_path):
21012241
except FileExistsError as e:
21022242
pass
21032243

2244+
log.info("writing anims")
2245+
anim_nodes = []
2246+
for anim in anims:
2247+
2248+
filename = path.join(basedir, folder_name, "anim_new.json")
2249+
log.info("writing to file:%s" % filename)
2250+
with open(filename, 'w') as f:
2251+
f.write(output_text)
2252+
2253+
anim = Node("LevelSequence", {"name": "anim_new"})
2254+
anim.push(Node("File", {"path": f"{folder_name}/anim_new.json"}))
2255+
anim_nodes.append(anim)
2256+
2257+
2258+
2259+
21042260
log.info("writing meshes")
21052261
for mesh in datasmith_context["meshes"]:
21062262
mesh.save(basedir, folder_name)
@@ -2118,6 +2274,10 @@ def collect_and_save(context, args, save_path):
21182274
log.info("building XML tree")
21192275

21202276
n = get_file_header()
2277+
2278+
for anim in anim_nodes:
2279+
n.push(anim)
2280+
21212281
for obj in objects:
21222282
n.push(obj)
21232283

@@ -2157,7 +2317,7 @@ def collect_and_save(context, args, save_path):
21572317

21582318

21592319

2160-
def save(context,*, filepath, **kwargs):
2320+
def save(context, *, filepath, **kwargs):
21612321

21622322
handler = None
21632323
use_logging = bool(kwargs["use_logging"])

testing/test_datasmith_export.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
custom_args = {}
5151
custom_args["experimental_tex_mode"] = True
5252
custom_args["apply_modifiers"] = True
53+
custom_args["export_animations"] = True
5354
custom_args["prefer_custom_nodes"] = True
5455
custom_args["use_logging"] = True
5556
custom_args["use_profiling"] = False

0 commit comments

Comments
 (0)