@@ -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
18111838def 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" ])
0 commit comments