Advertisement
test34

Untitled

Apr 6th, 2025
380
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 78.11 KB | Help | 0 0
  1. #  Copyright (c) 2014 Tom Edwards [email protected]
  2. #
  3. # ##### BEGIN GPL LICENSE BLOCK #####
  4. #
  5. #  This program is free software; you can redistribute it and/or
  6. #  modify it under the terms of the GNU General Public License
  7. #  as published by the Free Software Foundation; either version 2
  8. #  of the License, or (at your option) any later version.
  9. #
  10. #  This program is distributed in the hope that it will be useful,
  11. #  but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. #  GNU General Public License for more details.
  14. #
  15. #  You should have received a copy of the GNU General Public License
  16. #  along with this program; if not, write to the Free Software Foundation,
  17. #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. #
  19. # ##### END GPL LICENSE BLOCK #####
  20.  
  21. import bpy, bmesh, subprocess, collections, re
  22. from bpy import ops
  23. from bpy.app.translations import pgettext
  24. from mathutils import Vector, Matrix
  25. from math import *
  26. from bpy.types import Collection
  27. from bpy.props import CollectionProperty, StringProperty, BoolProperty
  28.  
  29. from .utils import *
  30. from . import datamodel, ordered_set, flex
  31.  
  32. class SMD_OT_Compile(bpy.types.Operator, Logger):
  33.     bl_idname = "smd.compile_qc"
  34.     bl_label = get_id("qc_compile_title")
  35.     bl_description = get_id("qc_compile_tip")
  36.  
  37.     files : CollectionProperty(type=bpy.types.OperatorFileListElement)
  38.     directory : StringProperty(maxlen=1024, default="", subtype='FILE_PATH')
  39.  
  40.     filepath : StringProperty(name="File path", maxlen=1024, default="", subtype='FILE_PATH')
  41.    
  42.     filter_folder : BoolProperty(default=True, options={'HIDDEN'})
  43.     filter_glob : StringProperty(default="*.qc;*.qci", options={'HIDDEN'})
  44.    
  45.     def __init__(self, *args, **kwargs):
  46.         bpy.types.Operator.__init__(self, *args, **kwargs)
  47.         Logger.__init__(self)
  48.    
  49.     @classmethod
  50.     def poll(cls,context):
  51.         return State.gamePath is not None and State.compiler == Compiler.STUDIOMDL
  52.  
  53.     def invoke(self,context, event):
  54.         bpy.context.window_manager.fileselect_add(self)
  55.         return {'RUNNING_MODAL'}
  56.  
  57.     def execute(self,context):
  58.         multi_files = len([file for file in self.properties.files if file.name]) > 0
  59.         if not multi_files and not (self.properties.filepath == "*" or os.path.isfile(self.properties.filepath)):
  60.             self.report({'ERROR'},"No QC files selected for compile.")
  61.             return {'CANCELLED'}
  62.  
  63.         num = self.compileQCs([os.path.join(self.properties.directory,file.name) for file in self.properties.files] if multi_files else self.properties.filepath)
  64.         #if num > 1:
  65.         #   bpy.context.window_manager.progress_begin(0,1)
  66.         self.errorReport(get_id("qc_compile_complete",True).format(num,State.engineBranchTitle))
  67.         bpy.context.window_manager.progress_end()
  68.         return {'FINISHED'}
  69.    
  70.     @classmethod
  71.     def getQCs(cls, path : str = None) -> list:
  72.         import glob
  73.         ext = ".qc"
  74.         out = []
  75.         internal = False
  76.         if not path:
  77.             path = bpy.path.abspath(bpy.context.scene.vs.qc_path)
  78.             internal = True
  79.         for result in glob.glob(path):
  80.             if result.endswith(ext):
  81.                 out.append(result)
  82.  
  83.         if not internal and not len(out) and not path.endswith(ext):
  84.             out = cls.getQCs(path + ext)
  85.         return out
  86.    
  87.     def compileQCs(self,path=None):
  88.         scene = bpy.context.scene
  89.         print("\n")
  90.  
  91.         studiomdl_path = os.path.join(bpy.path.abspath(scene.vs.engine_path),"studiomdl.exe")
  92.  
  93.         if path == "*":
  94.             paths = SMD_OT_Compile.getQCs()
  95.         elif isinstance(path,str):
  96.             paths = [os.path.realpath(bpy.path.abspath(path))]
  97.         elif hasattr(path,"__getitem__"):
  98.             paths = path
  99.         else:
  100.             paths = SMD_OT_Compile.getQCs()
  101.         num_good_compiles = 0
  102.         num_qcs = len(paths)
  103.         if num_qcs == 0:
  104.             self.error(get_id("qc_compile_err_nofiles"))
  105.         elif not os.path.exists(studiomdl_path):
  106.             self.error(get_id("qc_compile_err_compiler", True).format(studiomdl_path) )
  107.         else:
  108.             i = 0
  109.             for qc in paths:
  110.                 bpy.context.window_manager.progress_update((i+1) / (num_qcs+1))
  111.                 # save any version of the file currently open in Blender
  112.                 qc_mangled = qc.lower().replace('\\','/')
  113.                 for candidate_area in bpy.context.screen.areas:
  114.                     if candidate_area.type == 'TEXT_EDITOR' and candidate_area.spaces[0].text and candidate_area.spaces[0].text.filepath.lower().replace('\\','/') == qc_mangled:
  115.                         oldType = bpy.context.area.type
  116.                         bpy.context.area.type = 'TEXT_EDITOR'
  117.                         bpy.context.area.spaces[0].text = candidate_area.spaces[0].text
  118.                         ops.text.save()
  119.                         bpy.context.area.type = oldType
  120.                         break #what a farce!
  121.                
  122.                 print( "Running studiomdl for \"{}\"...\n".format(os.path.basename(qc)) )
  123.                 studiomdl = subprocess.Popen([studiomdl_path, "-nop4", "-game", State.gamePath, qc])
  124.                 studiomdl.communicate()
  125.  
  126.                 if studiomdl.returncode == 0:
  127.                     num_good_compiles += 1
  128.                 else:
  129.                     self.error(get_id("qc_compile_err_unknown", True).format(os.path.basename(qc)))
  130.                 i+=1
  131.         return num_good_compiles
  132.  
  133. class SmdExporter(bpy.types.Operator, Logger):
  134.     bl_idname = "export_scene.smd"
  135.     bl_label = get_id("exporter_title")
  136.     bl_description = get_id("exporter_tip")
  137.    
  138.     collection : bpy.props.StringProperty(name=get_id("exporter_prop_group"),description=get_id("exporter_prop_group_tip"))
  139.     export_scene : bpy.props.BoolProperty(name=get_id("scene_export"),description=get_id("exporter_prop_scene_tip"),default=False)
  140.  
  141.     def __init__(self, *args, **kwargs):
  142.         bpy.types.Operator.__init__(self, *args, **kwargs)
  143.         Logger.__init__(self)
  144.  
  145.     @classmethod
  146.     def poll(cls,context):
  147.         return len(context.scene.vs.export_list)
  148.        
  149.     def invoke(self, context, event):
  150.         State.update_scene()
  151.         ops.wm.call_menu(name="SMD_MT_ExportChoice")
  152.         return {'PASS_THROUGH'}
  153.  
  154.     def execute(self, context):
  155.         #bpy.context.window_manager.progress_begin(0,1)
  156.        
  157.         # Misconfiguration?
  158.         if State.datamodelEncoding != 0 and context.scene.vs.export_format == 'DMX':
  159.             datamodel.check_support("binary",State.datamodelEncoding)
  160.             if State.datamodelEncoding < 3 and State.datamodelFormat > 11 and not context.scene.vs.use_kv2:
  161.                 self.report({'ERROR'},"DMX format \"Model {}\" requires DMX encoding \"Binary 3\" or later".format(State.datamodelFormat))
  162.                 return {'CANCELLED' }
  163.         if not context.scene.vs.export_path:
  164.             bpy.ops.wm.call_menu(name="SMD_MT_ConfigureScene")
  165.             return {'CANCELLED'}
  166.         if context.scene.vs.export_path.startswith("//") and not context.blend_data.filepath:
  167.             self.report({'ERROR'},get_id("exporter_err_relativeunsaved"))
  168.             return {'CANCELLED'}
  169.         if State.datamodelEncoding == 0 and context.scene.vs.export_format == 'DMX':
  170.             self.report({'ERROR'},get_id("exporter_err_dmxother"))
  171.             return {'CANCELLED'}
  172.        
  173.         # Don't create an undo level from edit mode
  174.         prev_mode = prev_hidden = None
  175.         if context.active_object:
  176.             if context.active_object.hide_viewport:
  177.                 prev_hidden = context.active_object.name
  178.                 context.active_object.hide_viewport = False
  179.             prev_mode = context.mode
  180.             if prev_mode.find("EDIT") != -1: prev_mode = 'EDIT'
  181.             elif prev_mode.find("PAINT") != -1: # FFS Blender!
  182.                 prev_mode = prev_mode.split('_')
  183.                 prev_mode.reverse()
  184.                 prev_mode = "_".join(prev_mode)
  185.             ops.object.mode_set(mode='OBJECT')
  186.        
  187.         State.update_scene()
  188.         self.bake_results = []
  189.         self.bone_ids = {}
  190.         self.materials_used = set()
  191.        
  192.         for ob in [ob for ob in bpy.context.scene.objects if ob.type == 'ARMATURE' and len(ob.vs.subdir) == 0]:
  193.             ob.vs.subdir = "anims"
  194.        
  195.         ops.ed.undo_push(message=self.bl_label)
  196.                
  197.         try:
  198.             context.tool_settings.use_keyframe_insert_auto = False
  199.             context.tool_settings.use_keyframe_insert_keyingset = False
  200.             context.preferences.edit.use_enter_edit_mode = False
  201.             State.unhook_events()
  202.             if context.scene.rigidbody_world:
  203.                 context.scene.frame_set(context.scene.rigidbody_world.point_cache.frame_start)
  204.            
  205.             # lots of operators only work on visible objects
  206.             for ob in context.scene.objects:
  207.                 ob.hide_viewport = False
  208.             # ensure that objects in all collections are accessible to operators
  209.             context.view_layer.layer_collection.exclude = False
  210.            
  211.             self.files_exported = self.attemptedExports = 0
  212.            
  213.             if self.export_scene:
  214.                 for id in [exportable.item for exportable in context.scene.vs.export_list]:
  215.                     if type(id) == Collection:
  216.                         if shouldExportGroup(id):
  217.                             self.exportId(context, id)
  218.                     elif id.vs.export:
  219.                         self.exportId(context, id)
  220.             else:
  221.                 if self.collection == "":
  222.                     for exportable in getSelectedExportables():
  223.                         if type(exportable.item) != Collection:
  224.                             self.exportId(context, exportable.item)
  225.                 else:
  226.                     collection = bpy.data.collections[self.collection]
  227.                     if collection.vs.mute: self.error(get_id("exporter_err_groupmuted", True).format(collection.name))
  228.                     elif not collection.objects: self.error(get_id("exporter_err_groupempty", True).format(collection.name))
  229.                     else: self.exportId(context, collection)
  230.            
  231.             num_good_compiles = None
  232.  
  233.             if self.attemptedExports == 0:
  234.                 self.report({'ERROR'},get_id("exporter_err_noexportables"))
  235.             elif context.scene.vs.qc_compile and context.scene.vs.qc_path:
  236.                 # ...and compile the QC
  237.                 if not SMD_OT_Compile.poll(context):
  238.                     print("Skipping QC compile step: context incorrect\n")
  239.                 else:
  240.                     num_good_compiles = SMD_OT_Compile.compileQCs(self) # hack, use self as the logger
  241.                     print("\n")
  242.            
  243.             if num_good_compiles != None:
  244.                 self.errorReport(get_id("exporter_report_qc", True).format(
  245.                         self.files_exported,
  246.                         self.elapsed_time(),
  247.                         num_good_compiles,
  248.                         State.engineBranchTitle,
  249.                         os.path.basename(State.gamePath)
  250.                         ))
  251.             else:
  252.                 self.errorReport(get_id("exporter_report", True).format(
  253.                     self.files_exported,
  254.                     self.elapsed_time()
  255.                     ))
  256.         finally:
  257.             # Clean everything up
  258.             ops.ed.undo_push(message=self.bl_label)
  259.             if bpy.app.debug_value <= 1: ops.ed.undo()
  260.            
  261.             if prev_mode:
  262.                 ops.object.mode_set(mode=prev_mode)
  263.             if prev_hidden:
  264.                 context.scene.objects[prev_hidden].hide_viewport = True
  265.             context.scene.update_tag()
  266.            
  267.             context.window_manager.progress_end()
  268.             State.hook_events()
  269.  
  270.         self.collection = ""
  271.         self.export_scene = False
  272.         return {'FINISHED'}
  273.  
  274.     def sanitiseFilename(self,name):
  275.         new_name = name
  276.         for badchar in "/?<>\\:*|\"":
  277.             new_name = new_name.replace(badchar,"_")
  278.         if new_name != name:
  279.             self.warning(get_id("exporter_warn_sanitised_filename",True).format(name,new_name))
  280.         return new_name
  281.    
  282.     def exportId(self,context,id):
  283.         self.attemptedExports += 1
  284.         self.armature = self.armature_src = None
  285.         bench = BenchMarker()
  286.        
  287.         subdir = id.vs.subdir
  288.        
  289.         print( "\nBlender Source Tools: exporting {}".format(id.name) )
  290.                
  291.         subdir = subdir.lstrip("/") # don't want //s here!
  292.        
  293.         path = os.path.join(bpy.path.abspath(context.scene.vs.export_path), subdir)
  294.         if not os.path.exists(path):
  295.             try:
  296.                 os.makedirs(path)
  297.             except Exception as err:
  298.                 self.error(get_id("exporter_err_makedirs", True).format(err))
  299.                 return
  300.  
  301.         if isinstance(id, bpy.types.Collection) and not any(ob.vs.export for ob in id.objects):
  302.             self.error(get_id("exporter_err_nogroupitems",True).format(id.name))
  303.             return
  304.        
  305.         if isinstance(id, bpy.types.Object) and id.type == 'ARMATURE':
  306.             ad = id.animation_data
  307.             if not ad: return # otherwise we create a folder but put nothing in it
  308.             if id.data.vs.action_selection == 'FILTERED':
  309.                 pass
  310.             elif ad.action:
  311.                 export_name = ad.action.name
  312.             elif ad.nla_tracks:
  313.                 export_name = id.name
  314.             else:
  315.                 self.error(get_id("exporter_err_arm_noanims",True).format(id.name))
  316.         else:
  317.             export_name = id.name      
  318.            
  319.         # hide all metaballs that we don't want
  320.         for meta in [ob for ob in context.scene.objects if ob.type == 'META' and (not ob.vs.export or (isinstance(id, Collection) and not ob.name in id.objects))]:
  321.             for element in meta.data.elements: element.hide = True
  322.  
  323.         def find_basis_metaball(id):
  324.             basis_ns = id.name.rsplit(".")
  325.             if len(basis_ns) == 1: return id
  326.  
  327.             basis = id
  328.             for meta in [ob for ob in bpy.data.objects if ob.type == 'META']:
  329.                 ns = meta.name.rsplit(".")
  330.  
  331.                 if ns[0] != basis_ns[0]:
  332.                     continue
  333.                 if len(ns) == 1:
  334.                     basis = meta
  335.                     break
  336.  
  337.                 try:
  338.                     if int(ns[1]) < int(basis_ns[1]):
  339.                         basis = meta
  340.                         basis_ns = ns
  341.                 except ValueError:
  342.                     pass
  343.             return basis
  344.        
  345.         bake_results = []
  346.         baked_metaballs = []
  347.        
  348.         bench.report("setup")
  349.        
  350.         if bench.quiet: print("- Baking...")
  351.  
  352.         if type(id) == Collection:
  353.             group_vertex_maps = valvesource_vertex_maps(id)
  354.             for i, ob in enumerate([ob for ob in id.objects if ob.vs.export and ob.session_uid in State.exportableObjects]):
  355.                 bpy.context.window_manager.progress_update(i / len(id.objects))
  356.                 if ob.type == 'META':
  357.                     ob = find_basis_metaball(ob)
  358.                     if ob in baked_metaballs: continue
  359.                     else: baked_metaballs.append(ob)
  360.                        
  361.                 bake = self.bakeObj(ob)
  362.                 for vertex_map_name in group_vertex_maps:
  363.                     if not vertex_map_name in bake.object.data.vertex_colors:
  364.                         vertex_map = bake.object.data.vertex_colors.new(vertex_map_name)
  365.                         vertex_map.data.foreach_set("color",[1.0] * 4)
  366.  
  367.                 if bake:
  368.                     bake_results.append(bake)
  369.             bench.report("Group bake", len(bake_results))
  370.         else:
  371.             if id.type == 'META':
  372.                 bake = self.bakeObj(find_basis_metaball(id))               
  373.                 bench.report("Metaball bake")
  374.             else:
  375.                 bake = self.bakeObj(id)
  376.                 bench.report("Standard bake")
  377.  
  378.             if bake:
  379.                     bake_results.append(bake)
  380.  
  381.         if not any(bake_results):
  382.             return
  383.        
  384.         if State.exportFormat == ExportFormat.DMX and hasShapes(id):
  385.             self.flex_controller_mode = id.vs.flex_controller_mode
  386.             self.flex_controller_source = id.vs.flex_controller_source
  387.  
  388.         bpy.context.view_layer.objects.active = bake_results[0].object
  389.         bpy.ops.object.mode_set(mode='OBJECT')
  390.         mesh_bakes = [bake for bake in bake_results if bake.object.type == 'MESH']
  391.        
  392.         skip_vca = False
  393.         if isinstance(id, Collection) and len(id.vs.vertex_animations) and len(id.objects) > 1:
  394.             if len(mesh_bakes) > len([bake for bake in bake_results if (type(bake.envelope) is str and bake.envelope == bake_results[0].envelope) or bake.envelope is None]):
  395.                 self.error(get_id("exporter_err_unmergable",True).format(id.name))
  396.                 skip_vca = True
  397.             elif not id.vs.automerge:
  398.                 id.vs.automerge = True
  399.  
  400.         for va in id.vs.vertex_animations:
  401.             if skip_vca: break
  402.  
  403.             if State.exportFormat == ExportFormat.DMX:
  404.                 va.name = va.name.replace("_","-")
  405.  
  406.             vca = bake_results[0].vertex_animations[va.name] # only the first bake result will ever have a vertex animation defined
  407.             vca.export_sequence = va.export_sequence
  408.             vca.num_frames = va.end - va.start
  409.             two_percent = vca.num_frames * len(bake_results) / 50
  410.             print("- Generating vertex animation \"{}\"".format(va.name))
  411.             anim_bench = BenchMarker(1,va.name)
  412.            
  413.             for f in range(va.start,va.end):
  414.                 bpy.context.scene.frame_set(f)
  415.                 bpy.ops.object.select_all(action='DESELECT')
  416.                 depsgraph = bpy.context.evaluated_depsgraph_get()
  417.                 for bake in mesh_bakes: # create baked snapshots of each vertex animation frame
  418.                     bake.fob = bpy.data.objects.new("{}-{}".format(va.name,f), bpy.data.meshes.new_from_object((bake.src.evaluated_get(depsgraph))))
  419.                     bake.fob.matrix_world = bake.src.matrix_world
  420.                     bpy.context.scene.collection.objects.link(bake.fob)
  421.                     bpy.context.view_layer.objects.active = bake.fob
  422.                     bake.fob.select_set(True)
  423.  
  424.                     top_parent = self.getTopParent(bake.src)
  425.                     if top_parent:
  426.                         bake.fob.location -= top_parent.location
  427.                    
  428.                     if context.scene.rigidbody_world:
  429.                         # Blender 2.71 bug: https://developer.blender.org/T41388
  430.                         prev_rbw = bpy.context.scene.rigidbody_world.enabled
  431.                         bpy.context.scene.rigidbody_world.enabled = False
  432.  
  433.                     bpy.ops.object.transform_apply(location=True,scale=True,rotation=True)
  434.                
  435.                     if context.scene.rigidbody_world:
  436.                         bpy.context.scene.rigidbody_world.enabled = prev_rbw
  437.  
  438.                 if bpy.context.selected_objects and State.exportFormat == ExportFormat.SMD:
  439.                     bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
  440.                     ops.object.join()
  441.                
  442.                 vca.append(bpy.context.active_object if len(bpy.context.selected_objects) == 1 else bpy.context.selected_objects)
  443.                 anim_bench.report("bake")
  444.                
  445.                 if len(bpy.context.selected_objects) != 1:
  446.                     for bake in mesh_bakes:
  447.                         bpy.context.scene.collection.objects.unlink(bake.fob)
  448.                         del bake.fob
  449.                
  450.                 anim_bench.report("record")
  451.  
  452.                 if two_percent and len(vca) / len(bake_results) % two_percent == 0:
  453.                     print(".", debug_only=True, newline=False)
  454.                     bpy.context.window_manager.progress_update(len(vca) / vca.num_frames)
  455.  
  456.             bench.report("\n" + va.name)
  457.             bpy.context.view_layer.objects.active = bake_results[0].src
  458.  
  459.         if isinstance(id, Collection) and State.exportFormat == ExportFormat.DMX and id.vs.automerge:
  460.             bone_parents = collections.defaultdict(list)
  461.             scene_obs = bpy.context.scene.collection.objects
  462.             view_obs = bpy.context.view_layer.objects
  463.             for bake in [bake for bake in bake_results if type(bake.envelope) is str or bake.envelope is None]:
  464.                 bone_parents[bake.envelope].append(bake)
  465.                
  466.             for bp, parts in bone_parents.items():
  467.                 if len(parts) <= 1: continue
  468.                 shape_names = set()
  469.                 for key in [key for part in parts for key in part.shapes.keys()]:
  470.                     shape_names.add(key)
  471.                    
  472.                 ops.object.select_all(action='DESELECT')
  473.                 for part in parts:
  474.                     ob = part.object.copy()
  475.                     ob.data = ob.data.copy()
  476.                     ob.data.uv_layers.active.name = "__dmx_uv__"
  477.                     scene_obs.link(ob)
  478.                     ob.select_set(True)
  479.                     view_obs.active = ob
  480.                     bake_results.remove(part)
  481.                    
  482.                 bpy.ops.object.join()
  483.                 joined = self.BakeResult(bp + "_meshes" if bp else "loose_meshes")
  484.                 joined.object = bpy.context.active_object
  485.                 joined.object.name = joined.object.data.name = joined.name
  486.                 joined.envelope = bp
  487.  
  488.                 if parts[0].vertex_animations:
  489.                     for src_name,src_vca in parts[0].vertex_animations.items():
  490.                         vca = joined.vertex_animations[src_name] = self.BakedVertexAnimation()
  491.                         vca.bone_id = src_vca.bone_id
  492.                         vca.export_sequence = src_vca.export_sequence
  493.                         vca.num_frames = src_vca.num_frames
  494.  
  495.                         for i,frame in enumerate(src_vca):
  496.                             bpy.ops.object.select_all(action='DESELECT')
  497.                             frame.reverse()
  498.                             for ob in frame:
  499.                                 scene_obs.link(ob)
  500.                                 ob.select_set(True)
  501.                             bpy.context.view_layer.objects.active = frame[0]
  502.                             bpy.ops.object.join()
  503.                             bpy.context.active_object.name = "{}-{}".format(src_name,i)
  504.                             bpy.ops.object.transform_apply(location=True,scale=True,rotation=True)
  505.                             vca.append(bpy.context.active_object)
  506.                             scene_obs.unlink(bpy.context.active_object)
  507.                
  508.                 bake_results.append(joined)
  509.                    
  510.                 for shape_name in shape_names:
  511.                     ops.object.select_all(action='DESELECT')
  512.                        
  513.                     for part in parts:
  514.                         mesh = part.shapes[shape_name] if shape_name in part.shapes else part.object.data
  515.                         ob = bpy.data.objects.new(name="{} -> {}".format(part.name,shape_name),object_data = mesh.copy())
  516.                         scene_obs.link(ob)
  517.                         ob.matrix_local = part.matrix
  518.                         ob.select_set(True)
  519.                         view_obs.active = ob
  520.                        
  521.                     bpy.ops.object.join()
  522.                     joined.shapes[shape_name] = bpy.context.active_object.data
  523.                     bpy.context.active_object.data.name = "{} -> {}".format(joined.object.name,shape_name)
  524.                        
  525.                     scene_obs.unlink(ob)
  526.                     bpy.data.objects.remove(ob)
  527.                     del ob
  528.                        
  529.                 view_obs.active = joined.object
  530.             bench.report("Mech merge")
  531.  
  532.         for result in bake_results:
  533.             if result.armature:
  534.                 if not self.armature:
  535.                     self.armature = result.armature.object
  536.                     self.armature_src = result.armature.src
  537.                 elif self.armature != result.armature.object:
  538.                     self.warning(get_id("exporter_warn_multiarmature"))
  539.  
  540.         if self.armature_src:
  541.             if list(self.armature_src.scale).count(self.armature_src.scale[0]) != 3:
  542.                 self.warning(get_id("exporter_err_arm_nonuniform",True).format(self.armature_src.name))
  543.             if not self.armature:
  544.                 self.armature = self.bakeObj(self.armature_src).object
  545.             exporting_armature = isinstance(id, bpy.types.Object) and id.type == 'ARMATURE'
  546.             self.exportable_bones = list([self.armature.pose.bones[edit_bone.name] for edit_bone in self.armature.data.bones if (exporting_armature or edit_bone.use_deform)])
  547.             skipped_bones = len(self.armature.pose.bones) - len(self.exportable_bones)
  548.             if skipped_bones:
  549.                 print("- Skipping {} non-deforming bones".format(skipped_bones))
  550.  
  551.         write_func = self.writeDMX if State.exportFormat == ExportFormat.DMX else self.writeSMD
  552.         bench.report("Post Bake")
  553.  
  554.         if isinstance(id, bpy.types.Object) and id.type == 'ARMATURE' and id.data.vs.action_selection == 'FILTERED':
  555.             for action in actionsForFilter(id.vs.action_filter):
  556.                 bake_results[0].object.animation_data.action = action
  557.                 self.files_exported += write_func(id, bake_results, self.sanitiseFilename(action.name), path)
  558.                 bench.report(write_func.__name__)
  559.         else:
  560.             self.files_exported += write_func(id, bake_results, self.sanitiseFilename(export_name), path)
  561.             bench.report(write_func.__name__)
  562.        
  563.         # Source doesn't handle Unicode characters in models. Detect any unicode strings and warn the user about them.
  564.         unicode_tested = set()
  565.         def test_for_unicode(name, id, display_type):
  566.             if id in unicode_tested: return;
  567.             unicode_tested.add(id)
  568.  
  569.             try:
  570.                 name.encode('ascii')
  571.             except UnicodeEncodeError:
  572.                 self.warning(get_id("exporter_warn_unicode", format_string=True).format(pgettext(display_type), name))
  573.  
  574.         # Meanwhile, Source 2 wants only lowercase characters, digits, and underscore in model names
  575.         if State.compiler > Compiler.STUDIOMDL or State.datamodelFormat >= 22:
  576.             if re.match(r'[^a-z0-9_]', id.name):
  577.                 self.warning(get_id("exporter_warn_source2names", format_string=True).format(id.name))
  578.  
  579.         for bake in bake_results:
  580.             test_for_unicode(bake.name, bake, type(bake.src).__name__)
  581.             for shape_name, shape_id in bake.shapes.items():
  582.                 test_for_unicode(shape_name, shape_id, "Shape Key")
  583.             if hasattr(bake.object,"objects"):
  584.                 for ob in bake.object.objects:
  585.                     test_for_unicode(ob.name, ob, ob.type.capitalize())
  586.         for mat in self.materials_used:
  587.             test_for_unicode(mat[0], mat[1], type(mat[1]).__name__)
  588.  
  589.        
  590.     def getWeightmap(self,bake_result):
  591.         out = []
  592.         amod = bake_result.envelope
  593.         ob = bake_result.object
  594.         if not amod or not isinstance(amod, bpy.types.ArmatureModifier): return out
  595.        
  596.         amod_vg = ob.vertex_groups.get(amod.vertex_group)
  597.  
  598.         try:
  599.             amod_ob = next((bake.object for bake in self.bake_results if bake.src == amod.object))
  600.         except StopIteration as e:
  601.             raise ValueError("Armature for exportable \"{}\" was not baked".format(bake_result.name)) from e
  602.        
  603.         model_mat = amod_ob.matrix_world.inverted() @ ob.matrix_world
  604.  
  605.         num_verts = len(ob.data.vertices)
  606.         for v in ob.data.vertices:
  607.             weights = []
  608.             total_weight = 0
  609.             if len(out) % 50 == 0: bpy.context.window_manager.progress_update(len(out) / num_verts)
  610.            
  611.             if amod.use_vertex_groups:
  612.                 for v_group in v.groups:
  613.                     if v_group.group < len(ob.vertex_groups):
  614.                         ob_group = ob.vertex_groups[v_group.group]
  615.                         group_name = ob_group.name
  616.                         group_weight = v_group.weight                  
  617.                     else:
  618.                         continue # Vertex group might not exist on object if it's re-using a datablock             
  619.  
  620.                     bone = amod_ob.pose.bones.get(group_name)
  621.                     if bone and bone in self.exportable_bones:
  622.                         weights.append([ self.bone_ids[bone.name], group_weight ])
  623.                         total_weight += group_weight           
  624.                    
  625.             if amod.use_bone_envelopes and total_weight == 0: # vertex groups completely override envelopes
  626.                 for pose_bone in [pb for pb in amod_ob.pose.bones if pb in self.exportable_bones]:
  627.                     weight = pose_bone.bone.envelope_weight * pose_bone.evaluate_envelope( model_mat @ v.co )
  628.                     if weight:
  629.                         weights.append([ self.bone_ids[pose_bone.name], weight ])
  630.                         total_weight += weight
  631.                
  632.             # normalise weights, like Blender does. Otherwise Studiomdl puts anything left over onto the root bone.
  633.             if total_weight not in [0,1]:
  634.                 for link in weights:
  635.                     link[1] *= 1/total_weight
  636.            
  637.             # apply armature modifier vertex group
  638.             if amod_vg and total_weight > 0:
  639.                 amod_vg_weight = 0
  640.                 for v_group in v.groups:
  641.                     if v_group.group == amod_vg.index:
  642.                         amod_vg_weight = v_group.weight
  643.                         break
  644.                 if amod.invert_vertex_group:
  645.                     amod_vg_weight = 1 - amod_vg_weight
  646.                 for link in weights:
  647.                     link[1] *= amod_vg_weight
  648.  
  649.             out.append(weights)
  650.         return out
  651.        
  652.     def GetMaterialName(self, ob, material_index):
  653.         mat_name = None
  654.         mat_id = None
  655.         if len(ob.material_slots) > material_index:
  656.             mat_id = ob.material_slots[material_index].material
  657.             if mat_id:
  658.                 mat_name = mat_id.name
  659.         if mat_name:
  660.             self.materials_used.add((mat_name,mat_id))
  661.             return mat_name, True
  662.         else:
  663.             return "no_material", ob.display_type != 'TEXTURED' # assume it's a collision mesh if it's not textured
  664.  
  665.     def getTopParent(self,id):
  666.         top_parent = id
  667.         while top_parent.parent:
  668.             top_parent = top_parent.parent
  669.         return top_parent
  670.  
  671.     def getEvaluatedPoseBones(self):
  672.         depsgraph = bpy.context.evaluated_depsgraph_get()
  673.         evaluated_armature = self.armature.evaluated_get(depsgraph)
  674.  
  675.         return [evaluated_armature.pose.bones[bone.name] for bone in self.exportable_bones]
  676.  
  677.     class BakedVertexAnimation(list):
  678.         def __init__(self):
  679.             super().__init__()
  680.             self.export_sequence = False
  681.             self.bone_id = -1
  682.             self.num_frames = 0
  683.  
  684.     class VertexAnimationKey():
  685.         def __init__(self,vert_index,co,norm):
  686.             self.vert_index = vert_index
  687.             self.co = co
  688.             self.norm = norm
  689.  
  690.     class BakeResult:      
  691.         def __init__(self,name):
  692.             self.name = name
  693.             self.object = None
  694.             self.matrix = Matrix()
  695.             self.envelope = None
  696.             self.bone_parent_matrix = None
  697.             self.src = None
  698.             self.armature = None
  699.             self.balance_vg = None
  700.             self.shapes = collections.OrderedDict()
  701.             self.vertex_animations = collections.defaultdict(SmdExporter.BakedVertexAnimation)
  702.            
  703.     # Creates a mesh with object transformations and modifiers applied
  704.     def bakeObj(self,id, generate_uvs = True):
  705.         for bake in (bake for bake in self.bake_results if bake.src == id or bake.object == id):
  706.             return bake
  707.        
  708.         result = self.BakeResult(id.name)
  709.         result.src = id
  710.         self.bake_results.append(result)
  711.  
  712.         try:
  713.             select_only(id)
  714.         except RuntimeError:
  715.             self.warning(get_id("exporter_err_hidden", True).format(id.name))
  716.             return
  717.  
  718.         should_triangulate = State.exportFormat == ExportFormat.SMD or id.vs.triangulate
  719.  
  720.         def triangulate():
  721.             ops.object.mode_set(mode='EDIT')
  722.             ops.mesh.select_all(action='SELECT')
  723.             ops.mesh.quads_convert_to_tris(quad_method='FIXED')
  724.             ops.object.mode_set(mode='OBJECT')
  725.                
  726.         duplis = []
  727.         if id.instance_type != 'NONE':
  728.             bpy.ops.object.duplicates_make_real()
  729.             id.select_set(False)
  730.             if bpy.context.selected_objects:
  731.                 bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
  732.                 bpy.ops.object.join()
  733.                 duplis = bpy.context.active_object
  734.                 duplis.parent = id
  735.                 duplis = self.bakeObj(duplis, generate_uvs = False).object
  736.                 if should_triangulate: triangulate()       
  737.             elif id.type not in exportable_types:
  738.                 return
  739.             else:
  740.                 duplis = None
  741.  
  742.         if id.type != 'META': # eek, what about lib data?
  743.             id = id.copy()
  744.             bpy.context.scene.collection.objects.link(id)
  745.         if id.data:
  746.             id.data = id.data.copy()
  747.        
  748.         if bpy.context.active_object:
  749.             ops.object.mode_set(mode='OBJECT')
  750.         select_only(id)
  751.                
  752.         if hasShapes(id):
  753.             id.active_shape_key_index = 0
  754.  
  755.         top_parent = self.getTopParent(id) # record this before changing hierarchies!
  756.        
  757.         def captureBoneParent(armature, boneName):
  758.             result.envelope = boneName
  759.             result.armature = self.bakeObj(armature)
  760.             select_only(id)
  761.  
  762.             # Objects with bone parents are not updated in sync with depsgraph evaluation (as of Blender 3.0.1). So capture the correct matrix before we start to mess with them.
  763.             # Furthemore, Blender's bone transforms are inconsistent with object transforms:
  764.             # - A bone's matrix value is local to the armature, NOT the bone's parent
  765.             # - Object bone parent matricies are calculated from the head of the bone, NOT the tail (even though the tail defines the bone's location in pose mode!)
  766.             # - Bones are Y up, NOT Z up like everything else in Blender, and this affects their children's transforms
  767.             # To avoid this mess, we can use the bone and object world transforms to calculate a sane local matrix
  768.             result.bone_parent_matrix = armature.pose.bones[boneName].matrix.inverted() @ armature.matrix_world.inverted() @ id.matrix_world
  769.  
  770.         cur = id
  771.         while cur:
  772.             if cur.parent_bone and cur.parent_type == 'BONE' and not result.envelope:
  773.                 captureBoneParent(cur.parent, cur.parent_bone)
  774.             for con in [con for con in cur.constraints if not con.mute]:
  775.                 if con.type in ['CHILD_OF','COPY_TRANSFORMS'] and con.target and con.target.type == 'ARMATURE' and con.subtarget:
  776.                     if not result.envelope:
  777.                         captureBoneParent(con.target, con.subtarget)
  778.                     else:
  779.                         self.warning(get_id("exporter_err_dupeenv_con",True).format(con.name,cur.name))
  780.             if result.envelope:
  781.                 break
  782.             cur = cur.parent
  783.         del cur
  784.  
  785.         if id.type == 'MESH':
  786.             ops.object.mode_set(mode='EDIT')
  787.             ops.mesh.reveal()
  788.            
  789.             if id.matrix_world.is_negative:
  790.                 ops.mesh.select_all(action='SELECT')
  791.                 ops.mesh.flip_normals()
  792.  
  793.             ops.mesh.select_all(action="DESELECT")
  794.             ops.object.mode_set(mode='OBJECT')
  795.        
  796.         ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
  797.         id.matrix_world = Matrix.Translation(top_parent.location).inverted() @ getUpAxisMat(bpy.context.scene.vs.up_axis).inverted() @ id.matrix_world
  798.        
  799.         if id.type == 'ARMATURE':
  800.             for posebone in id.pose.bones: posebone.matrix_basis.identity()
  801.             if self.armature and self.armature != id:
  802.                 self.warning(get_id("exporter_warn_multiarmature"))
  803.             result.armature = result
  804.             result.object = id
  805.             return result
  806.        
  807.         if id.type == 'CURVE':
  808.             id.data.dimensions = '3D'
  809.        
  810.         for con in [con for con in id.constraints if not con.mute]:
  811.             con.mute = True
  812.        
  813.         solidify_fill_rim = None
  814.         shapes_invalid = False
  815.         for mod in id.modifiers:
  816.             if mod.type == 'ARMATURE' and mod.object:
  817.                 if result.envelope and any(br for br in self.bake_results if br.envelope != mod.object):
  818.                     self.warning(get_id("exporter_err_dupeenv_arm",True).format(mod.name,id.name))
  819.                 else:
  820.                     result.armature = self.bakeObj(mod.object)
  821.                     result.envelope = mod
  822.                     select_only(id)
  823.                 mod.show_viewport = False
  824.             elif mod.type == 'SOLIDIFY' and not solidify_fill_rim:
  825.                 solidify_fill_rim = mod.use_rim
  826.             elif hasShapes(id) and mod.type == 'DECIMATE' and mod.decimate_type != 'UNSUBDIV':
  827.                 self.error(get_id("exporter_err_shapes_decimate", True).format(id.name,mod.decimate_type))
  828.                 shapes_invalid = True
  829.         ops.object.mode_set(mode='OBJECT')
  830.        
  831.         depsgraph = bpy.context.evaluated_depsgraph_get()
  832.        
  833.         if id.type in exportable_types:
  834.             # Bake reference mesh
  835.             data = bpy.data.meshes.new_from_object(id.evaluated_get(depsgraph), preserve_all_data_layers=True, depsgraph=depsgraph)
  836.             data.name = id.name + "_baked"         
  837.        
  838.             def put_in_object(id, data, quiet=False):
  839.                 if bpy.context.view_layer.objects.active:
  840.                     ops.object.mode_set(mode='OBJECT')
  841.  
  842.                 ob = bpy.data.objects.new(name=id.name,object_data=data)
  843.                 ob.matrix_world = id.matrix_world
  844.  
  845.                 bpy.context.scene.collection.objects.link(ob)
  846.        
  847.                 select_only(ob)
  848.  
  849.                 exporting_smd = State.exportFormat == ExportFormat.SMD
  850.                 ops.object.transform_apply(scale=True, location=exporting_smd, rotation=exporting_smd)
  851.  
  852.                 if hasCurves(id):
  853.                     ops.object.mode_set(mode='EDIT')
  854.                     ops.mesh.select_all(action='SELECT')
  855.                     if id.data.vs.faces == 'BOTH':
  856.                         ops.mesh.duplicate()
  857.                         if solidify_fill_rim and not quiet:
  858.                             self.warning(get_id("exporter_err_solidifyinside", True).format(id.name))
  859.                     if id.data.vs.faces != 'FORWARD':
  860.                         ops.mesh.flip_normals()
  861.                     ops.object.mode_set(mode='OBJECT')
  862.  
  863.                 return ob
  864.  
  865.             baked = put_in_object(id,data)
  866.  
  867.             if should_triangulate: triangulate()
  868.  
  869.         if duplis:
  870.             if not id.type in exportable_types:
  871.                 id.select_set(False)
  872.                 bpy.context.view_layer.objects.active = duplis
  873.             duplis.select_set(True)
  874.             bpy.ops.object.join()
  875.             baked = bpy.context.active_object
  876.  
  877.         result.object = baked
  878.         data = baked.data
  879.  
  880.         if not data.polygons:
  881.             self.error(get_id("exporter_err_nopolys", True).format(result.name))
  882.             return
  883.        
  884.         result.matrix = baked.matrix_world
  885.                
  886.         if not shapes_invalid and hasShapes(id):
  887.             # calculate vert balance
  888.             if State.exportFormat == ExportFormat.DMX:
  889.                 if id.data.vs.flex_stereo_mode == 'VGROUP':
  890.                     if id.data.vs.flex_stereo_vg == "":
  891.                         self.warning(get_id("exporter_err_splitvgroup_undefined",True).format(id.name))
  892.                     else:
  893.                         result.balance_vg = baked.vertex_groups.get(id.data.vs.flex_stereo_vg)
  894.                         if not result.balance_vg:
  895.                             self.warning(get_id("exporter_err_splitvgroup_missing", True).format(id.data.vs.flex_stereo_vg,id.name))
  896.                 else:
  897.                     axis = axes_lookup[id.data.vs.flex_stereo_mode]
  898.                     balance_width = baked.dimensions[axis]  * ( 1 - (id.data.vs.flex_stereo_sharpness / 100) )
  899.                     result.balance_vg = baked.vertex_groups.new(name="__dmx_balance__")
  900.                     zeroes = []
  901.                     ones = []
  902.                     for vert in baked.data.vertices:
  903.                         if balance_width == 0:
  904.                             if vert.co[axis] > 0: ones.append(vert.index)
  905.                             else: zeroes.append(vert.index)
  906.                         else:
  907.                             balance = min(1,max(0, (-vert.co[axis] / balance_width / 2) + 0.5))
  908.                             if balance == 1: ones.append(vert.index)
  909.                             elif balance == 0: zeroes.append(vert.index)
  910.                             else: result.balance_vg.add([vert.index], balance, 'REPLACE')
  911.                     result.balance_vg.add(ones, 1, 'REPLACE')
  912.                     result.balance_vg.add(zeroes, 0, 'REPLACE')
  913.            
  914.             # bake shapes
  915.             id.show_only_shape_key = True
  916.             for i, shape in enumerate(id.data.shape_keys.key_blocks):
  917.                 if i == 0: continue
  918.                 id.active_shape_key_index = i
  919.                 depsgraph = bpy.context.evaluated_depsgraph_get()
  920.                 baked_shape = bpy.data.meshes.new_from_object(id.evaluated_get(depsgraph))
  921.                 baked_shape.name = "{} -> {}".format(id.name,shape.name)
  922.  
  923.                 shape_ob = put_in_object(id,baked_shape, quiet = True)
  924.  
  925.                 if duplis:
  926.                     select_only(shape_ob)
  927.                     duplis.select_set(True)
  928.                     bpy.ops.object.join()
  929.                     shape_ob = bpy.context.active_object
  930.  
  931.                 result.shapes[shape.name] = shape_ob.data
  932.  
  933.                 if should_triangulate:
  934.                     bpy.context.view_layer.objects.active = shape_ob
  935.                     triangulate()
  936.                
  937.                 bpy.context.scene.collection.objects.unlink(shape_ob)
  938.                 bpy.data.objects.remove(shape_ob)
  939.                 del shape_ob
  940.  
  941.         for mod in id.modifiers:
  942.             mod.show_viewport = False # mainly to disable physics modifiers
  943.  
  944.         bpy.context.view_layer.objects.active = baked
  945.         baked.select_set(True)
  946.  
  947.         # project a UV map
  948.         if generate_uvs and not baked.data.uv_layers:
  949.             ops.object.mode_set(mode='EDIT')
  950.             ops.mesh.select_all(action='SELECT')
  951.             if len(result.object.data.vertices) < 2000:
  952.                 result.object.data.uv_layers.new()
  953.                 ops.uv.smart_project()
  954.             else:
  955.                 ops.uv.unwrap()
  956.             ops.object.mode_set(mode='OBJECT')
  957.                
  958.         return result
  959.  
  960.     def openSMD(self,path,name,description):
  961.         full_path = os.path.realpath(os.path.join(path, name))
  962.  
  963.         try:
  964.             f = open(full_path, 'w',encoding='utf-8')
  965.         except Exception as err:
  966.             self.error(get_id("exporter_err_open", True).format(description, err))
  967.             return None
  968.        
  969.         f.write("version 1\n")
  970.         print("-",full_path)
  971.         return f
  972.  
  973.     def writeSMD(self, id, bake_results, name, filepath, filetype = 'smd'):
  974.         bench = BenchMarker(1,"SMD")
  975.         goldsrc = bpy.context.scene.vs.smd_format == "GOLDSOURCE"
  976.        
  977.         self.smd_file = self.openSMD(filepath,name + "." + filetype,filetype.upper())
  978.         if self.smd_file == None: return 0
  979.  
  980.         if State.compiler > Compiler.STUDIOMDL:
  981.             self.warning(get_id("exporter_warn_source2smdsupport"))
  982.  
  983.         # BONES
  984.         self.smd_file.write("nodes\n")
  985.         curID = 0
  986.         if not self.armature:
  987.             self.smd_file.write("0 \"root\" -1\n")
  988.             if filetype == 'smd': print("- No skeleton to export")
  989.         else:
  990.             if self.armature.data.vs.implicit_zero_bone:
  991.                 self.smd_file.write("0 \"{}\" -1\n".format(implicit_bone_name))
  992.                 curID += 1
  993.            
  994.             # Write to file
  995.             for bone in self.exportable_bones:
  996.                 parent = bone.parent
  997.                 while parent and not parent in self.exportable_bones:
  998.                     parent = parent.parent
  999.  
  1000.                 line = "{} ".format(curID)
  1001.                 self.bone_ids[bone.name] = curID
  1002.                 curID += 1
  1003.  
  1004.                 bone_name = bone.name
  1005.                 line += "\"" + bone_name + "\" "
  1006.  
  1007.                 if parent:
  1008.                     line += str(self.bone_ids[parent.name])
  1009.                 else:
  1010.                     line += "-1"
  1011.  
  1012.                 self.smd_file.write(line + "\n")
  1013.  
  1014.             num_bones = len(self.armature.data.bones)
  1015.             if filetype == 'smd': print("- Exported",num_bones,"bones")
  1016.            
  1017.             max_bones = 128
  1018.             if num_bones > max_bones:
  1019.                 self.warning(get_id("exporter_err_bonelimit", True).format(num_bones,max_bones))
  1020.  
  1021.         for vca in [vca for vca in bake_results[0].vertex_animations.items() if vca[1].export_sequence]:
  1022.             curID += 1
  1023.             vca[1].bone_id = curID
  1024.             self.smd_file.write("{} \"vcabone_{}\" -1\n".format(curID,vca[0]))
  1025.  
  1026.         self.smd_file.write("end\n")
  1027.        
  1028.         if filetype == 'smd':
  1029.             # ANIMATION
  1030.             self.smd_file.write("skeleton\n")
  1031.             if not self.armature:
  1032.                 self.smd_file.write("time 0\n0 0 0 0 0 0 0\nend\n")
  1033.             else:
  1034.                 # Get the working frame range
  1035.                 is_anim = len(bake_results) == 1 and bake_results[0].object.type == 'ARMATURE'
  1036.                 if is_anim:
  1037.                     ad = self.armature.animation_data
  1038.                     anim_len = animationLength(ad) + 1 # frame 0 is a frame too...
  1039.                     if anim_len == 1:
  1040.                         self.warning(get_id("exporter_err_noframes",True).format(self.armature_src.name))
  1041.                    
  1042.                     if ad.action and hasattr(ad.action,'fps'):
  1043.                         bpy.context.scene.render.fps = ad.action.fps
  1044.                         bpy.context.scene.render.fps_base = 1
  1045.                 else:
  1046.                     anim_len = 1
  1047.  
  1048.                 # remove any unkeyed poses, e.g. from other animations in this export operation.
  1049.                 for posebone in self.armature.pose.bones: posebone.matrix_basis.identity()
  1050.  
  1051.                 # Start writing out the animation
  1052.                 for i in range(anim_len):
  1053.                     bpy.context.window_manager.progress_update(i / anim_len)
  1054.                     self.smd_file.write("time {}\n".format(i))
  1055.                    
  1056.                     if self.armature.data.vs.implicit_zero_bone:
  1057.                         self.smd_file.write("0  0 0 0  0 0 0\n")
  1058.  
  1059.                     if is_anim:
  1060.                         bpy.context.scene.frame_set(i)
  1061.  
  1062.                     evaluated_bones = self.getEvaluatedPoseBones()
  1063.                     for posebone in evaluated_bones:
  1064.                         parent = posebone.parent
  1065.                         while parent and not parent in evaluated_bones:
  1066.                             parent = parent.parent
  1067.                
  1068.                         # Get the bone's Matrix from the current pose
  1069.                         PoseMatrix = posebone.matrix
  1070.                         if self.armature.data.vs.legacy_rotation:
  1071.                             PoseMatrix @= mat_BlenderToSMD
  1072.                         if parent:
  1073.                             parentMat = parent.matrix
  1074.                             if self.armature.data.vs.legacy_rotation: parentMat @= mat_BlenderToSMD
  1075.                             PoseMatrix = parentMat.inverted() @ PoseMatrix
  1076.                         else:
  1077.                             PoseMatrix = self.armature.matrix_world @ PoseMatrix
  1078.                
  1079.                         self.smd_file.write("{}  {}  {}\n".format(self.bone_ids[posebone.name], getSmdVec(PoseMatrix.to_translation()), getSmdVec(PoseMatrix.to_euler())))
  1080.  
  1081.                 self.smd_file.write("end\n")
  1082.  
  1083.                 ops.object.mode_set(mode='OBJECT')
  1084.                
  1085.                 print("- Exported {} frames{}".format(anim_len," (legacy rotation)" if self.armature.data.vs.legacy_rotation else ""))
  1086.  
  1087.             # POLYGONS
  1088.             done_header = False
  1089.             for bake in [bake for bake in bake_results if bake.object.type != 'ARMATURE']:
  1090.                 if not done_header:
  1091.                     self.smd_file.write("triangles\n")
  1092.                     done_header = True
  1093.                 face_index = 0
  1094.                 ob = bake.object
  1095.                 data = ob.data
  1096.  
  1097.                 uv_loop = data.uv_layers.active.data
  1098.  
  1099.                 weights = self.getWeightmap(bake)
  1100.                
  1101.                 ob_weight_str = None
  1102.                 if type(bake.envelope) == str and bake.envelope in self.bone_ids:
  1103.                     ob_weight_str = (" 1 {} 1" if not goldsrc else "{}").format(self.bone_ids[bake.envelope])
  1104.                 elif not weights:
  1105.                     ob_weight_str = " 0" if not goldsrc else "0"
  1106.                
  1107.                 bad_face_mats = 0
  1108.                 multi_weight_verts = set() # only relevant for GoldSrc exports
  1109.                 p = 0
  1110.                 for poly in data.polygons:
  1111.                     if p % 10 == 0: bpy.context.window_manager.progress_update(p / len(data.polygons))
  1112.                     mat_name, mat_success = self.GetMaterialName(ob, poly.material_index)
  1113.                     if not mat_success:
  1114.                         bad_face_mats += 1
  1115.                    
  1116.                     self.smd_file.write(mat_name + "\n")
  1117.                    
  1118.                     for loop in [data.loops[l] for l in poly.loop_indices]:
  1119.                         # Vertex locations, normal directions
  1120.                         v = data.vertices[loop.vertex_index]
  1121.                         pos_norm = "  {}  {}  ".format(getSmdVec(v.co),getSmdVec(loop.normal))
  1122.  
  1123.                         # UVs
  1124.                         uv = " ".join([getSmdFloat(j) for j in uv_loop[loop.index].uv])
  1125.  
  1126.                         if not goldsrc:
  1127.                             # Weightmaps
  1128.                             weight_string = ""
  1129.                             if ob_weight_str:
  1130.                                 weight_string = ob_weight_str
  1131.                             else:
  1132.                                 valid_weights = 0
  1133.                                 for link in [link for link in weights[v.index] if link[1] > 0]:
  1134.                                     weight_string += " {} {}".format(link[0], getSmdFloat(link[1]))
  1135.                                     valid_weights += 1
  1136.                                 weight_string = " {}{}".format(valid_weights,weight_string)
  1137.  
  1138.                             self.smd_file.write("0" + pos_norm + uv + weight_string + "\n") # write to file
  1139.  
  1140.                         else:
  1141.                             if ob_weight_str:
  1142.                                 weight_string = ob_weight_str
  1143.                             else:
  1144.                                 goldsrc_weights = [link for link in weights[v.index] if link[1] > 0]
  1145.                                 if len(goldsrc_weights) == 0:
  1146.                                     weight_string = "0"
  1147.                                 else:
  1148.                                     if len(goldsrc_weights) > 1:
  1149.                                         multi_weight_verts.add(v)
  1150.                                     weight_string = str(goldsrc_weights[0][0])
  1151.                             self.smd_file.write(weight_string + pos_norm + uv + "\n") # write to file
  1152.  
  1153.                     face_index += 1
  1154.  
  1155.                 if goldsrc and multi_weight_verts:
  1156.                     self.warning(get_id("exporterr_goldsrc_multiweights", format_string=True).format(len(multi_weight_verts), bake.src.data.name))
  1157.                 if bad_face_mats:
  1158.                     self.warning(get_id("exporter_err_facesnotex_ormat").format(bad_face_mats,bake.src.data.name))
  1159.                
  1160.                 print("- Exported",face_index,"polys")
  1161.                
  1162.                 print("- Exported {} materials".format(len(self.materials_used)))
  1163.                 for mat in self.materials_used:
  1164.                     print("   " + mat[0])
  1165.            
  1166.             if done_header:
  1167.                 self.smd_file.write("end\n")
  1168.         elif filetype == 'vta':
  1169.             self.smd_file.write("skeleton\n")
  1170.            
  1171.             def _writeTime(time, shape_name = None):
  1172.                 self.smd_file.write( "time {}{}\n".format(time, " # {}".format(shape_name) if shape_name else ""))
  1173.            
  1174.             shape_names = ordered_set.OrderedSet()
  1175.             for bake in [bake for bake in bake_results if bake.object.type != 'ARMATURE']:
  1176.                 for shape_name in bake.shapes.keys():
  1177.                     shape_names.add(shape_name)
  1178.                
  1179.             _writeTime(0)
  1180.             for i, shape_name in enumerate(shape_names):
  1181.                 _writeTime(i+1, shape_name)
  1182.             self.smd_file.write("end\n")
  1183.  
  1184.             self.smd_file.write("vertexanimation\n")
  1185.            
  1186.             total_verts = 0
  1187.             vert_id = 0
  1188.            
  1189.             def _makeVertLine(i,co,norm):
  1190.                 return "{} {} {}\n".format(i, getSmdVec(co), getSmdVec(norm))
  1191.            
  1192.             _writeTime(0)
  1193.             for bake in [bake for bake in bake_results if bake.object.type != 'ARMATURE']:
  1194.                 bake.offset = vert_id
  1195.                 verts = bake.object.data.vertices
  1196.                 for loop in [bake.object.data.loops[l] for poly in bake.object.data.polygons for l in poly.loop_indices]:
  1197.                     self.smd_file.write(_makeVertLine(vert_id,verts[loop.vertex_index].co,loop.normal))
  1198.                     vert_id += 1
  1199.            
  1200.             for i, shape_name in enumerate(shape_names):
  1201.                 i += 1
  1202.                 bpy.context.window_manager.progress_update(i / len(shape_names))
  1203.                 _writeTime(i,shape_name)
  1204.                 for bake in [bake for bake in bake_results if bake.object.type != 'ARMATURE']:
  1205.                     shape = bake.shapes.get(shape_name)
  1206.                     if not shape: continue
  1207.                    
  1208.                     vert_index = bake.offset
  1209.                     mesh_verts = bake.object.data.vertices
  1210.                     shape_verts = shape.vertices
  1211.  
  1212.                     for mesh_loop in [bake.object.data.loops[l] for poly in bake.object.data.polygons for l in poly.loop_indices]:
  1213.                         shape_vert = shape_verts[mesh_loop.vertex_index]
  1214.                         shape_loop = shape.loops[mesh_loop.index]
  1215.                         mesh_vert = mesh_verts[mesh_loop.vertex_index]
  1216.                         diff_vec = shape_vert.co - mesh_vert.co
  1217.                         if diff_vec > epsilon or shape_loop.normal - mesh_loop.normal > epsilon:
  1218.                             self.smd_file.write(_makeVertLine(vert_index,shape_vert.co,shape_loop.normal))
  1219.                             total_verts += 1
  1220.                         vert_index += 1
  1221.                
  1222.             self.smd_file.write("end\n")
  1223.             print("- Exported {} flex shapes ({} verts)".format(i,total_verts))
  1224.  
  1225.         self.smd_file.close()
  1226.  
  1227.        
  1228.         if bench.quiet:
  1229.             print("- {} export took".format(filetype.upper()) ,bench.total(),"\n")
  1230.  
  1231.         written = 1
  1232.         if filetype == 'smd':
  1233.             for bake in [bake for bake in bake_results if bake.shapes]:
  1234.                 written += self.writeSMD(id,bake_results,name,filepath,filetype='vta')
  1235.             for name,vca in bake_results[0].vertex_animations.items():
  1236.                 written += self.writeVCA(name,vca,filepath)
  1237.                 if vca.export_sequence:
  1238.                     written += self.writeVCASequence(name,vca,filepath)
  1239.         return written
  1240.  
  1241.     def writeVCA(self,name,vca,filepath):
  1242.         bench = BenchMarker()
  1243.         self.smd_file = self.openSMD(filepath,name + ".vta","vertex animation")
  1244.         if self.smd_file == None: return 0
  1245.            
  1246.         self.smd_file.write(
  1247. '''nodes
  1248. 0 "root" -1
  1249. end
  1250. skeleton
  1251. ''')
  1252.         for i,frame in enumerate(vca):
  1253.             self.smd_file.write("time {}\n0 0 0 0 0 0 0\n".format(i))
  1254.  
  1255.         self.smd_file.write("end\nvertexanimation\n")
  1256.         num_frames = len(vca)
  1257.         two_percent = num_frames / 50
  1258.        
  1259.         for frame, vca_ob in enumerate(vca):
  1260.             self.smd_file.write("time {}\n".format(frame))
  1261.  
  1262.             self.smd_file.writelines(["{} {} {}\n".format(loop.index, getSmdVec(vca_ob.data.vertices[loop.vertex_index].co), getSmdVec(loop.normal)) for loop in vca_ob.data.loops])
  1263.            
  1264.             if two_percent and frame % two_percent == 0:
  1265.                 print(".", debug_only=True, newline=False)
  1266.                 bpy.context.window_manager.progress_update(frame / num_frames)
  1267.  
  1268.             removeObject(vca_ob)
  1269.             vca[frame] = None
  1270.        
  1271.         self.smd_file.write("end\n")
  1272.         print(debug_only=True)
  1273.         print("Exported {} frames ({:.1f}MB)".format(num_frames, self.smd_file.tell() / 1024 / 1024))
  1274.         self.smd_file.close()
  1275.         bench.report("Vertex animation")
  1276.         print()
  1277.         return 1
  1278.  
  1279.     def writeVCASequence(self,name,vca,dir_path):
  1280.         self.smd_file = self.openSMD(dir_path,"vcaanim_{}.smd".format(name),"SMD")
  1281.         if self.smd_file == None: return 0
  1282.  
  1283.         self.smd_file.write(
  1284. '''nodes
  1285. {2}
  1286. {0} "vcabone_{1}" -1
  1287. end
  1288. skeleton
  1289. '''.format(vca.bone_id, name,
  1290.             "\n".join(['''{} "{}" -1'''.format(self.bone_ids[b.name],b.name) for b in self.exportable_bones if b.parent == None])
  1291.                 if self.armature_src else '0 "root" -1')
  1292.         )
  1293.  
  1294.         max_frame = float(len(vca)-1)
  1295.         for i in range(len(vca)):
  1296.             self.smd_file.write("time {}\n".format(i))
  1297.             if self.armature_src:
  1298.                 for root_bone in [b for b in self.exportable_bones if b.parent == None]:
  1299.                     mat = getUpAxisMat('Y').inverted() @ self.armature.matrix_world @ root_bone.matrix
  1300.                     self.smd_file.write("{} {} {}\n".format(self.bone_ids[root_bone.name], getSmdVec(mat.to_translation()), getSmdVec(mat.to_euler())))
  1301.             else:
  1302.                 self.smd_file.write("0 0 0 0 {} 0 0\n".format("-1.570797" if bpy.context.scene.vs.up_axis == 'Z' else "0"))
  1303.             self.smd_file.write("{0} 1.0 {1} 0 0 0 0\n".format(vca.bone_id,getSmdFloat(i / max_frame)))
  1304.         self.smd_file.write("end\n")
  1305.         self.smd_file.close()
  1306.         return 1
  1307.  
  1308.     def writeDMX(self, id, bake_results, name, dir_path):
  1309.         bench = BenchMarker(1,"DMX")
  1310.         filepath = os.path.realpath(os.path.join(dir_path,name + ".dmx"))
  1311.         print("-",filepath)
  1312.         armature_name = self.armature_src.name if self.armature_src else name
  1313.         materials = {}
  1314.         written = 0
  1315.        
  1316.         def makeTransform(name,matrix,object_name):
  1317.             trfm = dm.add_element(name,"DmeTransform",id=object_name+"transform")
  1318.             trfm["position"] = datamodel.Vector3(matrix.to_translation())
  1319.             trfm["orientation"] = getDatamodelQuat(matrix.to_quaternion())
  1320.             return trfm
  1321.        
  1322.         dm = datamodel.DataModel("model",State.datamodelFormat)
  1323.         dm.allow_random_ids = False
  1324.  
  1325.         source2 = dm.format_ver >= 22
  1326.  
  1327.         root = dm.add_element(bpy.context.scene.name,id="Scene"+bpy.context.scene.name)
  1328.         DmeModel = dm.add_element(armature_name,"DmeModel",id="Object" + armature_name)
  1329.         DmeModel_children = DmeModel["children"] = datamodel.make_array([],datamodel.Element)
  1330.        
  1331.         DmeModel_transforms = dm.add_element("base","DmeTransformList",id="transforms"+bpy.context.scene.name)
  1332.         DmeModel["baseStates"] = datamodel.make_array([ DmeModel_transforms ],datamodel.Element)
  1333.         DmeModel_transforms["transforms"] = datamodel.make_array([],datamodel.Element)
  1334.         DmeModel_transforms = DmeModel_transforms["transforms"]
  1335.  
  1336.         if source2:
  1337.             DmeAxisSystem = DmeModel["axisSystem"] = dm.add_element("axisSystem","DmeAxisSystem","AxisSys" + armature_name)
  1338.             DmeAxisSystem["upAxis"] = axes_lookup_source2[bpy.context.scene.vs.up_axis]
  1339.             DmeAxisSystem["forwardParity"] = 1 # ??
  1340.             DmeAxisSystem["coordSys"] = 0 # ??
  1341.        
  1342.         DmeModel["transform"] = makeTransform("",Matrix(),DmeModel.name + "transform")
  1343.  
  1344.         keywords = getDmxKeywords(dm.format_ver)
  1345.                
  1346.         # skeleton
  1347.         root["skeleton"] = DmeModel
  1348.         want_jointlist = dm.format_ver >= 11
  1349.         want_jointtransforms = dm.format_ver in range(0,21)
  1350.         if want_jointlist:
  1351.             jointList = DmeModel["jointList"] = datamodel.make_array([],datamodel.Element)
  1352.             if source2:
  1353.                 jointList.append(DmeModel)
  1354.         if want_jointtransforms:
  1355.             jointTransforms = DmeModel["jointTransforms"] = datamodel.make_array([],datamodel.Element)     
  1356.             if source2:
  1357.                 jointTransforms.append(DmeModel["transform"])
  1358.         bone_elements = {}
  1359.         if self.armature: armature_scale = self.armature.matrix_world.to_scale()
  1360.        
  1361.         def writeBone(bone):
  1362.             if isinstance(bone,str):
  1363.                 bone_name = bone
  1364.                 bone = None
  1365.             else:
  1366.                 if bone and not bone in self.exportable_bones:
  1367.                     children = []
  1368.                     for child_elems in [writeBone(child) for child in bone.children]:
  1369.                         if child_elems: children.extend(child_elems)
  1370.                     return children
  1371.                 bone_name = bone.name
  1372.  
  1373.             bone_elements[bone_name] = bone_elem = dm.add_element(bone_name,"DmeJoint",id=bone_name)
  1374.             if want_jointlist: jointList.append(bone_elem)
  1375.             self.bone_ids[bone_name] = len(bone_elements) - (0 if source2 else 1) # in Source 2, index 0 is the DmeModel
  1376.            
  1377.             if not bone: relMat = Matrix()
  1378.             else:
  1379.                 cur_p = bone.parent
  1380.                 while cur_p and not cur_p in self.exportable_bones: cur_p = cur_p.parent
  1381.                 if cur_p:
  1382.                     relMat = cur_p.matrix.inverted() @ bone.matrix
  1383.                 else:
  1384.                     relMat = self.armature.matrix_world @ bone.matrix
  1385.            
  1386.             trfm = makeTransform(bone_name,relMat,"bone"+bone_name)
  1387.             trfm_base = makeTransform(bone_name,relMat,"bone_base"+bone_name)
  1388.            
  1389.             if bone and bone.parent:
  1390.                 for j in range(3):
  1391.                     trfm["position"][j] *= armature_scale[j]
  1392.             trfm_base["position"] = trfm["position"]
  1393.            
  1394.             if want_jointtransforms: jointTransforms.append(trfm)
  1395.             bone_elem["transform"] = trfm
  1396.            
  1397.             DmeModel_transforms.append(trfm_base)
  1398.            
  1399.             if bone:
  1400.                 children = bone_elem["children"] = datamodel.make_array([],datamodel.Element)
  1401.                 for child_elems in [writeBone(child) for child in bone.children]:
  1402.                     if child_elems: children.extend(child_elems)
  1403.  
  1404.                 bpy.context.window_manager.progress_update(len(bone_elements)/num_bones)
  1405.             return [bone_elem]
  1406.    
  1407.         if self.armature:
  1408.             num_bones = len(self.exportable_bones)
  1409.             add_implicit_bone = not source2
  1410.            
  1411.             if add_implicit_bone:
  1412.                 DmeModel_children.extend(writeBone(implicit_bone_name))
  1413.             for root_elems in [writeBone(bone) for bone in self.armature.pose.bones if not bone.parent and not (add_implicit_bone and bone.name == implicit_bone_name)]:
  1414.                 if root_elems: DmeModel_children.extend(root_elems)
  1415.  
  1416.             bench.report("Bones")
  1417.  
  1418.         for vca in bake_results[0].vertex_animations:
  1419.             DmeModel_children.extend(writeBone("vcabone_{}".format(vca)))
  1420.  
  1421.         DmeCombinationOperator = None
  1422.         for _ in [bake for bake in bake_results if bake.shapes]:
  1423.             if self.flex_controller_mode == 'ADVANCED':
  1424.                 if not hasFlexControllerSource(self.flex_controller_source):
  1425.                     self.error(get_id("exporter_err_flexctrl_undefined",True).format(name) )
  1426.                     return written
  1427.  
  1428.                 text = bpy.data.texts.get(self.flex_controller_source)
  1429.                 msg = "- Loading flex controllers from "
  1430.                 element_path = [ "combinationOperator" ]
  1431.                 try:
  1432.                     if text:
  1433.                         print(msg + "text block \"{}\"".format(text.name))
  1434.                         controller_dm = datamodel.parse(text.as_string(),element_path=element_path)
  1435.                     else:
  1436.                         path = os.path.realpath(bpy.path.abspath(self.flex_controller_source))
  1437.                         print(msg + path)
  1438.                         controller_dm = datamodel.load(path=path,element_path=element_path)
  1439.            
  1440.                     DmeCombinationOperator = controller_dm.root["combinationOperator"]
  1441.  
  1442.                     for elem in [elem for elem in DmeCombinationOperator["targets"] if elem.type != "DmeFlexRules"]:
  1443.                         DmeCombinationOperator["targets"].remove(elem)
  1444.                 except Exception as err:
  1445.                     self.error(get_id("exporter_err_flexctrl_loadfail", True).format(err))
  1446.                     return written
  1447.             else:
  1448.                 DmeCombinationOperator = flex.DmxWriteFlexControllers.make_controllers(id).root["combinationOperator"]
  1449.  
  1450.             break
  1451.  
  1452.         if not DmeCombinationOperator and len(bake_results[0].vertex_animations):
  1453.             DmeCombinationOperator = flex.DmxWriteFlexControllers.make_controllers(id).root["combinationOperator"]
  1454.  
  1455.         if DmeCombinationOperator:
  1456.             root["combinationOperator"] = DmeCombinationOperator
  1457.             bench.report("Flex setup")
  1458.  
  1459.         for bake in [bake for bake in bake_results if bake.object.type != 'ARMATURE']:
  1460.             root["model"] = DmeModel
  1461.            
  1462.             ob = bake.object
  1463.            
  1464.             vertex_data = dm.add_element("bind","DmeVertexData",id=bake.name+"verts")
  1465.            
  1466.             DmeMesh = dm.add_element(bake.name,"DmeMesh",id=bake.name+"mesh")
  1467.             DmeMesh["visible"] = True          
  1468.             DmeMesh["bindState"] = vertex_data
  1469.             DmeMesh["currentState"] = vertex_data
  1470.             DmeMesh["baseStates"] = datamodel.make_array([vertex_data],datamodel.Element)
  1471.                        
  1472.             DmeDag = dm.add_element(bake.name,"DmeDag",id="ob"+bake.name+"dag")
  1473.             if want_jointlist: jointList.append(DmeDag)
  1474.             DmeDag["shape"] = DmeMesh
  1475.            
  1476.             bone_child = isinstance(bake.envelope, str)
  1477.             if bone_child and bake.envelope in bone_elements:
  1478.                 bone_elements[bake.envelope]["children"].append(DmeDag)
  1479.                 trfm_mat = bake.bone_parent_matrix
  1480.             else:
  1481.                 DmeModel_children.append(DmeDag)
  1482.                 trfm_mat = ob.matrix_world
  1483.  
  1484.             trfm = makeTransform(bake.name, trfm_mat, "ob"+bake.name)
  1485.                        
  1486.             if want_jointtransforms: jointTransforms.append(trfm)
  1487.            
  1488.             DmeDag["transform"] = trfm
  1489.             DmeModel_transforms.append(makeTransform(bake.name, trfm_mat, "ob_base"+bake.name))
  1490.            
  1491.             jointCount = 0
  1492.             weight_link_limit = 4 if source2 else 3
  1493.             badJointCounts = 0
  1494.             culled_weight_links = 0
  1495.             cull_threshold = bpy.context.scene.vs.dmx_weightlink_threshold
  1496.             have_weightmap = False
  1497.  
  1498.             if type(bake.envelope) is bpy.types.ArmatureModifier:
  1499.                 ob_weights = self.getWeightmap(bake)
  1500.  
  1501.                 for vert_weights in ob_weights:
  1502.                     count = len(vert_weights)
  1503.  
  1504.                     if weight_link_limit:
  1505.                         if count > weight_link_limit and cull_threshold > 0:
  1506.                             vert_weights.sort(key=lambda link: link[1],reverse=True)
  1507.                             while len(vert_weights) > weight_link_limit and vert_weights[-1][1] <= cull_threshold:
  1508.                                 vert_weights.pop()
  1509.                                 culled_weight_links += 1
  1510.                             count = len(vert_weights)
  1511.                         if count > weight_link_limit: badJointCounts += 1
  1512.  
  1513.                     jointCount = max(jointCount,count)
  1514.                 if jointCount: have_weightmap = True
  1515.             elif bake.envelope:
  1516.                 jointCount = 1
  1517.                    
  1518.             if badJointCounts:
  1519.                 self.warning(get_id("exporter_warn_weightlinks_excess",True).format(badJointCounts,bake.src.name,weight_link_limit))
  1520.             if culled_weight_links:
  1521.                 self.warning(get_id("exporter_warn_weightlinks_culled",True).format(culled_weight_links,cull_threshold,bake.src.name))
  1522.                        
  1523.             format = vertex_data["vertexFormat"] = datamodel.make_array( [ keywords['pos'], keywords['norm'] ], str)
  1524.            
  1525.             vertex_data["flipVCoordinates"] = True
  1526.             vertex_data["jointCount"] = jointCount
  1527.            
  1528.             num_verts = len(ob.data.vertices)
  1529.             num_loops = len(ob.data.loops)
  1530.             norms = [None] * num_loops
  1531.             texco = ordered_set.OrderedSet()
  1532.             face_sets = collections.OrderedDict()
  1533.             texcoIndices = [None] * num_loops
  1534.             jointWeights = []
  1535.             jointIndices = []
  1536.             balance = [0.0] * num_verts
  1537.            
  1538.             Indices = [None] * num_loops
  1539.  
  1540.             uv_layer = ob.data.uv_layers.active.data
  1541.            
  1542.             bench.report("object setup")           
  1543.            
  1544.             v=0
  1545.             for vert in ob.data.vertices:
  1546.                 vert.select = False
  1547.                
  1548.                 if bake.shapes and bake.balance_vg:
  1549.                     try: balance[vert.index] = bake.balance_vg.weight(vert.index)
  1550.                     except: pass
  1551.                
  1552.                 if have_weightmap:
  1553.                     weights = [0.0] * jointCount
  1554.                     indices = [0] * jointCount
  1555.                     i = 0
  1556.                     total_weight = 0
  1557.                     vert_weights = ob_weights[vert.index]
  1558.                     for i in range(len(vert_weights)):
  1559.                         indices[i] = vert_weights[i][0]
  1560.                         weights[i] = vert_weights[i][1]
  1561.                         total_weight += weights[i]
  1562.                         i+=1
  1563.  
  1564.                     if source2 and total_weight == 0:
  1565.                         weights[0] = 1.0 # attach to the DmeModel itself, avoiding motion.
  1566.                    
  1567.                     jointWeights.extend(weights)
  1568.                     jointIndices.extend(indices)
  1569.                     v += 1
  1570.                 if v % 50 == 0:
  1571.                     bpy.context.window_manager.progress_update(v / num_verts)
  1572.  
  1573.             bench.report("verts")
  1574.  
  1575.             for loop in [ob.data.loops[i] for poly in ob.data.polygons for i in poly.loop_indices]:
  1576.                 texcoIndices[loop.index] = texco.add(datamodel.Vector2(uv_layer[loop.index].uv))
  1577.                 norms[loop.index] = datamodel.Vector3(loop.normal)
  1578.                 Indices[loop.index] = loop.vertex_index                
  1579.  
  1580.             bench.report("loops")
  1581.  
  1582.             bpy.context.view_layer.objects.active = ob
  1583.             bpy.ops.object.mode_set(mode='EDIT')
  1584.             bm = bmesh.from_edit_mesh(ob.data)
  1585.             bm.verts.ensure_lookup_table()
  1586.             bm.faces.ensure_lookup_table()
  1587.  
  1588.             vertex_data[keywords['pos']] = datamodel.make_array((v.co for v in bm.verts),datamodel.Vector3)
  1589.             vertex_data[keywords['pos'] + "Indices"] = datamodel.make_array((l.vert.index for f in bm.faces for l in f.loops),int)
  1590.  
  1591.             if source2: # write out arbitrary vertex data
  1592.                 loops = [loop for face in bm.faces for loop in face.loops]
  1593.                 loop_indices = datamodel.make_array([loop.index for loop in loops], int)
  1594.                 layerGroups = bm.loops.layers
  1595.  
  1596.                 class exportLayer:
  1597.                     name : str
  1598.                    
  1599.                     def __init__(self, layer, exportName = None):
  1600.                         self._layer = layer
  1601.                         self.name = exportName or layer.name
  1602.                        
  1603.                     def data_for(self, loop): return loop[self._layer]
  1604.                
  1605.                 def get_bmesh_layers(layerGroup):
  1606.                     return [exportLayer(l) for l in layerGroup if re.match(r".*\$[0-9]+", l.name)]
  1607.  
  1608.                 defaultUvLayer = "texcoord$0"
  1609.                 uv_layers_to_export = list(get_bmesh_layers(layerGroups.uv))
  1610.                 if not defaultUvLayer in [l.name for l in uv_layers_to_export]: # select a default UV map
  1611.                     uv_render_layer = next((l.name for l in ob.data.uv_layers if l.active_render and not l in uv_layers_to_export), None)
  1612.                     if uv_render_layer:
  1613.                         uv_layers_to_export.append(exportLayer(layerGroups.uv[uv_render_layer], defaultUvLayer))
  1614.                         print("- Exporting '{}' as {}".format(uv_render_layer, defaultUvLayer))
  1615.                     else:
  1616.                         self.warning("'{}' does not contain a UV Map called {} and no suitable fallback map could be found. The model may be missing UV data.".format(bake.name, defaultUvLayer))
  1617.  
  1618.                 for layer in uv_layers_to_export:
  1619.                     uv_set = ordered_set.OrderedSet()
  1620.                     uv_indices = []
  1621.                     for uv in (layer.data_for(loop).uv for loop in loops):
  1622.                         uv_indices.append(uv_set.add(datamodel.Vector2(uv)))
  1623.                        
  1624.                     vertex_data[layer.name] = datamodel.make_array(uv_set, datamodel.Vector2)
  1625.                     vertex_data[layer.name + "Indices"] = datamodel.make_array(uv_indices, int)
  1626.                     format.append(layer.name)
  1627.  
  1628.                 def make_vertex_layer(layer : exportLayer, arrayType):
  1629.                     vertex_data[layer.name] = datamodel.make_array([layer.data_for(loop) for loop in loops], arrayType)
  1630.                     vertex_data[layer.name + "Indices"] = loop_indices
  1631.                     format.append(layer.name)
  1632.  
  1633.                 for layer in get_bmesh_layers(layerGroups.color):
  1634.                     make_vertex_layer(layer, datamodel.Vector4)
  1635.                 for layer in get_bmesh_layers(layerGroups.float):
  1636.                     make_vertex_layer(layer, float)
  1637.                 for layer in get_bmesh_layers(layerGroups.int):
  1638.                     make_vertex_layer(layer, int)
  1639.                 for layer in get_bmesh_layers(layerGroups.string):
  1640.                     make_vertex_layer(layer, str)
  1641.  
  1642.                 bench.report("Source 2 vertex data")
  1643.            
  1644.             else:
  1645.                 format.append("textureCoordinates")
  1646.                 vertex_data["textureCoordinates"] = datamodel.make_array(texco,datamodel.Vector2)
  1647.                 vertex_data["textureCoordinatesIndices"] = datamodel.make_array(texcoIndices,int)
  1648.                                
  1649.             if have_weightmap:
  1650.                 vertex_data[keywords["weight"]] = datamodel.make_array(jointWeights,float)
  1651.                 vertex_data[keywords["weight_indices"]] = datamodel.make_array(jointIndices,int)
  1652.                 format.extend( [ keywords['weight'], keywords["weight_indices"] ] )
  1653.  
  1654.             deform_layer = bm.verts.layers.deform.active
  1655.             if deform_layer:
  1656.                 for cloth_enable in (group for group in ob.vertex_groups if re.match(r"cloth_enable\$[0-9]+", group.name)):
  1657.                     format.append(cloth_enable.name)
  1658.                     values = [v[deform_layer].get(cloth_enable.index, 0) for v in bm.verts]
  1659.                     valueSet = ordered_set.OrderedSet(values)
  1660.                     vertex_data[cloth_enable.name] = datamodel.make_array(valueSet, float)
  1661.                     vertex_data[cloth_enable.name + "Indices"] = datamodel.make_array((valueSet.index(values[i]) for i in Indices), int)
  1662.            
  1663.             if bake.shapes and bake.balance_vg:
  1664.                 vertex_data[keywords["balance"]] = datamodel.make_array(balance,float)
  1665.                 vertex_data[keywords["balance"] + "Indices"] = datamodel.make_array(Indices,int)
  1666.                 format.append(keywords["balance"])
  1667.                        
  1668.             vertex_data[keywords['norm']] = datamodel.make_array(norms,datamodel.Vector3)
  1669.             vertex_data[keywords['norm'] + "Indices"] = datamodel.make_array(range(len(norms)),int)
  1670.            
  1671.             bench.report("insert")
  1672.            
  1673.             bad_face_mats = 0
  1674.             p = 0
  1675.             num_polys = len(bm.faces)
  1676.  
  1677.             two_percent = int(num_polys / 50)
  1678.             print("Polygons: ",debug_only=True,newline=False)
  1679.  
  1680.             bm_face_sets = collections.defaultdict(list)
  1681.             for face in bm.faces:
  1682.                 mat_name, mat_success = self.GetMaterialName(ob, face.material_index)
  1683.                 if not mat_success:
  1684.                     bad_face_mats += 1
  1685.                 bm_face_sets[mat_name].extend((*(l.index for l in face.loops),-1))
  1686.                
  1687.                 p+=1
  1688.                 if two_percent and p % two_percent == 0:
  1689.                     print(".", debug_only=True, newline=False)
  1690.                     bpy.context.window_manager.progress_update(p / num_polys)
  1691.  
  1692.             for (mat_name,indices) in bm_face_sets.items():
  1693.                 material_elem = materials.get(mat_name)
  1694.                 if not material_elem:
  1695.                     materials[mat_name] = material_elem = dm.add_element(mat_name,"DmeMaterial",id=mat_name + "mat")
  1696.                     material_elem["mtlName"] = os.path.join(bpy.context.scene.vs.material_path, mat_name).replace('\\','/')
  1697.                    
  1698.                 face_set = dm.add_element(mat_name,"DmeFaceSet",id=bake.name+mat_name+"faces")
  1699.                 face_sets[mat_name] = face_set
  1700.  
  1701.                 face_set["material"] = material_elem
  1702.                 face_set["faces"] = datamodel.make_array(indices,int)
  1703.  
  1704.             print(debug_only=True)
  1705.             DmeMesh["faceSets"] = datamodel.make_array(list(face_sets.values()),datamodel.Element)
  1706.            
  1707.             if bad_face_mats:
  1708.                 self.warning(get_id("exporter_err_facesnotex_ormat").format(bad_face_mats, bake.name))
  1709.             bench.report("polys")
  1710.  
  1711.             bpy.ops.object.mode_set(mode='OBJECT')
  1712.             del bm
  1713.  
  1714.             two_percent = int(len(bake.shapes) / 50)
  1715.             print("Shapes: ",debug_only=True,newline=False)
  1716.             delta_states = []
  1717.             corrective_shapes_seen = []
  1718.             if bake.shapes:
  1719.                 shape_names = []
  1720.                 num_shapes = len(bake.shapes)
  1721.                 num_correctives = 0
  1722.                 num_wrinkles = 0
  1723.                
  1724.                 for shape_name,shape in bake.shapes.items():
  1725.                     wrinkle_scale = 0
  1726.                     corrective = getCorrectiveShapeSeparator() in shape_name
  1727.                     if corrective:
  1728.                         # drivers always override shape name to avoid name truncation issues
  1729.                         corrective_targets_driver = ordered_set.OrderedSet(flex.getCorrectiveShapeKeyDrivers(bake.src.data.shape_keys.key_blocks[shape_name]) or [])
  1730.                         corrective_targets_name = ordered_set.OrderedSet(shape_name.split(getCorrectiveShapeSeparator()))
  1731.                         corrective_targets = corrective_targets_driver or corrective_targets_name
  1732.                         corrective_targets.source = shape_name
  1733.  
  1734.                         if(corrective_targets in corrective_shapes_seen):
  1735.                             previous_shape = next(x for x in corrective_shapes_seen if x == corrective_targets)
  1736.                             self.warning(get_id("exporter_warn_correctiveshape_duplicate", True).format(shape_name, "+".join(corrective_targets), previous_shape.source))
  1737.                             continue
  1738.                         else:
  1739.                             corrective_shapes_seen.append(corrective_targets)
  1740.                        
  1741.                         if corrective_targets_driver and corrective_targets_driver != corrective_targets_name:
  1742.                             generated_shape_name = getCorrectiveShapeSeparator().join(corrective_targets_driver)
  1743.                             print("- Renamed shape key '{}' to '{}' to match its corrective shape drivers.".format(shape_name, generated_shape_name))
  1744.                             shape_name = generated_shape_name
  1745.                         num_correctives += 1
  1746.                     else:
  1747.                         if self.flex_controller_mode == 'ADVANCED':
  1748.                             def _FindScale():
  1749.                                 for control in controller_dm.root["combinationOperator"]["controls"]:
  1750.                                     for i in range(len(control["rawControlNames"])):
  1751.                                         if control["rawControlNames"][i] == shape_name:
  1752.                                             scales = control.get("wrinkleScales")
  1753.                                             return scales[i] if scales else 0
  1754.                                 raise ValueError()
  1755.                             try:
  1756.                                 wrinkle_scale = _FindScale()
  1757.                             except ValueError:
  1758.                                 self.warning(get_id("exporter_err_flexctrl_missing", True).format(shape_name))
  1759.                             pass
  1760.                    
  1761.                     shape_names.append(shape_name)
  1762.                     DmeVertexDeltaData = dm.add_element(shape_name,"DmeVertexDeltaData",id=ob.name+shape_name)
  1763.                     delta_states.append(DmeVertexDeltaData)
  1764.  
  1765.                     vertexFormat = DmeVertexDeltaData["vertexFormat"] = datamodel.make_array([ keywords['pos'], keywords['norm'] ],str)
  1766.                    
  1767.                     wrinkle = []
  1768.                     wrinkleIndices = []
  1769.  
  1770.                     # what do these do?
  1771.                     #DmeVertexDeltaData["flipVCoordinates"] = False
  1772.                     #DmeVertexDeltaData["corrected"] = True
  1773.  
  1774.                     shape_pos = []
  1775.                     shape_posIndices = []
  1776.                     shape_norms = []
  1777.                     shape_normIndices = []
  1778.                     cache_deltas = wrinkle_scale
  1779.                     if cache_deltas:
  1780.                         delta_lengths = [None] * len(ob.data.vertices)
  1781.                         max_delta = 0
  1782.                    
  1783.                     for ob_vert in ob.data.vertices:
  1784.                         shape_vert = shape.vertices[ob_vert.index]
  1785.  
  1786.                         if ob_vert.co != shape_vert.co:
  1787.                             delta = shape_vert.co - ob_vert.co
  1788.                             delta_length = delta.length
  1789.  
  1790.                             if abs(delta_length) > 1e-5:
  1791.                                 if cache_deltas:
  1792.                                     delta_lengths[ob_vert.index] = delta_length
  1793.                                 shape_pos.append(datamodel.Vector3(delta))
  1794.                                 shape_posIndices.append(ob_vert.index)
  1795.  
  1796.                     if corrective:
  1797.                         corrective_target_shapes = []
  1798.                         for corrective_shape_name in corrective_targets:
  1799.                             corrective_target = bake.shapes.get(corrective_shape_name)
  1800.                             if corrective_target:
  1801.                                 corrective_target_shapes.append(corrective_target)
  1802.                             else:
  1803.                                 self.warning(get_id("exporter_err_missing_corrective_target", format_string=True).format(shape_name, corrective_shape_name))
  1804.                                 continue
  1805.  
  1806.                             # We need the absolute normals as generated by Blender
  1807.                             for shape_vert in shape.vertices:
  1808.                                 shape_vert.co -= ob.data.vertices[shape_vert.index].co - corrective_target.vertices[shape_vert.index].co
  1809.  
  1810.                     for ob_loop in ob.data.loops:
  1811.                         shape_loop = shape.loops[ob_loop.index]
  1812.                         norm = shape_loop.normal
  1813.  
  1814.                         if corrective:
  1815.                             base = Vector(ob_loop.normal)
  1816.                             for corrective_target in corrective_target_shapes:
  1817.                                 # Normals for corrective shape keys are deltas from those of the deformed mesh, not the basis shape.
  1818.                                 base += corrective_target.loops[shape_loop.index].normal - ob_loop.normal
  1819.                         else:
  1820.                             base = ob_loop.normal
  1821.  
  1822.                         if norm.dot(base.normalized()) < 1 - 1e-3:
  1823.                             shape_norms.append(datamodel.Vector3(norm - base))
  1824.                             shape_normIndices.append(shape_loop.index)
  1825.  
  1826.                         if wrinkle_scale:
  1827.                             delta_len = delta_lengths[ob_loop.vertex_index]
  1828.                             if delta_len:
  1829.                                 max_delta = max(max_delta,delta_len)
  1830.                                 wrinkle.append(delta_len)
  1831.                                 wrinkleIndices.append(texcoIndices[ob_loop.index])
  1832.  
  1833.                     del shape_vert
  1834.  
  1835.                     if wrinkle_scale and max_delta:
  1836.                         wrinkle_mod = wrinkle_scale / max_delta
  1837.                         if wrinkle_mod != 1:
  1838.                             for i in range(len(wrinkle)):
  1839.                                 wrinkle[i] *= wrinkle_mod
  1840.  
  1841.                     DmeVertexDeltaData[keywords['pos']] = datamodel.make_array(shape_pos,datamodel.Vector3)
  1842.                     DmeVertexDeltaData[keywords['pos'] + "Indices"] = datamodel.make_array(shape_posIndices,int)
  1843.                     DmeVertexDeltaData[keywords['norm']] = datamodel.make_array(shape_norms,datamodel.Vector3)
  1844.                     DmeVertexDeltaData[keywords['norm'] + "Indices"] = datamodel.make_array(shape_normIndices,int)
  1845.  
  1846.                     if wrinkle_scale:
  1847.                         vertexFormat.append(keywords["wrinkle"])
  1848.                         num_wrinkles += 1
  1849.                         DmeVertexDeltaData[keywords["wrinkle"]] = datamodel.make_array(wrinkle,float)
  1850.                         DmeVertexDeltaData[keywords["wrinkle"] + "Indices"] = datamodel.make_array(wrinkleIndices,int)
  1851.                                        
  1852.                     bpy.context.window_manager.progress_update(len(shape_names) / num_shapes)
  1853.                     if two_percent and len(shape_names) % two_percent == 0:
  1854.                         print(".",debug_only=True,newline=False)
  1855.  
  1856.                 if bpy.app.debug_value <= 1:
  1857.                     for shape in bake.shapes.values():                 
  1858.                         bpy.data.meshes.remove(shape)
  1859.                         del shape
  1860.                     bake.shapes.clear()
  1861.  
  1862.                 print(debug_only=True)
  1863.                 bench.report("shapes")
  1864.                 print("- {} flexes ({} with wrinklemaps) + {} correctives".format(num_shapes - num_correctives,num_wrinkles,num_correctives))
  1865.            
  1866.             vca_matrix = ob.matrix_world.inverted()
  1867.             for vca_name,vca in bake_results[0].vertex_animations.items():
  1868.                 frame_shapes = []
  1869.  
  1870.                 for i, vca_ob in enumerate(vca):
  1871.                     DmeVertexDeltaData = dm.add_element("{}-{}".format(vca_name,i),"DmeVertexDeltaData",id=ob.name+vca_name+str(i))
  1872.                     delta_states.append(DmeVertexDeltaData)
  1873.                     frame_shapes.append(DmeVertexDeltaData)
  1874.                     DmeVertexDeltaData["vertexFormat"] = datamodel.make_array([ "positions", "normals" ],str)
  1875.  
  1876.                     shape_pos = []
  1877.                     shape_posIndices = []
  1878.                     shape_norms = []
  1879.                     shape_normIndices = []
  1880.  
  1881.                     for shape_loop in vca_ob.data.loops:
  1882.                         shape_vert = vca_ob.data.vertices[shape_loop.vertex_index]
  1883.                         ob_loop = ob.data.loops[shape_loop.index]
  1884.                         ob_vert = ob.data.vertices[ob_loop.vertex_index]
  1885.  
  1886.                         if ob_vert.co != shape_vert.co:
  1887.                             delta = vca_matrix @ shape_vert.co - ob_vert.co
  1888.  
  1889.                             if abs(delta.length) > 1e-5:
  1890.                                 shape_pos.append(datamodel.Vector3(delta))
  1891.                                 shape_posIndices.append(ob_vert.index)
  1892.                        
  1893.                         norm = Vector(shape_loop.normal)
  1894.                         norm.rotate(vca_matrix)
  1895.                         if abs(1.0 - norm.dot(ob_loop.normal)) > epsilon[0]:
  1896.                             shape_norms.append(datamodel.Vector3(norm - ob_loop.normal))
  1897.                             shape_normIndices.append(shape_loop.index)
  1898.  
  1899.                     DmeVertexDeltaData["positions"] = datamodel.make_array(shape_pos,datamodel.Vector3)
  1900.                     DmeVertexDeltaData["positionsIndices"] = datamodel.make_array(shape_posIndices,int)
  1901.                     DmeVertexDeltaData["normals"] = datamodel.make_array(shape_norms,datamodel.Vector3)
  1902.                     DmeVertexDeltaData["normalsIndices"] = datamodel.make_array(shape_normIndices,int)
  1903.  
  1904.                     removeObject(vca_ob)
  1905.                     vca[i] = None
  1906.  
  1907.                 if vca.export_sequence: # generate and export a skeletal animation that drives the vertex animation
  1908.                     vca_arm = bpy.data.objects.new("vca_arm",bpy.data.armatures.new("vca_arm"))
  1909.                     bpy.context.scene.collection.objects.link(vca_arm)
  1910.                     bpy.context.view_layer.objects.active = vca_arm
  1911.  
  1912.                     bpy.ops.object.mode_set(mode='EDIT')
  1913.                     vca_bone_name = "vcabone_" + vca_name
  1914.                     vca_bone = vca_arm.data.edit_bones.new(vca_bone_name)
  1915.                     vca_bone.tail.y = 1
  1916.                    
  1917.                     bpy.context.scene.frame_set(0)
  1918.                     mat = getUpAxisMat('y').inverted()
  1919.                     # DMX animations don't handle missing root bones or meshes, so create bones to represent them
  1920.                     if self.armature_src:
  1921.                         for bone in [bone for bone in self.armature_src.data.bones if bone.parent is None]:
  1922.                             b = vca_arm.data.edit_bones.new(bone.name)
  1923.                             b.head = mat @ bone.head
  1924.                             b.tail = mat @ bone.tail
  1925.                     else:
  1926.                         for bake in bake_results:
  1927.                             bake_mat = mat @ bake.object.matrix_world
  1928.                             b = vca_arm.data.edit_bones.new(bake.name)
  1929.                             b.head = bake_mat @ b.head
  1930.                             b.tail = bake_mat @ Vector([0,1,0])
  1931.  
  1932.                     bpy.ops.object.mode_set(mode='POSE')
  1933.                     ops.pose.armature_apply() # refreshes the armature's internal state, required!
  1934.                     action = vca_arm.animation_data_create().action = bpy.data.actions.new("vcaanim_" + vca_name)
  1935.                     for i in range(2):
  1936.                         fc = action.fcurves.new('pose.bones["{}"].location'.format(vca_bone_name),index=i)
  1937.                         fc.keyframe_points.add(count=2)
  1938.                         for key in fc.keyframe_points: key.interpolation = 'LINEAR'
  1939.                         if i == 0: fc.keyframe_points[0].co = (0,1.0)
  1940.                         fc.keyframe_points[1].co = (vca.num_frames,1.0)
  1941.                         fc.update()
  1942.  
  1943.                     # finally, write it out
  1944.                     self.exportId(bpy.context,vca_arm)
  1945.                     written += 1
  1946.  
  1947.             if delta_states:
  1948.                 DmeMesh["deltaStates"] = datamodel.make_array(delta_states,datamodel.Element)
  1949.                 DmeMesh["deltaStateWeights"] = DmeMesh["deltaStateWeightsLagged"] = \
  1950.                     datamodel.make_array([datamodel.Vector2([0.0,0.0])] * len(delta_states),datamodel.Vector2)
  1951.  
  1952.                 targets = DmeCombinationOperator["targets"]
  1953.                 added = False
  1954.                 for elem in targets:
  1955.                     if elem.type == "DmeFlexRules":
  1956.                         if elem["deltaStates"][0].name in shape_names: # can't have the same delta name on multiple objects
  1957.                             elem["target"] = DmeMesh
  1958.                             added = True
  1959.                 if not added:
  1960.                     targets.append(DmeMesh)
  1961.  
  1962.         if len(bake_results) == 1 and bake_results[0].object.type == 'ARMATURE': # animation
  1963.             ad = self.armature.animation_data
  1964.                        
  1965.             anim_len = animationLength(ad) if ad else 0
  1966.             if anim_len == 0:
  1967.                 self.warning(get_id("exporter_err_noframes",True).format(self.armature_src.name))
  1968.            
  1969.             if ad.action and hasattr(ad.action,'fps'):
  1970.                 fps = bpy.context.scene.render.fps = ad.action.fps
  1971.                 bpy.context.scene.render.fps_base = 1
  1972.             else:
  1973.                 fps = bpy.context.scene.render.fps * bpy.context.scene.render.fps_base
  1974.            
  1975.             DmeChannelsClip = dm.add_element(name,"DmeChannelsClip",id=name+"clip")    
  1976.             DmeAnimationList = dm.add_element(armature_name,"DmeAnimationList",id=armature_name+"list")
  1977.             DmeAnimationList["animations"] = datamodel.make_array([DmeChannelsClip],datamodel.Element)
  1978.             root["animationList"] = DmeAnimationList
  1979.            
  1980.             DmeTimeFrame = dm.add_element("timeframe","DmeTimeFrame",id=name+"time")
  1981.             duration = anim_len / fps
  1982.             if dm.format_ver >= 11:
  1983.                 DmeTimeFrame["duration"] = datamodel.Time(duration)
  1984.             else:
  1985.                 DmeTimeFrame["durationTime"] = int(duration * 10000)
  1986.             DmeTimeFrame["scale"] = 1.0
  1987.             DmeChannelsClip["timeFrame"] = DmeTimeFrame
  1988.             DmeChannelsClip["frameRate"] = fps if source2 else int(fps)
  1989.            
  1990.             channels = DmeChannelsClip["channels"] = datamodel.make_array([],datamodel.Element)
  1991.             bone_channels = {}
  1992.             def makeChannel(bone):
  1993.                 bone_channels[bone.name] = []
  1994.                 channel_template = [
  1995.                     [ "_p", "position", "Vector3", datamodel.Vector3 ],
  1996.                     [ "_o", "orientation", "Quaternion", datamodel.Quaternion ]
  1997.                 ]
  1998.                 for template in channel_template:
  1999.                     cur = dm.add_element(bone.name + template[0],"DmeChannel",id=bone.name+template[0])
  2000.                     cur["toAttribute"] = template[1]
  2001.                     cur["toElement"] = (bone_elements[bone.name] if bone else DmeModel)["transform"]
  2002.                     cur["mode"] = 1            
  2003.                     val_arr = dm.add_element(template[2]+" log","Dme"+template[2]+"LogLayer",cur.name+"loglayer")              
  2004.                     cur["log"] = dm.add_element(template[2]+" log","Dme"+template[2]+"Log",cur.name+"log")
  2005.                     cur["log"]["layers"] = datamodel.make_array([val_arr],datamodel.Element)               
  2006.                     val_arr["times"] = datamodel.make_array([],datamodel.Time if dm.format_ver > 11 else int)
  2007.                     val_arr["values"] = datamodel.make_array([],template[3])
  2008.                     if bone: bone_channels[bone.name].append(val_arr)
  2009.                     channels.append(cur)
  2010.            
  2011.             for bone in self.exportable_bones:
  2012.                 makeChannel(bone)
  2013.             num_frames = int(anim_len + 1)
  2014.             bench.report("Animation setup")
  2015.             prev_pos = {}
  2016.             prev_rot = {}
  2017.             skipped_pos = {}
  2018.             skipped_rot = {}
  2019.  
  2020.             two_percent = num_frames / 50
  2021.             print("Frames: ",debug_only=True,newline=False)
  2022.             for frame in range(0,num_frames):
  2023.                 bpy.context.window_manager.progress_update(frame/num_frames)
  2024.                 bpy.context.scene.frame_set(frame)
  2025.                 keyframe_time = datamodel.Time(frame / fps) if dm.format_ver > 11 else int(frame/fps * 10000)
  2026.                 evaluated_bones = self.getEvaluatedPoseBones()
  2027.                 for bone in evaluated_bones:
  2028.                     channel = bone_channels[bone.name]
  2029.  
  2030.                     cur_p = bone.parent
  2031.                     while cur_p and not cur_p in evaluated_bones: cur_p = cur_p.parent
  2032.                     if cur_p:
  2033.                         relMat = cur_p.matrix.inverted() @ bone.matrix
  2034.                     else:
  2035.                         relMat = self.armature.matrix_world @ bone.matrix
  2036.                    
  2037.                     pos = relMat.to_translation()
  2038.                     if bone.parent:
  2039.                         for j in range(3): pos[j] *= armature_scale[j]
  2040.                    
  2041.                     rot = relMat.to_quaternion()
  2042.                     rot_vec = Vector(rot.to_euler())
  2043.  
  2044.                     if not prev_pos.get(bone) or pos - prev_pos[bone] > epsilon:
  2045.                         skip_time = skipped_pos.get(bone)
  2046.                         if skip_time != None:
  2047.                             channel[0]["times"].append(skip_time)
  2048.                             channel[0]["values"].append(channel[0]["values"][-1])
  2049.                             del skipped_pos[bone]
  2050.  
  2051.                         channel[0]["times"].append(keyframe_time)
  2052.                         channel[0]["values"].append(datamodel.Vector3(pos))
  2053.                     else:
  2054.                         skipped_pos[bone] = keyframe_time
  2055.  
  2056.                    
  2057.                     if not prev_rot.get(bone) or rot_vec - prev_rot[bone] > epsilon:
  2058.                         skip_time = skipped_rot.get(bone)
  2059.                         if skip_time != None:
  2060.                             channel[1]["times"].append(skip_time)
  2061.                             channel[1]["values"].append(channel[1]["values"][-1])
  2062.                             del skipped_rot[bone]
  2063.  
  2064.                         channel[1]["times"].append(keyframe_time)
  2065.                         channel[1]["values"].append(getDatamodelQuat(rot))
  2066.                     else:
  2067.                         skipped_rot[bone] = keyframe_time
  2068.  
  2069.                     prev_pos[bone] = pos
  2070.                     prev_rot[bone] = rot_vec
  2071.                    
  2072.                 if two_percent and frame % two_percent:
  2073.                     print(".",debug_only=True,newline=False)
  2074.             print(debug_only=True)
  2075.        
  2076.         bpy.context.window_manager.progress_update(0.99)
  2077.         print("- Writing DMX...")
  2078.         try:
  2079.             if bpy.context.scene.vs.use_kv2:
  2080.                 dm.write(filepath,"keyvalues2",1)
  2081.             else:
  2082.                 dm.write(filepath,"binary",State.datamodelEncoding)
  2083.             written += 1
  2084.         except (PermissionError, FileNotFoundError) as err:
  2085.             self.error(get_id("exporter_err_open", True).format("DMX",err))
  2086.  
  2087.         bench.report("write")
  2088.         if bench.quiet:
  2089.             print("- DMX export took",bench.total(),"\n")
  2090.        
  2091.         return written
  2092.  
Tags: blender
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement