├── .gitattributes ├── LICENSE ├── README.md └── advancedfx ├── __init__.py ├── export_agr2fbx.py ├── export_bvh.py ├── export_cam.py ├── import_agr.py ├── import_bvh.py ├── import_cam.py ├── readme.txt └── utils.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 advancedfx.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # afx-blender-scripts 2 | Scripts for Blender by the advancedfx.org project 3 | 4 | ## Download 5 | 6 | Downloads are on the [releases page](https://github.com/ripieces/afx-blender-scripts/releases). 7 | 8 | ## Instructions 9 | 10 | Please read the [readme.txt](https://github.com/ripieces/afx-blender-scripts/blob/master/advancedfx/readme.txt) included with each download. 11 | -------------------------------------------------------------------------------- /advancedfx/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | bl_info = { 4 | "name": "advancedfx Blender Scripts", 5 | "author": "advancedfx.org", 6 | "version": (1, 14, 6), 7 | "blender": (3, 5, 0), 8 | "location": "File > Import/Export", 9 | "description": "For inter-operation with HLAE.", 10 | #"warning": "", 11 | "doc_url": "https://github.com/advancedfx/advancedfx/wiki/Source:mirv_agr", 12 | "tracker_url": "https://github.com/advancedfx/afx-blender-scripts/issues", 13 | "category": "Import-Export", 14 | } 15 | 16 | from . import utils, import_agr, import_cam, export_cam, import_bvh, export_bvh, export_agr2fbx 17 | 18 | classes = ( 19 | import_bvh.BvhImporter, 20 | export_bvh.BvhExporter, 21 | import_cam.CamImporter, 22 | export_cam.CamExporter, 23 | import_agr.AgrImporter, 24 | export_agr2fbx.AgrExport, 25 | ) 26 | 27 | def menu_func_import_agr(self, context): 28 | self.layout.operator(import_agr.AgrImporter.bl_idname, text="HLAE afxGameRecord (.agr)") 29 | 30 | def menu_func_export_agr2fbx(self, context): 31 | self.layout.operator(export_agr2fbx.AgrExport.bl_idname, text="HLAE AGR Batch Export (.fbx)") 32 | 33 | def menu_func_import_cam(self, context): 34 | self.layout.operator(import_cam.CamImporter.bl_idname, text="HLAE Camera IO (.cam)") 35 | 36 | def menu_func_export_cam(self, context): 37 | self.layout.operator(export_cam.CamExporter.bl_idname, text="HLAE Camera IO (.cam)") 38 | 39 | def menu_func_import_bvh(self, context): 40 | self.layout.operator(import_bvh.BvhImporter.bl_idname, text="HLAE old Cam IO (.bvh)") 41 | 42 | def menu_func_export_bvh(self, context): 43 | self.layout.operator(export_bvh.BvhExporter.bl_idname, text="HLAE old Cam IO (.bvh)") 44 | 45 | def register(): 46 | from bpy.utils import register_class 47 | for cls in classes: 48 | register_class(cls) 49 | 50 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import_agr) 51 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export_agr2fbx) 52 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import_cam) 53 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export_cam) 54 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import_bvh) 55 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export_bvh) 56 | 57 | def unregister(): 58 | from bpy.utils import unregister_class 59 | for cls in reversed(classes): 60 | unregister_class(cls) 61 | 62 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_bvh) 63 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_bvh) 64 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import_agr) 65 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export_agr2fbx) 66 | 67 | if __name__ == "__main__": 68 | unregister() 69 | register() -------------------------------------------------------------------------------- /advancedfx/export_agr2fbx.py: -------------------------------------------------------------------------------- 1 | # Thanks to Darkhandrob for doing the base function 2 | # https://github.com/darkhandrob 3 | 4 | import bpy, bpy.props, bpy.ops, time 5 | 6 | class AgrExport(bpy.types.Operator): 7 | """Exports every models with its animation as a FBX""" 8 | bl_idname = "advancedfx.agr_to_fbx" 9 | bl_label = "HLAE afxGameRecord" 10 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 11 | 12 | filepath: bpy.props.StringProperty(subtype="DIR_PATH") 13 | 14 | global_scale: bpy.props.FloatProperty( 15 | name="Scale", 16 | description="Scale everything by this value", 17 | min=0.000001, max=100000.0, 18 | soft_min=0.1, soft_max=10.0, 19 | default=1, 20 | ) 21 | 22 | root_name: bpy.props.StringProperty( 23 | name="Root Bone Name", 24 | description="Set the root bone name for each model", 25 | default="root", 26 | ) 27 | 28 | skip_meshes: bpy.props.BoolProperty( 29 | name="Skip Meshes", 30 | description="Skips mesh export for faster export", 31 | default=True, 32 | ) 33 | 34 | def menu_draw_export(self, context): 35 | layout = self.layout 36 | layout.operator("advancedfx.agr_to_fbx", text="HLAE afxGameRecord") 37 | 38 | def invoke(self, context, event): 39 | context.window_manager.fileselect_add(self) 40 | return {'RUNNING_MODAL'} 41 | 42 | def execute(self, context): 43 | time_start = time.time() 44 | # Change Filepath, if something got insert in the File Name box 45 | if not self.filepath.endswith("\\"): 46 | self.filepath = self.filepath.rsplit(sep="\\", maxsplit=1)[0] + "\\" 47 | 48 | # export model 49 | bpy.ops.object.select_all(action='DESELECT') 50 | for CurrentModel in bpy.data.objects: 51 | if CurrentModel.name.find("afx.") != -1: 52 | for o in context.scene.objects: 53 | if o.name == CurrentModel.name: 54 | # select root 55 | CurrentModel.select_set(1) 56 | # select childrens 57 | for CurrentChildren in CurrentModel.children: 58 | CurrentChildren.select_set(1) 59 | # rename top to root 60 | CurrentObjectName = CurrentModel.name 61 | CurrentModel.name = self.root_name 62 | # export single objects as fbx 63 | fullfiles = self.filepath + "/" + CurrentObjectName + ".fbx" 64 | if self.skip_meshes: 65 | bpy.ops.export_scene.fbx( 66 | filepath = fullfiles, 67 | object_types={'ARMATURE'}, 68 | use_selection = True, 69 | global_scale = self.global_scale, 70 | bake_anim_use_nla_strips = False, 71 | bake_anim_use_all_actions = False, 72 | bake_anim_simplify_factor = 0, 73 | add_leaf_bones=False) 74 | else: 75 | bpy.ops.export_scene.fbx( 76 | filepath = fullfiles, 77 | object_types={'ARMATURE', 'MESH'}, 78 | use_selection = True, 79 | global_scale = self.global_scale, 80 | bake_anim_use_nla_strips = False, 81 | bake_anim_use_all_actions = False, 82 | bake_anim_simplify_factor = 0, 83 | add_leaf_bones=False) 84 | # undo all changes 85 | CurrentModel.name = CurrentObjectName 86 | CurrentModel.select_set(0) 87 | for CurrentChildren in CurrentModel.children: 88 | CurrentChildren.select_set(0) 89 | 90 | # export camera 91 | for CameraData in bpy.data.objects: 92 | if any(CameraData.name.startswith(c) for c in ("afxCam", "camera")): 93 | # select camera 94 | CameraData.select_set(1) 95 | # scale camera 96 | bpy.ops.transform.resize(value=(0.01, 0.01, 0.01)) 97 | # export single cameras as fbx 98 | fullfiles = self.filepath + "/" + CameraData.name + ".fbx" 99 | bpy.ops.export_scene.fbx( 100 | filepath = fullfiles, 101 | object_types={'CAMERA'}, 102 | use_selection = True, 103 | global_scale = self.global_scale, 104 | bake_anim_use_nla_strips = False, 105 | bake_anim_use_all_actions = False, 106 | bake_anim_simplify_factor = 0) 107 | # undo all changes 108 | bpy.ops.transform.resize(value=(100, 100, 100)) 109 | CameraData.select_set(0) 110 | 111 | print("FBX-Export script finished in %.4f sec." % (time.time() - time_start)) 112 | return {'FINISHED'} 113 | -------------------------------------------------------------------------------- /advancedfx/export_bvh.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import struct 5 | 6 | import bpy, bpy.props, bpy.ops 7 | import mathutils 8 | 9 | from io_scene_valvesource import utils as vs_utils 10 | 11 | from .utils import QAngle 12 | 13 | # Formats a float value to be suitable for bvh output 14 | def FloatToBvhString(value): 15 | return "{0:f}".format(value) 16 | 17 | def WriteHeader(file, frames, frameTime): 18 | file.write("HIERARCHY\n") 19 | file.write("ROOT MdtCam\n") 20 | file.write("{\n") 21 | file.write("\tOFFSET 0.00 0.00 0.00\n") 22 | file.write("\tCHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n") 23 | file.write("\tEnd Site\n") 24 | file.write("\t{\n") 25 | file.write("\t\tOFFSET 0.00 0.00 -1.00\n") 26 | file.write("\t}\n") 27 | file.write("}\n") 28 | file.write("MOTION\n") 29 | file.write("Frames: "+str(frames)+"\n") 30 | file.write("Frame Time: "+FloatToBvhString(frameTime)+"\n") 31 | 32 | class BvhExporter(bpy.types.Operator, vs_utils.Logger): 33 | bl_idname = "advancedfx.bvhexporter" 34 | bl_label = "HLAE old Cam IO (.bvh)" 35 | bl_options = {'UNDO'} 36 | 37 | # Properties used by the file browser 38 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 39 | filter_glob: bpy.props.StringProperty(default="*.bvh", options={'HIDDEN'}) 40 | 41 | # Custom properties 42 | global_scale: bpy.props.FloatProperty( 43 | name="Scale", 44 | description="Scale everything by this value", 45 | min=0.000001, max=1000000.0, 46 | soft_min=1.0, soft_max=1000.0, 47 | default=100.0, 48 | ) 49 | 50 | frame_start: bpy.props.IntProperty( 51 | name="Start Frame", 52 | description="Starting frame to export", 53 | default=0, 54 | ) 55 | frame_end: bpy.props.IntProperty( 56 | name="End Frame", 57 | description="End frame to export", 58 | default=0, 59 | ) 60 | 61 | def __init__(self, *args, **kwargs): 62 | super().__init__(*args, **kwargs) 63 | vs_utils.Logger.__init__(self) 64 | 65 | def execute(self, context): 66 | ok = self.writeBvh(context) 67 | 68 | self.errorReport("Error report") 69 | 70 | return {'FINISHED'} 71 | 72 | def invoke(self, context, event): 73 | self.frame_start = context.scene.frame_start 74 | self.frame_end = context.scene.frame_end 75 | 76 | bpy.context.window_manager.fileselect_add(self) 77 | 78 | return {'RUNNING_MODAL'} 79 | 80 | def writeBvh(self, context): 81 | scene = context.scene 82 | frame_current = scene.frame_current 83 | fps = context.scene.render.fps 84 | 85 | obj = context.active_object 86 | 87 | if obj is None: 88 | self.error("No object selected.") 89 | return False 90 | 91 | lastRot = None 92 | 93 | mRot = mathutils.Matrix.Rotation(math.radians(-90.0 if "CAMERA" == obj.type else 0.0), 4, 'X') 94 | mTrans = mathutils.Matrix.Scale(-1,4,(-1.0, 0.0, 0.0)) 95 | 96 | file = None 97 | 98 | try: 99 | file = open(self.filepath, "w", encoding="utf8", newline="\n") 100 | 101 | frameCount = self.frame_end -self.frame_start +1 102 | if frameCount < 0: frameCount = 0 103 | 104 | frameTime = 1.0 105 | if 0.0 != fps: frameTime = frameTime / fps 106 | 107 | WriteHeader(file, frameCount, frameTime) 108 | 109 | for frame in range(self.frame_start, self.frame_end + 1): 110 | scene.frame_set(frame) 111 | 112 | mat = obj.matrix_world 113 | mat = mat @ mRot 114 | 115 | loc = mat.to_translation() 116 | 117 | rot = mat.to_euler('YXZ') if lastRot is None else mat.to_euler('YXZ', lastRot) 118 | lastRot = rot 119 | 120 | 121 | 122 | X = -(-loc[0]) * self.global_scale 123 | Y = loc[2] * self.global_scale 124 | Z = -loc[1] * self.global_scale 125 | 126 | XR = -math.degrees(-rot[0]) 127 | YR = math.degrees( rot[2]) 128 | ZR = -math.degrees( rot[1]) 129 | 130 | S = "" +FloatToBvhString(X) +" " +FloatToBvhString(Y) +" " +FloatToBvhString(Z) +" " +FloatToBvhString(ZR) +" " +FloatToBvhString(XR) +" " +FloatToBvhString(YR) + "\n" 131 | file.write(S) 132 | 133 | finally: 134 | if file is not None: 135 | file.close() 136 | 137 | scene.frame_set(frame_current) 138 | 139 | return True 140 | -------------------------------------------------------------------------------- /advancedfx/export_cam.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import struct 5 | 6 | import bpy, bpy.props, bpy.ops 7 | import mathutils 8 | 9 | from io_scene_valvesource import utils as vs_utils 10 | 11 | # Formats a float value to be suitable for bvh output 12 | def FloatToBvhString(value): 13 | return "{0:f}".format(value) 14 | 15 | def WriteHeader(file, frames, frameTime): 16 | file.write("advancedfx Cam\n") 17 | file.write("version 2\n") 18 | file.write("channels time xPosition yPosition zPositon xRotation yRotation zRotation fov\n") 19 | file.write("DATA\n") 20 | 21 | 22 | class CamExporter(bpy.types.Operator, vs_utils.Logger): 23 | bl_idname = "advancedfx.camexporter" 24 | bl_label = "HLAE Camera IO (.cam)" 25 | bl_options = {'UNDO'} 26 | 27 | # Properties used by the file browser 28 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 29 | filter_glob: bpy.props.StringProperty(default="*.cam", options={'HIDDEN'}) 30 | 31 | # Custom properties 32 | global_scale: bpy.props.FloatProperty( 33 | name="Scale", 34 | description="Scale everything by this value", 35 | min=0.000001, max=1000000.0, 36 | soft_min=1.0, soft_max=1000.0, 37 | default=100.0, 38 | ) 39 | 40 | frame_start: bpy.props.IntProperty( 41 | name="Start Frame", 42 | description="Starting frame to export", 43 | default=0, 44 | ) 45 | frame_end: bpy.props.IntProperty( 46 | name="End Frame", 47 | description="End frame to export", 48 | default=0, 49 | ) 50 | 51 | def __init__(self, *args, **kwargs): 52 | super().__init__(*args, **kwargs) 53 | vs_utils.Logger.__init__(self) 54 | 55 | def execute(self, context): 56 | ok = self.writeBvh(context) 57 | 58 | self.errorReport("Error report") 59 | 60 | return {'FINISHED'} 61 | 62 | def invoke(self, context, event): 63 | self.frame_start = context.scene.frame_start 64 | self.frame_end = context.scene.frame_end 65 | 66 | bpy.context.window_manager.fileselect_add(self) 67 | 68 | return {'RUNNING_MODAL'} 69 | 70 | def writeBvh(self, context): 71 | scene = context.scene 72 | frame_current = scene.frame_current 73 | fps = context.scene.render.fps 74 | 75 | obj = context.active_object 76 | 77 | if (obj is None) or (obj.type != 'CAMERA'): 78 | self.error("No camera selected.") 79 | return False 80 | 81 | cam = obj.data 82 | 83 | lastRot = None 84 | 85 | unRot = mathutils.Matrix.Rotation(math.radians(-90.0), 4, 'X') 86 | 87 | file = None 88 | 89 | try: 90 | file = open(self.filepath, "w", encoding="utf8", newline="\n") 91 | 92 | frameCount = self.frame_end -self.frame_start +1 93 | if frameCount < 0: frameCount = 0 94 | 95 | frameTime = 1.0 96 | if 0.0 != fps: frameTime = frameTime / fps 97 | 98 | WriteHeader(file, frameCount, frameTime) 99 | 100 | for frame in range(self.frame_start, self.frame_end + 1): 101 | scene.frame_set(frame) 102 | 103 | mat = obj.matrix_world 104 | mat = mat @ unRot 105 | 106 | loc = mat.to_translation() 107 | rot = mat.to_euler('YXZ') if lastRot is None else mat.to_euler('YXZ', lastRot) 108 | lastRot = rot 109 | 110 | loc = self.global_scale * mathutils.Vector((loc[1],-loc[0],loc[2])) 111 | 112 | qAngleVec = mathutils.Vector((math.degrees(rot[1]),-math.degrees(rot[0]),math.degrees(rot[2]))) 113 | 114 | # lens = camData.c.sensor_width / (2.0 * math.tan(math.radians(fov) / 2.0)) 115 | fov = math.degrees(2.0 * math.atan((cam.sensor_width / cam.lens) / 2.0)) 116 | 117 | S = ""+FloatToBvhString((frame-1) * frameTime) +" " +FloatToBvhString(loc[0]) +" " +FloatToBvhString(loc[1]) +" " +FloatToBvhString(loc[2]) +" " +FloatToBvhString(qAngleVec[0]) +" " +FloatToBvhString(qAngleVec[1]) +" " +FloatToBvhString(qAngleVec[2]) +" " +FloatToBvhString(fov) + "\n" 118 | file.write(S) 119 | 120 | finally: 121 | if file is not None: 122 | file.close() 123 | 124 | scene.frame_set(frame_current) 125 | 126 | return True 127 | -------------------------------------------------------------------------------- /advancedfx/import_agr.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import struct 5 | import copy 6 | from collections import defaultdict 7 | 8 | import traceback 9 | 10 | import bpy, bpy.props, bpy.ops, time 11 | import mathutils 12 | 13 | from os.path import splitext, basename 14 | 15 | from io_scene_valvesource import import_smd as vs_import_smd, utils as vs_utils 16 | 17 | from advancedfx import utils as afx_utils 18 | 19 | class GAgrImporter: 20 | onlyBones = False 21 | smd = None 22 | 23 | class SmdImporterEx(vs_import_smd.SmdImporter): 24 | bl_idname = "advancedfx.smd_importer_ex" 25 | 26 | qc = None 27 | smd = None 28 | bSkip = False 29 | 30 | # Properties used by the file browser 31 | filepath : bpy.props.StringProperty(name="File Path", description="File filepath used for importing the SMD/VTA/DMX/QC file", maxlen=1024, default="", options={'HIDDEN'}) 32 | files : bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN'}) 33 | directory : bpy.props.StringProperty(maxlen=1024, default="", subtype='FILE_PATH', options={'HIDDEN'}) 34 | filter_folder : bpy.props.BoolProperty(name="Filter Folders", description="", default=True, options={'HIDDEN'}) 35 | filter_glob : bpy.props.StringProperty(default="*.smd;*.vta;*.dmx;*.qc;*.qci", options={'HIDDEN'}) 36 | 37 | # Custom properties 38 | doAnim : bpy.props.BoolProperty(name="importer_doanims", default=True) 39 | createCollections : bpy.props.BoolProperty(name="importer_use_collections", description="importer_use_collections_tip", default=False) 40 | makeCamera : bpy.props.BoolProperty(name="importer_makecamera",description="importer_makecamera_tip",default=False) 41 | append : bpy.props.EnumProperty(name="importer_bones_mode",description="importer_bones_mode_desc",items=( 42 | ('VALIDATE',"importer_bones_validate","importer_bones_validate_desc"), 43 | ('APPEND',"importer_bones_append","importer_bones_append_desc"), 44 | ('NEW_ARMATURE',"importer_bones_newarm","importer_bones_newarm_desc")), 45 | default='APPEND') 46 | upAxis : bpy.props.EnumProperty(name="Up Axis",items=vs_utils.axes,default='Z',description="importer_up_tip") 47 | rotMode : bpy.props.EnumProperty(name="importer_rotmode",items=( ('XYZ', "Euler", ''), ('QUATERNION', "Quaternion", "") ),default='XYZ',description="importer_rotmode_tip") 48 | boneMode : bpy.props.EnumProperty(name="importer_bonemode",items=(('NONE','Default',''),('ARROWS','Arrows',''),('SPHERE','Sphere','')),default='SPHERE',description="importer_bonemode_tip") 49 | 50 | def execute(self, context): 51 | self.existingBones = [] 52 | self.num_files_imported = 0 53 | self.readQC(self.filepath, False, False, False, 'XYZ', outer_qc = True) 54 | GAgrImporter.smd = self.smd 55 | return {'FINISHED'} 56 | 57 | def readPolys(self): 58 | if GAgrImporter.onlyBones: 59 | return 60 | super(SmdImporterEx, self).readPolys() 61 | 62 | def readShapes(self): 63 | if GAgrImporter.onlyBones: 64 | return 65 | super(SmdImporterEx, self).readShapes() 66 | 67 | def readSMD(self, filepath, upAxis, rotMode, newscene = False, smd_type = None, target_layer = 0): 68 | s = splitext(basename(filepath))[0].rstrip("123456789") 69 | if SmdImporterEx.bSkip and (smd_type == vs_utils.PHYS or s.endswith("_lod") or any(filepath.endswith(u) for u in ("skeleto.smd", "skel.smd"))): 70 | return 0 71 | else: 72 | return super().readSMD(filepath, upAxis, rotMode, newscene, smd_type, target_layer) # call parent method 73 | 74 | def ReadString(file): 75 | buf = bytearray() 76 | while True: 77 | b = file.read(1) 78 | if len(b) < 1: 79 | return None 80 | elif b == b"\0": 81 | return buf.decode("utf-8") 82 | else: 83 | buf.append(b[0]) 84 | 85 | def ReadBool(file): 86 | buf = file.read(1) 87 | if(len(buf) < 1): 88 | return None 89 | return struct.unpack('= error) or (0.001 < error): 472 | self.errorCount = self.errorCount + 1 473 | if (self.maxError is None) or (abs(self.maxError) < abs(error)): 474 | self.maxError = error 475 | 476 | def FrameEnd(self): 477 | self.newTime = self.time + self.frameTime 478 | 479 | def GetTime(self): 480 | return 1.0 + self.time * self.fps 481 | 482 | class AgrImporter(bpy.types.Operator, vs_utils.Logger): 483 | bl_idname = "advancedfx.agrimporter" 484 | bl_label = "HLAE afxGameRecord (.agr)" 485 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 486 | 487 | # Properties used by the file browser 488 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 489 | filter_glob: bpy.props.StringProperty(default="*.agr", options={'HIDDEN'}) 490 | 491 | # Custom properties 492 | assetPath: bpy.props.StringProperty( 493 | name="Asset Path", 494 | description="Directory path containing the (decompiled) assets in a folder structure as in the pak01_dir.pak.", 495 | default="", 496 | #subtype = 'DIR_PATH' 497 | ) 498 | 499 | interKey: bpy.props.BoolProperty( 500 | name="Add interpolated key frames", 501 | description="Create interpolated key frames for frames in-between the original key frames.", 502 | default=False) 503 | 504 | global_scale: bpy.props.FloatProperty( 505 | name="Scale", 506 | description="Scale everything by this value (0.01 old default, 0.0254 is more accurate)", 507 | min=0.000001, max=1000000.0, 508 | soft_min=0.001, soft_max=1.0, 509 | default=0.01, 510 | ) 511 | 512 | scaleInvisibleZero: bpy.props.BoolProperty( 513 | name="Scale invisible to zero", 514 | description="If set entities will scaled to zero when not visible.", 515 | default=False, 516 | ) 517 | 518 | bSkip: bpy.props.BoolProperty( 519 | name="Skip Physic, LOD and Shared_Player_Skeleton meshes", 520 | description="Skips the import of physic (collision) meshes if the .qc contains them.", 521 | default = True 522 | ) 523 | 524 | aSkip: bpy.props.BoolProperty( 525 | name="Skip Stattrack and Stickers", 526 | description="Skips the import of Stattrack and Sticker meshes if the .qc contains them.", 527 | default = True 528 | ) 529 | 530 | onlyBones: bpy.props.BoolProperty( 531 | name="Bones (skeleton) only", 532 | description="Import only bones (skeletons) (faster).", 533 | default=False) 534 | 535 | modelInstancing: bpy.props.BoolProperty( 536 | name="Model instancing", 537 | description="Objects with same model are instanced, animation data is separate and modifiers duplicated (faster). Recommended to disable it for beginners, who want to export it to other 3D application", 538 | default=True) 539 | 540 | keyframeInterpolation: bpy.props.EnumProperty( 541 | name="Keyframe interpolation", 542 | description="Constant recommended for beginners." if afx_utils.NEWER_THAN_290 else "Constant recommended for beginners. Advanced users can choose Bezier for significantly faster import times.", 543 | items=[ 544 | ('CONSTANT', "Constant (recommended)", "No interpolation"), 545 | ('LINEAR', "Linear", "Linear interpolation"), 546 | ('BEZIER', "Bezier" if afx_utils.NEWER_THAN_290 else "Bezier (fast import)", "Smooth interpolation"), 547 | ], 548 | default='CONSTANT' 549 | ) 550 | 551 | # class properties 552 | valveMatrixToBlender = mathutils.Matrix.Rotation(math.pi/2,4,'Z') 553 | blenderCamUpQuat = mathutils.Quaternion((math.cos(0.5 * math.radians(90.0)), math.sin(0.5* math.radians(90.0)), 0.0, 0.0)) 554 | 555 | def __init__(self, *args, **kwargs): 556 | super().__init__(*args, **kwargs) 557 | vs_utils.Logger.__init__(self) 558 | 559 | def execute(self, context): 560 | time_start = time.time() 561 | result = None 562 | try: 563 | bpy.utils.unregister_class(vs_import_smd.SmdImporter) 564 | bpy.utils.register_class(SmdImporterEx) 565 | result = self.readAgr(context) 566 | finally: 567 | bpy.utils.unregister_class(SmdImporterEx) 568 | bpy.utils.register_class(vs_import_smd.SmdImporter) 569 | 570 | for area in context.screen.areas: 571 | if area.type == 'VIEW_3D': 572 | space = area.spaces.active 573 | #space.grid_lines = 64 574 | #space.grid_scale = self.global_scale * 512 575 | #space.grid_subdivisions = 8 576 | space.clip_end = self.global_scale * 56756 577 | 578 | self.errorReport("Error report") 579 | 580 | if result is not None: 581 | if result['frameBegin'] is not None: 582 | bpy.context.scene.frame_start = result['frameBegin'] 583 | if result['frameEnd'] is not None: 584 | bpy.context.scene.frame_end = result['frameEnd'] 585 | 586 | print("AGR import finished in %.4f sec." % (time.time() - time_start)) 587 | return {'FINISHED'} 588 | 589 | def invoke(self, context, event): 590 | bpy.context.window_manager.fileselect_add(self) 591 | return {'RUNNING_MODAL'} 592 | 593 | def addCurvesToModel(self, context, modelData): 594 | 595 | a = modelData.smd.a 596 | 597 | # Create actions and their curves (boobs): 598 | #vs_utils.select_only(a) 599 | 600 | a.animation_data_create() 601 | action = bpy.data.actions.new(name="game_data") 602 | a.animation_data.action = action 603 | 604 | if afx_utils.NEWER_THAN_440: 605 | action.slots.new(id_type='OBJECT', name="game_data") 606 | a.animation_data.action_slot = action.slots[0] 607 | 608 | modelData.curves.append(action.fcurves.new("hide_render")) 609 | 610 | for i in range(3): 611 | modelData.curves.append(action.fcurves.new("location",index = i)) 612 | 613 | for i in range(4): 614 | modelData.curves.append(action.fcurves.new("rotation_quaternion",index = i)) 615 | 616 | for i in range(3): 617 | modelData.curves.append(action.fcurves.new("scale",index = i)) 618 | 619 | num_bones = len(a.pose.bones) 620 | 621 | for i in range(num_bones): 622 | bone = a.pose.bones[modelData.smd.boneIDs[i]] 623 | 624 | bone_string = "pose.bones[\"{}\"].".format(bone.name) 625 | 626 | for j in range(3): 627 | modelData.curves.append(action.fcurves.new(bone_string + "location",index = j)) 628 | 629 | for j in range(4): 630 | modelData.curves.append(action.fcurves.new(bone_string + "rotation_quaternion",index = j)) 631 | 632 | for j in range(3): 633 | modelData.curves.append(action.fcurves.new(bone_string + "scale",index = j)) 634 | 635 | # Create visibility driver: 636 | 637 | for child in a.children: 638 | d = child.driver_add('hide_render').driver 639 | d.type = 'AVERAGE' 640 | v = d.variables.new() 641 | v.name = 'hide_render' 642 | v.targets[0].id = a 643 | v.targets[0].data_path = 'hide_render' 644 | 645 | if self.scaleInvisibleZero: 646 | 647 | ds = child.driver_add('scale') 648 | 649 | for df in ds: 650 | d = df.driver 651 | d.type = 'SCRIPTED' 652 | d.use_self = False 653 | h = d.variables.new() 654 | h.name = 'hide_render' 655 | h.targets[0].id = a 656 | h.targets[0].data_path = 'hide_render' 657 | d.expression = "1-hide_render" 658 | 659 | 660 | return modelData 661 | 662 | def importModel(self, context, modelHandle): 663 | 664 | def makeModelName(modelHandle): 665 | name = modelHandle.modelName.rsplit('/',1) 666 | name = name[len(name) -1] 667 | name = (name[:30] + '..') if len(name) > 30 else name 668 | name = "afx." +str(modelHandle.objNr)+ " " + name 669 | return name 670 | 671 | def copyObj(src,parent=None): 672 | dst = src.copy() 673 | dst.animation_data_clear() 674 | dst.modifiers.clear() 675 | 676 | for srcMod in src.modifiers: 677 | 678 | dstMod = dst.modifiers.new(srcMod.name, srcMod.type) 679 | 680 | #collect names of writable properties 681 | properties = [p.identifier for p in srcMod.bl_rna.properties 682 | if not p.is_readonly] 683 | 684 | # copy those properties 685 | for prop in properties: 686 | setattr(dstMod, prop, getattr(srcMod, prop)) 687 | 688 | if (srcMod.name == 'Armature') and (srcMod.object == src.parent): 689 | dstMod.object = parent 690 | 691 | bpy.context.scene.collection.objects.link(dst) 692 | 693 | for srcChild in src.children: 694 | dstChild = copyObj(srcChild,dst) 695 | dstChild.parent = dst 696 | dstChild.matrix_parent_inverse = srcChild.matrix_parent_inverse.copy() 697 | 698 | return dst 699 | 700 | modelData = None 701 | 702 | if self.modelInstancing: 703 | modelData = self.modelObjects.pop(modelHandle.modelName, None) 704 | 705 | if modelData is None: 706 | # No instance we are allowed to use, so import it for real: 707 | 708 | filePath = self.assetPath.rstrip("/\\") + "/" +modelHandle.modelName.lower() 709 | filePath = os.path.splitext(filePath)[0] 710 | filePath = filePath + "/" + os.path.basename(filePath).lower() + ".qc" 711 | 712 | SmdImporterEx.bSkip = self.bSkip 713 | GAgrImporter.smd = None 714 | GAgrImporter.onlyBones = self.onlyBones 715 | modelData = None 716 | 717 | try: 718 | if self.aSkip and any(filePath.endswith(a) for a in ("stattrack.qc", "decal_a.qc", "decal_b.qc", "decal_c.qc", "decal_d.qc", "decal_e.qc")): 719 | return 720 | bpy.ops.advancedfx.smd_importer_ex(filepath=filePath, doAnim=False) 721 | modelData = ModelData(GAgrImporter.smd) 722 | except Exception as e: 723 | if '?.qc' in str(e): 724 | pass 725 | else: 726 | self.error("Failed to import \""+filePath+"\".") 727 | return None 728 | finally: 729 | GAgrImporter.smd = None 730 | 731 | armature = modelData.smd.a 732 | 733 | # Update name: 734 | armature.name = makeModelName(modelHandle) 735 | 736 | # Fix rotation: 737 | if armature.rotation_mode != 'QUATERNION': 738 | armature.rotation_mode = 'QUATERNION' 739 | for bone in armature.pose.bones: 740 | if bone.rotation_mode != 'QUATERNION': 741 | bone.rotation_mode = 'QUATERNION' 742 | 743 | # Scale: 744 | 745 | armature.scale[0] = self.global_scale 746 | armature.scale[1] = self.global_scale 747 | armature.scale[2] = self.global_scale 748 | 749 | # Insert into instance dictionary: 750 | self.modelObjects[modelHandle.modelName] = modelData 751 | 752 | else: 753 | print("Instancing %i (%s)." % (modelHandle.objNr,modelHandle.modelName)) 754 | modelData = copy.copy(modelData) 755 | 756 | modelData.smd = copy.copy(modelData.smd) 757 | modelData.smd.a = copyObj(modelData.smd.a) 758 | modelData.smd.a.name = makeModelName(modelHandle) 759 | 760 | modelData.curves = [] 761 | 762 | modelData = self.addCurvesToModel(context, modelData) 763 | 764 | return modelData 765 | 766 | def createCamera(self, context, camName): 767 | 768 | c = bpy.data.cameras.new(camName) 769 | o = bpy.data.objects.new(camName, c) 770 | 771 | context.scene.collection.objects.link(o) 772 | 773 | #vs_utils.select_only(o) 774 | 775 | camData = CameraData(o,c) 776 | 777 | # Rotation mode: 778 | if o.rotation_mode != 'QUATERNION': 779 | o.rotation_mode = 'QUATERNION' 780 | 781 | 782 | # Create actions and their curves: 783 | 784 | o.animation_data_create() 785 | action = bpy.data.actions.new(name="game_data") 786 | o.animation_data.action = action 787 | 788 | if afx_utils.NEWER_THAN_440: 789 | slot = action.slots.new(id_type='OBJECT', name="agr") 790 | o.animation_data.action_slot = slot 791 | 792 | for i in range(3): 793 | camData.curves.append(action.fcurves.new("location",index = i)) 794 | 795 | for i in range(4): 796 | camData.curves.append(action.fcurves.new("rotation_quaternion",index = i)) 797 | 798 | c.animation_data_create() 799 | action = bpy.data.actions.new(name="game_data") 800 | c.animation_data.action = action 801 | 802 | if afx_utils.NEWER_THAN_440: 803 | slot = action.slots.new(id_type='CAMERA', name="agr") 804 | c.animation_data.action_slot = slot 805 | 806 | camData.curves.append(action.fcurves.new("lens")) 807 | 808 | return camData 809 | 810 | def readAgr(self,context): 811 | file = None 812 | result = { 'result': False, 'frameBegin': 1, 'frameEnd': None } 813 | 814 | try: 815 | self.modelObjects = {} 816 | 817 | file = open(self.filepath, 'rb') 818 | 819 | if file is None: 820 | self.error('Could not open file.') 821 | return result 822 | 823 | file.seek(0, 2) 824 | fileSize = file.tell() 825 | file.seek(0, 0) 826 | 827 | context.window_manager.progress_begin(0.0, 1.0) 828 | 829 | version = ReadAgrVersion(file) 830 | 831 | if version is None: 832 | self.error('Invalid file format.') 833 | return result 834 | 835 | if (5 != version and version != 6): 836 | self.error('Version '+str(version)+' is not supported!') 837 | return result 838 | 839 | timeConverter = AgrTimeConverter(context) 840 | currentTime = timeConverter.GetTime() 841 | dictionary = AgrDictionary() 842 | handleToLastModelHandle = {} 843 | unusedModelHandles = [] 844 | camData = None 845 | 846 | modelHandles = [] 847 | 848 | stupidCount = 0 849 | 850 | objNr = 0 851 | 852 | while True: 853 | 854 | if 0 < fileSize and 0 == stupidCount % 100: 855 | val = float(file.tell())/float(fileSize) 856 | context.window_manager.progress_update(val * 0.5) 857 | print("AGR Read %f%%" % (100*val)) 858 | 859 | stupidCount = stupidCount +1 860 | 861 | if 4096 <= stupidCount: 862 | stupidCount = 0 863 | gc.collect() 864 | #break 865 | 866 | node0 = dictionary.Read(file) 867 | 868 | if node0 is None: 869 | break 870 | 871 | elif 'afxFrame' == node0: 872 | frameTime = ReadFloat(file) 873 | 874 | timeConverter.Frame(frameTime) 875 | currentTime = timeConverter.GetTime() 876 | 877 | afxHiddenOffset = ReadInt(file) 878 | if afxHiddenOffset: 879 | curOffset = file.tell() 880 | file.seek(afxHiddenOffset -4, 1) 881 | 882 | numHidden = ReadInt(file) 883 | for i in range(numHidden): 884 | handle = ReadInt(file) 885 | 886 | modelHandle = handleToLastModelHandle.pop(handle, None) 887 | if modelHandle is not None: 888 | # Make ent invisible: 889 | modelData = modelHandle.modelData 890 | if modelData: # this can happen if the model could not be loaded 891 | modelHandle.UpdateVisible(currentTime, False, self.interKey) 892 | 893 | unusedModelHandles.append(modelHandle) 894 | #print("Marking %i (%s) as hidden/reusable." % (modelHandle.objNr,modelHandle.modelName)) 895 | 896 | file.seek(curOffset,0) 897 | 898 | elif 'afxFrameEnd' == node0: 899 | timeConverter.FrameEnd() 900 | 901 | elif 'afxHidden' == node0: 902 | # skipped, because will be handled earlier by afxHiddenOffset 903 | 904 | numHidden = ReadInt(file) 905 | for i in range(numHidden): 906 | handle = ReadInt(file) 907 | 908 | elif 'deleted' == node0: 909 | handle = ReadInt(file) 910 | modelHandle = handleToLastModelHandle.pop(handle, None) 911 | if modelHandle is not None: 912 | # Make removed ent invisible: 913 | modelData = modelHandle.modelData 914 | if modelData: # this can happen if the model could not be loaded 915 | modelHandle.UpdateVisible(currentTime, False, self.interKey) 916 | 917 | unusedModelHandles.append(modelHandle) 918 | #print("Marking %i (%s) as deleted/reusable." % (modelHandle.objNr,modelHandle.modelName) 919 | 920 | elif 'entity_state' == node0: 921 | visible = None 922 | modelHandle = None 923 | modelData = None 924 | handle = ReadInt(file) 925 | if dictionary.Peekaboo(file,'baseentity'): 926 | 927 | modelName = dictionary.Read(file) 928 | 929 | visible = ReadBool(file) 930 | 931 | if 5 == version: 932 | renderOrigin = ReadVector(file, quakeFormat=True) 933 | renderAngles = ReadQAngle(file) 934 | renderRotQuat = renderAngles.to_quaternion() 935 | renderScale = mathutils.Vector((1,1,1)) 936 | else: 937 | matrix = ReadMatrix3x4(file) 938 | matrix = self.valveMatrixToBlender @ matrix 939 | renderOrigin, renderRotQuat, renderScale = matrix.decompose() 940 | 941 | renderOrigin = renderOrigin * self.global_scale 942 | renderScale = renderScale * self.global_scale 943 | 944 | modelHandle = handleToLastModelHandle.get(handle, None) 945 | 946 | if (modelHandle is not None) and (modelHandle.modelName != modelName): 947 | # Switched model, make old model invisible: 948 | modelData = modelHandle.modelData 949 | if modelData: # this can happen if the model could not be loaded 950 | modelHandle.UpdateVisible(currentTime, False, self.interKey) 951 | 952 | modelHandle = None 953 | 954 | if modelHandle is None: 955 | 956 | # Check if we can reuse s.th. and if not create new one: 957 | 958 | bestIndex = 0 959 | bestLength = 0 960 | 961 | for idx,val in enumerate(unusedModelHandles): 962 | if (val.modelName == modelName) and ((modelHandle is None) or val.lastRenderOrigin is None or ((val.lastRenderOrigin -renderOrigin).length < bestLength)): 963 | modelHandle = val 964 | bestLength = 0 if val.lastRenderOrigin is None else (val.lastRenderOrigin -renderOrigin).length 965 | bestIndex = idx 966 | 967 | if modelHandle is not None: 968 | # Use the one we found: 969 | del unusedModelHandles[bestIndex] 970 | print("Reusing %i (%s)." % (modelHandle.objNr,modelHandle.modelName)) 971 | else: 972 | # If not then create a new one: 973 | objNr = objNr + 1 974 | modelHandle = ModelHandle(objNr, modelName) 975 | print("Creating %i (%s)." % (modelHandle.objNr,modelHandle.modelName)) 976 | modelHandles.append(modelHandle) 977 | 978 | handleToLastModelHandle[handle] = modelHandle 979 | 980 | modelData = modelHandle.modelData 981 | if modelData is False: 982 | # We have not tried to import the model for this (new) handle yet, so try to import it: 983 | modelData = self.importModel(context, modelHandle) 984 | modelHandle.modelData = modelData 985 | 986 | if modelData is not None: 987 | modelHandle.UpdateVisible(currentTime, visible, self.interKey) 988 | modelHandle.UpdateLocation(currentTime, renderOrigin, self.interKey) 989 | modelHandle.UpdateRotation(currentTime, renderRotQuat, self.interKey) 990 | modelHandle.UpdateScale(currentTime, renderScale, self.interKey) 991 | 992 | if dictionary.Peekaboo(file,'baseanimating'): 993 | #skin = ReadInt(file) 994 | #body = ReadInt(file) 995 | #sequence = ReadInt(file) 996 | hasBoneList = ReadBool(file) 997 | if hasBoneList: 998 | numBones = ReadInt(file) 999 | 1000 | bones = {} 1001 | 1002 | for i in range(numBones): 1003 | #pos = file.tell() 1004 | if 5 == version: 1005 | vec = ReadVector(file, quakeFormat=False) 1006 | quat = ReadQuaternion(file, quakeFormat=False) 1007 | matrix = mathutils.Matrix.Translation(vec) @ quat.to_matrix().to_4x4() 1008 | else: 1009 | matrix = ReadMatrix3x4(file) 1010 | 1011 | if (modelData is None): 1012 | continue 1013 | 1014 | #if not(True == visible): 1015 | # # Only key-frame if visible 1016 | # continue 1017 | 1018 | if(i < len(modelData.smd.boneIDs)): 1019 | bone = modelData.smd.a.pose.bones[modelData.smd.boneIDs[i]] 1020 | 1021 | #self.warning(str(pos)+": "+str(i)+"("+bone.name+"): "+str(vec)+" "+str(quat)) 1022 | 1023 | if bone.parent: 1024 | matrix = bone.parent.matrix @ matrix 1025 | else: 1026 | if 5 == version: 1027 | matrix = self.valveMatrixToBlender @ matrix 1028 | 1029 | bone.matrix = matrix 1030 | 1031 | bones[i] = bone 1032 | 1033 | modelHandle.UpdateBones(currentTime,bones,self.interKey) 1034 | 1035 | if dictionary.Peekaboo(file,'camera'): 1036 | thidPerson = ReadBool(file) 1037 | pos = ReadVector(file, quakeFormat=True) 1038 | rot = ReadQAngle(file) 1039 | fov = ReadFloat(file) 1040 | 1041 | modelCamData = modelHandle.camData 1042 | if modelHandle.camData is None: 1043 | modelCamData = self.createCamera(context,"camera."+str(modelHandle.objNr)) 1044 | modelHandle.camData = modelCamData 1045 | 1046 | lens = modelCamData.c.sensor_width / (2.0 * math.tan(math.radians(fov) / 2.0)) 1047 | 1048 | renderOrigin = pos * self.global_scale 1049 | renderRotQuat = rot.to_quaternion() @ self.blenderCamUpQuat 1050 | 1051 | modelCamData.UpdateLens(currentTime, lens, self.interKey) 1052 | modelCamData.UpdateLocation(currentTime, renderOrigin, self.interKey) 1053 | modelCamData.UpdateRotation(currentTime, renderRotQuat, self.interKey) 1054 | 1055 | dictionary.Peekaboo(file,'/') 1056 | 1057 | viewModel = ReadBool(file) 1058 | 1059 | #if modelData is not None: 1060 | # for fc in modelData.curves: 1061 | # fc.update() 1062 | 1063 | elif 'afxCam' == node0: 1064 | 1065 | if camData is None: 1066 | camData = self.createCamera(context,"afxCam") 1067 | 1068 | if camData is None: 1069 | self.error("Failed to create camera.") 1070 | return result 1071 | 1072 | 1073 | renderOrigin = ReadVector(file, quakeFormat=True) 1074 | renderAngles = ReadQAngle(file) 1075 | 1076 | fov = ReadFloat(file) 1077 | 1078 | lens = camData.c.sensor_width / (2.0 * math.tan(math.radians(fov) / 2.0)) 1079 | 1080 | renderOrigin = renderOrigin * self.global_scale 1081 | renderRotQuat = renderAngles.to_quaternion() @ self.blenderCamUpQuat 1082 | 1083 | camData.UpdateLens(currentTime, lens, self.interKey) 1084 | camData.UpdateLocation(currentTime, renderOrigin, self.interKey) 1085 | camData.UpdateRotation(currentTime, renderRotQuat, self.interKey) 1086 | 1087 | else: 1088 | self.warning('Unknown packet at '+str(file.tell())) 1089 | return result 1090 | 1091 | totalFrames = 0 1092 | for modelHandle in modelHandles: 1093 | modelHandle.Update(None,self.interKey) #finish lingering updates 1094 | modelCamData = modelHandle.camData 1095 | if modelCamData is not None: 1096 | modelCamData.Update(None,self.interKey) #finish lingering updates 1097 | totalFrames += len(modelCamData.locationXFrames) * 3 1098 | totalFrames += len(modelCamData.rotationWFrames) * 4 1099 | totalFrames += len(modelCamData.lensFrames) 1100 | totalFrames += len(modelHandle.visibilityFrames) 1101 | totalFrames += len(modelHandle.locationXFrames) * 3 1102 | totalFrames += len(modelHandle.rotationWFrames) * 4 1103 | totalFrames += len(modelHandle.scaleXFrames) * 3 1104 | for i in modelHandle.boneLocationXFrames: 1105 | totalFrames += len(modelHandle.boneLocationXFrames[i]) * 3 1106 | totalFrames += len(modelHandle.boneRotationWFrames[i]) * 4 1107 | totalFrames += len(modelHandle.boneScaleXFrames[i]) * 3 1108 | if camData is not None: 1109 | camData.Update(None,self.interKey) #finish lingering updates 1110 | totalFrames += len(camData.locationXFrames) * 3 1111 | totalFrames += len(camData.rotationWFrames) * 4 1112 | totalFrames += len(camData.lensFrames) 1113 | 1114 | importedFrames = 0 1115 | 1116 | def updateImportProgress(newFrames): 1117 | nonlocal importedFrames 1118 | importedFrames += newFrames 1119 | val = importedFrames / totalFrames 1120 | print("AGR Import %f%%" % (100*val)) 1121 | context.window_manager.progress_update(0.5 + val * 0.5) 1122 | 1123 | for modelHandle in modelHandles: 1124 | modelCamData = modelHandle.camData 1125 | if modelCamData is not None: 1126 | curves = modelCamData.curves 1127 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[0].keyframe_points, curves[1].keyframe_points, curves[2].keyframe_points, modelCamData.locationXFrames, modelCamData.locationYFrames, modelCamData.locationZFrames) 1128 | afx_utils.AddKeysList_Rotation(self.keyframeInterpolation, curves[3].keyframe_points, curves[4].keyframe_points, curves[5].keyframe_points, curves[6].keyframe_points, modelCamData.rotationWFrames, modelCamData.rotationXFrames, modelCamData.rotationYFrames, modelCamData.rotationZFrames) 1129 | afx_utils.AddKeysList_Value(self.keyframeInterpolation, curves[7].keyframe_points, modelCamData.lensFrames) 1130 | updateImportProgress(len(modelCamData.locationXFrames) * 3 + len(modelCamData.rotationWFrames) * 4 + len(modelCamData.lensFrames)) 1131 | for curve in curves: 1132 | curve.update() 1133 | if modelHandle.modelData is None: 1134 | continue 1135 | curves = modelHandle.modelData.curves 1136 | afx_utils.AddKeysList_Visible(curves[0].keyframe_points, modelHandle.visibilityFrames) 1137 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[1].keyframe_points, curves[2].keyframe_points, curves[3].keyframe_points, modelHandle.locationXFrames, modelHandle.locationYFrames, modelHandle.locationZFrames) 1138 | afx_utils.AddKeysList_Rotation(self.keyframeInterpolation, curves[4].keyframe_points, curves[5].keyframe_points, curves[6].keyframe_points, curves[7].keyframe_points, modelHandle.rotationWFrames, modelHandle.rotationXFrames, modelHandle.rotationYFrames, modelHandle.rotationZFrames) 1139 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[8].keyframe_points, curves[9].keyframe_points, curves[10].keyframe_points, modelHandle.scaleXFrames, modelHandle.scaleYFrames, modelHandle.scaleZFrames) 1140 | updateImportProgress(len(modelHandle.visibilityFrames) + len(modelHandle.locationXFrames) * 3 + len(modelHandle.rotationWFrames) * 4 + len(modelHandle.scaleXFrames) * 3) 1141 | currentFrames = 0 1142 | for i in modelHandle.boneLocationXFrames: 1143 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[10*i+11].keyframe_points, curves[10*i+12].keyframe_points, curves[10*i+13].keyframe_points, modelHandle.boneLocationXFrames[i], modelHandle.boneLocationYFrames[i], modelHandle.boneLocationZFrames[i]) 1144 | currentFrames += len(modelHandle.boneLocationXFrames[i]) * 3 1145 | updateImportProgress(currentFrames) 1146 | currentFrames = 0 1147 | for i in modelHandle.boneRotationWFrames: 1148 | afx_utils.AddKeysList_Rotation(self.keyframeInterpolation, curves[10*i+14].keyframe_points, curves[10*i+15].keyframe_points, curves[10*i+16].keyframe_points, curves[10*i+17].keyframe_points, modelHandle.boneRotationWFrames[i], modelHandle.boneRotationXFrames[i], modelHandle.boneRotationYFrames[i], modelHandle.boneRotationZFrames[i]) 1149 | currentFrames += len(modelHandle.boneRotationWFrames[i]) * 4 1150 | updateImportProgress(currentFrames) 1151 | currentFrames = 0 1152 | for i in modelHandle.boneScaleXFrames: 1153 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[10*i+18].keyframe_points, curves[10*i+19].keyframe_points, curves[10*i+20].keyframe_points, modelHandle.boneScaleXFrames[i], modelHandle.boneScaleYFrames[i], modelHandle.boneScaleZFrames[i]) 1154 | currentFrames += len(modelHandle.boneScaleXFrames[i]) * 3 1155 | updateImportProgress(currentFrames) 1156 | for curve in curves: 1157 | curve.update() 1158 | if camData is not None: 1159 | curves = camData.curves 1160 | afx_utils.AddKeysList_Location(self.keyframeInterpolation, curves[0].keyframe_points, curves[1].keyframe_points, curves[2].keyframe_points, camData.locationXFrames, camData.locationYFrames, camData.locationZFrames) 1161 | afx_utils.AddKeysList_Rotation(self.keyframeInterpolation, curves[3].keyframe_points, curves[4].keyframe_points, curves[5].keyframe_points, curves[6].keyframe_points, camData.rotationWFrames, camData.rotationXFrames, camData.rotationYFrames, camData.rotationZFrames) 1162 | afx_utils.AddKeysList_Value(self.keyframeInterpolation, curves[7].keyframe_points, camData.lensFrames) 1163 | updateImportProgress(len(camData.locationXFrames) * 3 + len(camData.rotationWFrames) * 4 + len(camData.lensFrames)) 1164 | for curve in curves: 1165 | curve.update() 1166 | 1167 | result['frameEnd'] = int(math.ceil(timeConverter.GetTime())) 1168 | 1169 | if 0 < timeConverter.errorCount: 1170 | self.warning("FPS mismatch was detected %i times. The maximum error was %f. Solution: Make sure to set the Blender project FPS correctly before importing." % (timeConverter.errorCount, timeConverter.maxError)) 1171 | 1172 | context.window_manager.progress_end() 1173 | 1174 | finally: 1175 | if file is not None: 1176 | file.close() 1177 | 1178 | result['result'] = True 1179 | return result 1180 | -------------------------------------------------------------------------------- /advancedfx/import_bvh.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import struct 5 | 6 | import bpy, bpy.props, bpy.ops 7 | import mathutils 8 | 9 | from io_scene_valvesource import utils as vs_utils 10 | 11 | from advancedfx import utils as afx_utils 12 | 13 | # reads a line from file and separates it into words by splitting whitespace 14 | # file to read from 15 | # list of words 16 | def ReadLineWords(file): 17 | line = file.readline() 18 | words = [ll for ll in line.split() if ll] 19 | return words 20 | 21 | 22 | # searches a list of words for a word by lower case comparison 23 | # list to search 24 | # word to find 25 | # less than 0 if not found, otherwise the first list index 26 | def FindWordL(words, word): 27 | i = 0 28 | word = word.lower() 29 | while i < len(words): 30 | if words[i].lower() == word: 31 | return i 32 | i += 1 33 | return -1 34 | 35 | 36 | # Scans the file till a line containing a lower case match of filterWord is found 37 | # file to read from 38 | # word to find 39 | # False on fail, otherwise same as ReadLineWords for this line 40 | def ReadLineWordsFilterL(file, filterWord): 41 | while True: 42 | words = ReadLineWords(file) 43 | if 0 < len(words): 44 | if 0 <= FindWordL(words, filterWord): 45 | return words 46 | else: 47 | return False 48 | 49 | 50 | # Scans the file till the channels line and reads channel information 51 | # file to read from 52 | # False on fail, otherwise channel indexes as follows: [Xposition, Yposition, Zposition, Zrotation, Xrotation, Yrotation] 53 | def ReadChannels(file): 54 | words = ReadLineWordsFilterL(file, 'CHANNELS') 55 | 56 | if not words: 57 | return False 58 | 59 | channels = [\ 60 | FindWordL(words, 'Xposition'),\ 61 | FindWordL(words, 'Yposition'),\ 62 | FindWordL(words, 'Zposition'),\ 63 | FindWordL(words, 'Zrotation'),\ 64 | FindWordL(words, 'Xrotation'),\ 65 | FindWordL(words, 'Yrotation')\ 66 | ] 67 | 68 | idx = 0 69 | while idx < 6: 70 | channels[idx] -= 2 71 | idx += 1 72 | 73 | for channel in channels: 74 | if not (0 <= channel and channel < 6): 75 | return False 76 | 77 | return channels 78 | 79 | 80 | def ReadRootName(file): 81 | words = ReadLineWordsFilterL(file, 'ROOT') 82 | 83 | if not words or len(words)<2: 84 | return False 85 | 86 | return words[1] 87 | 88 | 89 | def ReadFrames(file): 90 | words = ReadLineWordsFilterL(file, 'Frames:') 91 | 92 | if not words or len(words)<2: 93 | return -1 94 | 95 | return int(words[1]) 96 | 97 | def ReadFrameTime(file): 98 | words = ReadLineWordsFilterL(file, 'Time:') 99 | 100 | if not words or len(words)<3: 101 | return -1 102 | 103 | return float(words[2]) 104 | 105 | 106 | def ReadFrame(file, channels): 107 | line = ReadLineWords(file) 108 | 109 | if len(line) < 6: 110 | return False; 111 | 112 | Xpos = float(line[channels[0]]) 113 | Ypos = float(line[channels[1]]) 114 | Zpos = float(line[channels[2]]) 115 | Zrot = float(line[channels[3]]) 116 | Xrot = float(line[channels[4]]) 117 | Yrot = float(line[channels[5]]) 118 | 119 | return [Xpos, Ypos, Zpos, Zrot, Xrot, Yrot] 120 | 121 | class CameraData: 122 | def __init__(self,o): 123 | self.o = o 124 | self.curves = [] 125 | 126 | class BvhImporter(bpy.types.Operator, vs_utils.Logger): 127 | bl_idname = "advancedfx.bvhimporter" 128 | bl_label = "HLAE old Cam IO (.bvh)" 129 | bl_options = {'UNDO'} 130 | 131 | # Properties used by the file browser 132 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 133 | filter_glob: bpy.props.StringProperty(default="*.bvh", options={'HIDDEN'}) 134 | 135 | # Custom properties 136 | 137 | interKey: bpy.props.BoolProperty( 138 | name="Add interpolated key frames", 139 | description="Create interpolated key frames for frames in-between the original key frames.", 140 | default=False) 141 | 142 | global_scale: bpy.props.FloatProperty( 143 | name="Scale", 144 | description="Scale everything by this value", 145 | min=0.000001, max=1000000.0, 146 | soft_min=0.001, soft_max=1.0, 147 | default=0.01, 148 | ) 149 | 150 | cameraFov: bpy.props.FloatProperty( 151 | name="FOV", 152 | description="Camera engine FOV", 153 | min=1.0, max=179.0, 154 | default=90, 155 | ) 156 | 157 | scaleFov: bpy.props.BoolProperty( 158 | name="Scale FOV", 159 | description="If to scale FOV by aspect ratio (required i.e. for CS:GO and Alien Swarm).", 160 | default=True, 161 | ) 162 | 163 | screenWidth: bpy.props.FloatProperty( 164 | name="Screen Width", 165 | description="Only relevant when Scale FOV is set.", 166 | min=1.0, max=65536.0, 167 | default=16.0, 168 | ) 169 | 170 | screenHeight: bpy.props.FloatProperty( 171 | name="Screen Height", 172 | description="Only relevant when Scale FOV is set.", 173 | min=1.0, max=65536.0, 174 | default=9.0, 175 | ) 176 | 177 | # class properties 178 | blenderCamUpQuat = mathutils.Quaternion((math.cos(0.5 * math.radians(90.0)), math.sin(0.5* math.radians(90.0)), 0.0, 0.0)) 179 | 180 | def __init__(self, *args, **kwargs): 181 | super().__init__(*args, **kwargs) 182 | vs_utils.Logger.__init__(self) 183 | 184 | def execute(self, context): 185 | ok = self.readBvh(context) 186 | 187 | self.errorReport("Error report") 188 | 189 | return {'FINISHED'} 190 | 191 | def invoke(self, context, event): 192 | bpy.context.window_manager.fileselect_add(self) 193 | return {'RUNNING_MODAL'} 194 | 195 | def createCamera(self, context, camName): 196 | 197 | camBData = bpy.data.cameras.new(camName) 198 | o = bpy.data.objects.new(camName, camBData) 199 | 200 | context.scene.collection.objects.link(o) 201 | 202 | #vs_utils.select_only(o) 203 | 204 | camData = CameraData(o) 205 | 206 | # Rotation mode: 207 | if o.rotation_mode != 'QUATERNION': 208 | o.rotation_mode = 'QUATERNION' 209 | 210 | 211 | # Create actions and their curves (boobs): 212 | 213 | o.animation_data_create() 214 | action = bpy.data.actions.new(name="game_data") 215 | o.animation_data.action = action 216 | 217 | if afx_utils.NEWER_THAN_440: 218 | slot = action.slots.new(id_type='OBJECT', name="bvh") 219 | o.animation_data.action_slot = slot 220 | 221 | for i in range(3): 222 | camData.curves.append(action.fcurves.new("location",index = i)) 223 | 224 | for i in range(4): 225 | camData.curves.append(action.fcurves.new("rotation_quaternion",index = i)) 226 | 227 | return camData 228 | 229 | # Camera FOV in radians 230 | def calcCameraFov(self): 231 | fov = math.radians(self.cameraFov) 232 | 233 | if self.scaleFov: 234 | defaultAspectRatio = 4.0/3.0 235 | engineAspectRatio = (self.screenWidth / self.screenHeight) if 0 != self.screenHeight else defaultAspectRatio 236 | ratio = engineAspectRatio / defaultAspectRatio 237 | halfAngle = fov * 0.5 238 | fov = math.atan(math.tan(halfAngle) * ratio) * 2.0 239 | 240 | return fov 241 | 242 | def readBvh(self, context): 243 | fps = context.scene.render.fps 244 | 245 | camData = self.createCamera(context,"BvhCam") 246 | 247 | if camData is None: 248 | self.error("Failed to create camera.") 249 | return False 250 | 251 | #vs_utils.select_only(camData.o) 252 | camData.o.data.angle = self.calcCameraFov() 253 | 254 | file = None 255 | 256 | try: 257 | file = open(self.filepath, 'r') 258 | 259 | rootName = ReadRootName(file) 260 | if not rootName: 261 | self.error('Failed parsing ROOT.') 262 | return False 263 | 264 | print('ROOT:', rootName) 265 | 266 | channels = ReadChannels(file) 267 | if not channels: 268 | self.error('Failed parsing CHANNELS.') 269 | return False 270 | 271 | frames = ReadFrames(file); 272 | if frames < 0: 273 | self.error('Failed parsing Frames.') 274 | return False 275 | 276 | if 0 == frames: frames = 1 277 | 278 | frames = float(frames) 279 | 280 | frameTime = ReadFrameTime(file) 281 | if not frameTime: 282 | self.error('Failed parsing Frame Time.') 283 | return False 284 | 285 | frameCount = int(0) 286 | 287 | lastQuat = None 288 | 289 | while True: 290 | frame = ReadFrame(file, channels) 291 | if not frame: 292 | break 293 | 294 | frameCount += 1 295 | 296 | BTT = (float(frameTime) * float(frameCount-1)) 297 | 298 | BYP = -frame[0] 299 | BZP = frame[1] 300 | BXP = -frame[2] 301 | 302 | BZR = -frame[3] 303 | BXR = -frame[4] 304 | BYR = frame[5] 305 | 306 | time = BTT 307 | time = 1.0 + time * fps 308 | 309 | renderOrigin = mathutils.Vector((-BYP,BXP,BZP)) * self.global_scale 310 | 311 | curves = camData.curves 312 | 313 | afx_utils.AddKey_Location(self.interKey, curves[0+0].keyframe_points, curves[0+1].keyframe_points, curves[0+2].keyframe_points, time, renderOrigin) 314 | 315 | qAngle = afx_utils.QAngle(BXR,BYR,BZR) 316 | 317 | quat = qAngle.to_quaternion() @ self.blenderCamUpQuat 318 | 319 | # Make sure we travel the short way: 320 | if lastQuat: 321 | dp = lastQuat.dot(quat) 322 | if dp < 0: 323 | quat.negate() 324 | 325 | lastQuat = quat 326 | 327 | afx_utils.AddKey_Rotation(self.interKey, curves[0+3].keyframe_points, curves[0+4].keyframe_points, curves[0+5].keyframe_points, curves[0+6].keyframe_points, time, quat) 328 | 329 | if not frameCount == frames: 330 | self.error("Frames are missing in BVH file.") 331 | return False 332 | 333 | finally: 334 | if file is not None: 335 | file.close() 336 | 337 | return True 338 | -------------------------------------------------------------------------------- /advancedfx/import_cam.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import math 3 | import os 4 | import struct 5 | 6 | import bpy, bpy.props, bpy.ops 7 | import mathutils 8 | 9 | from io_scene_valvesource import utils as vs_utils 10 | 11 | from advancedfx import utils as afx_utils 12 | 13 | # reads a line from file and separates it into words by splitting whitespace 14 | # file to read from 15 | # list of words 16 | def ReadLineWords(file): 17 | line = file.readline() 18 | words = [ll for ll in line.split() if ll] 19 | return words 20 | 21 | def AlienSwarm_FovScaling(width, height, fov): 22 | if 0 == height: 23 | return fov 24 | engineAspectRatio = width / height 25 | defaultAscpectRatio = 4.0 / 3.0 26 | ratio = engineAspectRatio / defaultAscpectRatio 27 | t = ratio * math.tan(math.radians(0.5 * fov)) 28 | return 2.0 * math.degrees(math.atan(t)) 29 | 30 | class CameraData: 31 | def __init__(self,o,c): 32 | self.o = o 33 | self.c = c 34 | self.curves = [] 35 | 36 | class CamImporter(bpy.types.Operator, vs_utils.Logger): 37 | bl_idname = "advancedfx.camimporter" 38 | bl_label = "HLAE Camera IO (.cam)" 39 | bl_options = {'UNDO'} 40 | 41 | # Properties used by the file browser 42 | filepath: bpy.props.StringProperty(subtype="FILE_PATH") 43 | filter_glob: bpy.props.StringProperty(default="*.cam", options={'HIDDEN'}) 44 | 45 | # Custom properties 46 | 47 | interKey: bpy.props.BoolProperty( 48 | name="Add interpolated key frames", 49 | description="Create interpolated key frames for frames in-between the original key frames.", 50 | default=False) 51 | 52 | global_scale: bpy.props.FloatProperty( 53 | name="Scale", 54 | description="Scale everything by this value", 55 | min=0.000001, max=1000000.0, 56 | soft_min=0.001, soft_max=1.0, 57 | default=0.01, 58 | ) 59 | 60 | # class properties 61 | blenderCamUpQuat = mathutils.Quaternion((math.cos(0.5 * math.radians(90.0)), math.sin(0.5* math.radians(90.0)), 0.0, 0.0)) 62 | 63 | def __init__(self, *args, **kwargs): 64 | super().__init__(*args, **kwargs) 65 | vs_utils.Logger.__init__(self) 66 | 67 | def execute(self, context): 68 | ok = self.readCam(context) 69 | 70 | self.errorReport("Error report") 71 | 72 | return {'FINISHED'} 73 | 74 | def invoke(self, context, event): 75 | bpy.context.window_manager.fileselect_add(self) 76 | return {'RUNNING_MODAL'} 77 | 78 | def createCamera(self, context, camName): 79 | 80 | c = bpy.data.cameras.new(camName) 81 | o = bpy.data.objects.new(camName, c) 82 | 83 | context.scene.collection.objects.link(o) 84 | 85 | #vs_utils.select_only(o) 86 | 87 | camData = CameraData(o,c) 88 | 89 | # Rotation mode: 90 | if o.rotation_mode != 'QUATERNION': 91 | o.rotation_mode = 'QUATERNION' 92 | 93 | 94 | # Create actions and their curves: 95 | 96 | o.animation_data_create() 97 | action = bpy.data.actions.new(name="game_data") 98 | o.animation_data.action = action 99 | 100 | 101 | if afx_utils.NEWER_THAN_440: 102 | slot = action.slots.new(id_type='OBJECT', name="camio") 103 | o.animation_data.action_slot = slot 104 | 105 | for i in range(3): 106 | camData.curves.append(action.fcurves.new("location",index = i)) 107 | 108 | for i in range(4): 109 | camData.curves.append(action.fcurves.new("rotation_quaternion",index = i)) 110 | 111 | c.animation_data_create() 112 | action = bpy.data.actions.new(name="game_data") 113 | c.animation_data.action = action 114 | 115 | if afx_utils.NEWER_THAN_440: 116 | slot = action.slots.new(id_type='CAMERA', name="camio") 117 | c.animation_data.action_slot = slot 118 | 119 | camData.curves.append(action.fcurves.new("lens")) 120 | 121 | return camData 122 | 123 | def readCam(self, context): 124 | fps = context.scene.render.fps 125 | 126 | width = context.scene.render.pixel_aspect_x * context.scene.render.resolution_x 127 | height = context.scene.render.pixel_aspect_y * context.scene.render.resolution_y 128 | 129 | frame_end = None 130 | 131 | camData = self.createCamera(context,"afxCam") 132 | 133 | if camData is None: 134 | self.error("Failed to create camera.") 135 | return False 136 | 137 | #vs_utils.select_only( camData.o ) 138 | 139 | file = None 140 | 141 | try: 142 | file = open(self.filepath, 'r') 143 | 144 | version = 0 145 | scaleFov = '' 146 | 147 | words = ReadLineWords(file) 148 | 149 | if not(2 <= len(words) and 'advancedfx' == words[0] and 'Cam' == words[1]): 150 | self.error('Not an valid advancedfx Cam file.') 151 | return False 152 | 153 | while True: 154 | words = ReadLineWords(file) 155 | 156 | if(1 <= len(words)): 157 | if 'DATA' == words[0]: 158 | break; 159 | if 'version' == words[0] and 2 <= len(words): 160 | version = int(words[1]) 161 | if 'scaleFov' == words[0] and 2 <= len(words): 162 | scaleFov = words[1] 163 | 164 | if(version < 1 or version > 2): 165 | self.error("Invalid version, only 1 - 2 are supported.") 166 | return False 167 | 168 | if not(scaleFov in ['','none', 'alienSwarm']): 169 | self.error("Unsupported scaleFov value.") 170 | return False 171 | 172 | lastTime = None 173 | lastQuat = None 174 | 175 | while True: 176 | words = ReadLineWords(file) 177 | 178 | if not( 8 <= len(words)): 179 | break 180 | 181 | time = float(words[0]) 182 | 183 | if not lastTime: 184 | lastTime = time 185 | 186 | orgTime = time 187 | 188 | time = time -lastTime 189 | 190 | time = 1.0 + time * fps 191 | 192 | frame_end = int(math.ceil(time)) 193 | 194 | renderOrigin = mathutils.Vector((-float(words[2]),float(words[1]),float(words[3]))) * self.global_scale 195 | qAngle = afx_utils.QAngle(float(words[5]),float(words[6]),float(words[4])) 196 | 197 | quat = qAngle.to_quaternion() @ self.blenderCamUpQuat 198 | 199 | # Make sure we travel the short way: 200 | if lastQuat: 201 | dp = lastQuat.dot(quat) 202 | if dp < 0: 203 | quat.negate() 204 | 205 | lastQuat = quat 206 | 207 | fov = float(words[7]) 208 | 209 | # none and alienSwarm was confused in version 1, version 2 always outputs real fov and doesn't have scaleFov. 210 | if 'none' == scaleFov: 211 | fov = AlienSwarm_FovScaling(width, height, fov) 212 | 213 | lens = camData.c.sensor_width / (2.0 * math.tan(math.radians(fov) / 2.0)) 214 | 215 | curves = camData.curves 216 | 217 | afx_utils.AddKey_Location(self.interKey, curves[0+0].keyframe_points, curves[0+1].keyframe_points, curves[0+2].keyframe_points, time, renderOrigin) 218 | 219 | afx_utils.AddKey_Rotation(self.interKey, curves[0+3].keyframe_points, curves[0+4].keyframe_points, curves[0+5].keyframe_points, curves[0+6].keyframe_points, time, quat) 220 | 221 | afx_utils.AddKey_Value(self.interKey, curves[0+7].keyframe_points, time, lens) 222 | 223 | if frame_end is not None: 224 | bpy.context.scene.frame_start = 1 225 | bpy.context.scene.frame_end = frame_end 226 | 227 | finally: 228 | if file is not None: 229 | file.close() 230 | 231 | return True 232 | -------------------------------------------------------------------------------- /advancedfx/readme.txt: -------------------------------------------------------------------------------- 1 | Installation: 2 | 3 | You need to install latest Blender Source Tools first 4 | ( http://steamreview.org/BlenderSourceTools/ ), 5 | since we depend on it. 6 | This version of afx-blender-scripts was tested using 7 | Blender Source Tools 3.2.5. 8 | 9 | If you have a previous version of afx-blender-scripts installed, uninstall 10 | it first through Blender! 11 | 12 | afx-blender-scripts is installed like the Blender Source Tools are. 13 | 14 | Of course when using AGR import you need the decompiled(player) models 15 | in a folder structure like it is in CS:GO's pak01_dir.pak 16 | We recommend Crowbar ( http://steamcommunity.com/groups/CrowbarTool ) 17 | and YOU NEED TO TICK THE "Folder for each model" option in the Decompile 18 | options! 19 | 20 | 21 | 22 | Usage: 23 | 24 | Always make sure to select the correct render properties in your project first 25 | (FPS, resolution). 26 | 27 | The scripts can be accessed through entries in the import menu and export menu. 28 | 29 | "Add interpolated key frames" option: 30 | Default is off (disabled). 31 | This creates interpolated key frames for Blender frames in-between the original 32 | key frames. This is useful in case your Blender project FPS doesn't match your 33 | recorded FPS, because interpolation is set to constant between all keyframes 34 | (see notice bellow for reason). 35 | Of course this will add more data and take longer if that's the case when you 36 | enable it. 37 | 38 | Don't forget to enter the "Asset Path" when using AGR import, it needs to be 39 | the full path to the folder structure with the decompiled models. 40 | We recommend using Crowbar ( http://steamcommunity.com/groups/CrowbarTool ) 41 | and YOU NEED TO TICK THE "Folder for each model" option in the Decompile 42 | options! 43 | 44 | Notice: 45 | The interpolation is set to CONSTANT for everything, because 46 | Blender doesn't support proper interpolation of curves for quaternions yet. 47 | 48 | For more informations visit it's Advancedfx Wiki page ( https://github.com/advancedfx/advancedfx/wiki/Source:mirv_agr ) 49 | 50 | 51 | Changelog: 52 | 53 | 1.14.6 (2025-05-30T11:48Z): 54 | - Fixed bug with initalization of base classes 55 | 56 | 1.14.4 (2025-03-19T11:42Z): 57 | - add support for Blender 4.4 58 | - removed unnecessary indents 59 | 60 | 1.14.3 (2023-09-11T09:32Z): 61 | - fixed cam and bvh import for Blender 3.5+ 62 | 63 | 1.14.2 (2022-06-12T10:43Z): 64 | - fixed AGR Batch Export issue, which caused an error on export if an armature was deleted of the viewport 65 | - fixed documentation button in addon preferences 66 | 67 | 1.14.1 (2022-03-11T22:28Z): 68 | - Added support for AGR version 6, still supports version 5. 69 | 70 | 1.13.2 (2021-12-06T22:11Z): 71 | - fix keyframes when there's multiple updates for same thing during a frame. 72 | 73 | 1.13.1 (2021-12-04T06:32Z): 74 | - fixed scale of HLAE AGR Batch Export (.fbx) 75 | 76 | 1.13.0 (2021-09-17T05:17Z): 77 | - HLAE Camera IO (.cam): 78 | - Added version 2 support for to import. 79 | - Adjusted import to understand how HLAE mixes up the scaleFov for version 1 and fixed bugs. 80 | - Updated export to version 2. 81 | - Import now sets frame_begin and frame_end. 82 | 83 | 1.12.7 (2021-08-31T16:04Z): 84 | - fixed decal_e sticker skipping 85 | 86 | 1.12.6 (2021-08-31T15:17Z): 87 | - added skip import option for Stattrack and Stickers 88 | - added skip import for shared_player_skeleton to Skip Physic and LOD Meshes 89 | 90 | 1.12.5 (2020-04-28T21:03Z): 91 | - added support for Blender 3.0 Alpha 92 | - fixed changing Root Bone Name 93 | - fixed camera scale export 94 | 95 | 1.12.2 (2020-03-13T13:35Z): 96 | - Fixed camera FOV not being animated porperly. 97 | 98 | 1.12.1 (2020-01-25T15:07Z): 99 | - Fix BST becoming unusable after using AGR importer. Thanks to @Lasa01. 100 | 101 | 1.12.0 (2020-09-11T06:30Z): 102 | - Use faster foreach_set for keyframe interpolation in 2.90+. Thanks to @Lasa01. 103 | 104 | 1.11.2 (2020-08-28T21:19Z): 105 | - added "Documentation" button 106 | - added "Report a Bug" button 107 | - added AGR batch .FBX export 108 | 109 | 1.11.0 (2020-08-11T19:28Z): 110 | - Updated HLAE AGR Import to agr version 5 111 | 112 | 1.10.4 (2020-08-10T09:21Z): 113 | - skip LOD meshes for Team Fortress 2. Thanks to @Lasa01 for using his code 114 | - fixed a character issue for Linux. Thanks to @AgenteDog for doing it real quick 115 | 116 | 1.10.2 (2020-05-22T16:54Z): 117 | - Fixed BVH Export. 118 | 119 | 1.10.0 (2020-05-13T14:55Z) (by lasa01): 120 | (Many thanks, also for answering annoying questions about pull-request.) 121 | - Read agr keyframes into memory and add all at once (faster). 122 | - Make sure bone rotations take shortest path. 123 | - User-selectable keyframe interpolation mode (agr): Bezier is much faster than constant but not recommended for beginners that don't get project FPS right 100%. 124 | - Reduce import logging spam. 125 | - Fix modelHandle reusing not selecting closest one. 126 | 127 | 1.9.8 (2020-05-09T13:01Z) (by Devostated): 128 | - Support for Blender Source Tools 3.1.0 Test version: 129 | - Now it doesn't create collections anymore for cleaner project files, just like it was in Blender 2.79 and below. 130 | - 3.0.3 is still supported! 131 | - Added Timer: 132 | - Now you can see how long the import of the AGR took, for the curious ones. 133 | - Changed Model instancing option 134 | - Changed description text after getting a lot questions about model instancing. 135 | - Added an advice for beginners that get confused by model instancing. 136 | - Added automatic frame range adjustment. 137 | - Tested with Blender 2.82a 138 | - Tested with Blender Source Tools 3.0.3 and 3.1.0. 139 | 140 | 1.9.4 (2020-01-03T13:46Z): 141 | - Removed "Remove useless meshes" option (by Devostated) 142 | - Added "Skip Physic Meshes" option, enabled by default (by Devostated) 143 | - Removed irritating missing ?.qc Error (by Devostated) 144 | - Tested with Blender Source Tools 3.0.3. 145 | - Tested with Blender 2.81a. 146 | 147 | 1.9.2 (2020-01-03T10:31Z): 148 | - Added option for model instancing (faster), enabled by default. 149 | - Tested with Blender Source Tools 3.0.3. 150 | - Tested with Blender 2.81a. 151 | 152 | 1.8.0 (2019-08-30T06:28Z): 153 | - Added option "Remove useless meshes" (Removes Physics and smd_bone_vis for faster workflow.) (by Devostated). 154 | - Added saving, loading and removing presets (by Devostated). 155 | - Test with Blender Source Tools 3.0.1. 156 | 157 | 1.7.1.1 (2019-08-06T13:32Z): 158 | - Changed back default scale to 0.01, even though 0.0254 is more accurate. 159 | 160 | 1.7.1 (2019-08-06T13:20Z): 161 | - Minor changes 162 | 163 | 1.7.0 (2019-01-26T14:18Z): 164 | - Updated to Blender 2.80 beta (needs latest Blender Source Tools 2.11.0b1-3251fc47b768116b91a8f5550166bc5ccb01efdf or newer ( https://github.com/Artfunkel/BlenderSourceTools/tree/master/io_scene_valvesource )). 165 | - Fixed HLAE BVH Export exporting wrong rotation. 166 | 167 | 1.6.0 (2018-10-05T17:45Z): 168 | - Update HLAE AGR Import: 169 | - Added option "Bones (skeleton) only", thanks to https://github.com/Darkhandrob 170 | 171 | 1.5.1 (2018-08-16T08:24Z): 172 | - Update HLAE Camera IO (.cam) export: 173 | - Fixed it not working when camera object name did not match camera object data name 174 | 175 | 1.5.0 (2018-04-27T17:11Z): 176 | - Added HLAE Camera IO (.cam) import 177 | - Added HLAE Camera IO (.cam) export 178 | - Update HLAE AGR Import: 179 | - Added option "Add interpolated key frames" (default off) 180 | - Updated HLAE BVH Import: 181 | - Renamed to HLAE old Cam IO (.bvh) import 182 | - Added option "Add interpolated key frames" (default off) 183 | - Bug fixes 184 | - Updated HLAE BVH Export: 185 | - Renamed to HLAE old Cam IO (.bvh) export 186 | - Bug fixes 187 | - Tested with Blender Source Tools 2.10.2 188 | - Please see updated usage note above regarding 189 | "Add interpolated key frames" 190 | 191 | 1.4.3 (2017-12-23T21:14Z): 192 | - Updated HLAE AGR Import: 193 | Added option "Preserve SMD Polygons & Normals": 194 | Import raw (faster), disconnected polygons from SMD files; 195 | these are harder to edit but a closer match to the original mesh. 196 | (Enabled by default, much less time spent on importing models now.) 197 | 198 | 1.4.2 (2017-11-18T20:00Z): 199 | - Updated HLAE AGR Import: Added option "Scale invisible to zero" 200 | to scale entities to 0 upon hide_render (might be useful for FBX export). 201 | This option creates drivers and modifiers on each entity, 202 | so no extra animation data. 203 | Please don't be scared if at frame 0 (default) everything is scaled to 204 | zero (not visible), this is because the animations start at frame 1. 205 | 206 | 1.4.1 (2017-11-01T18:53Z): 207 | - Updated HLAE AGR Import: Now will only work with the "Folder for each model" 208 | Option in Crowbar. This is important to avoid naming collissions that can occur. 209 | In return this also works with the newest Crowbar version (currently 0.49.0). 210 | 211 | 1.4.0 (2017-09-16T22:00Z): 212 | - Updated HLAE AGR Import to agr version 4 (also bug fixes) 213 | 214 | 1.3.0 (2017-09-12T12:00Z): 215 | - Updated HLAE AGR Import to agr version 3 216 | 217 | 1.2.0 (2017-08-03T12:00Z): 218 | - Updated HLAE AGR Import to agr version 2 (also bug fixes) 219 | 220 | 1.1.0 (2017-06-25T20:02Z): 221 | - Updated HLAE AGR Import to agr version 1 222 | 223 | 1.0.2 (2016-12-14T12:36Z): 224 | - Fixed HLAE AGR Import so now it will always take the shortest path for Euler based 225 | rotation of the models between two keyframes. 226 | 227 | 1.0.1 (2016-08-10T12:48Z): 228 | - Fixed HLAE AGR Import failing when missing model was marked as deleted in 229 | AGR (should now report the missing model(s) instead as intended) 230 | 231 | 1.0.0 (2016-08-09T16:17Z): 232 | - Added HLAE BVH Import 233 | - Added HLAE BVH Export 234 | 235 | 0.0.1 (2016-07-27T20:39Z): 236 | - First version 237 | - Added HLAE AGR Import 238 | 239 | 240 | MIT License 241 | 242 | Copyright (c) 2019 advancedfx.org 243 | 244 | Permission is hereby granted, free of charge, to any person obtaining a copy 245 | of this software and associated documentation files (the "Software"), to deal 246 | in the Software without restriction, including without limitation the rights 247 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 248 | copies of the Software, and to permit persons to whom the Software is 249 | furnished to do so, subject to the following conditions: 250 | 251 | The above copyright notice and this permission notice shall be included in all 252 | copies or substantial portions of the Software. 253 | 254 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 255 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 256 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 257 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 258 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 259 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 260 | SOFTWARE. 261 | -------------------------------------------------------------------------------- /advancedfx/utils.py: -------------------------------------------------------------------------------- 1 | import mathutils 2 | import math 3 | import bpy 4 | 5 | NEWER_THAN_290 = bpy.app.version >= (2, 90, 0) 6 | NEWER_THAN_440 = bpy.app.version >= (4, 4, 0) 7 | 8 | class QAngle: 9 | def __init__(self,x,y,z): 10 | self.x = x 11 | self.y = y 12 | self.z = z 13 | 14 | def to_quaternion(self): 15 | pitchH = 0.5 * math.radians(self.x) 16 | qPitchY = mathutils.Quaternion((math.cos(pitchH), -math.sin(pitchH), 0.0, 0.0)) 17 | 18 | yawH = 0.5 * math.radians(self.y) 19 | qYawZ = mathutils.Quaternion((math.cos(yawH), 0.0, 0.0, math.sin(yawH))) 20 | 21 | rollH = 0.5 * math.radians(self.z) 22 | qRollX = mathutils.Quaternion((math.cos(rollH), 0.0, math.sin(rollH), 0.0)) 23 | 24 | return qYawZ @ qPitchY @ qRollX 25 | 26 | def GetInterKeyRange(lastTime, time): 27 | loF = lastTime 28 | lo = int(math.ceil(loF)) 29 | if(lo == loF): 30 | lo = lo + 1 31 | 32 | hiF = time 33 | hi = int(math.floor(hiF)) 34 | if( hi == hiF ): 35 | hi = hi -1 36 | 37 | return range(lo,hi+1) 38 | 39 | def AddKey_Value(interKey, keyframe_points, time, value): 40 | if(interKey and 0 < len(keyframe_points)): 41 | lastItem = keyframe_points[-1] 42 | lastTime = lastItem.co[0] 43 | lastValue = lastItem.co[1] 44 | 45 | for interTime in GetInterKeyRange(lastTime, time): 46 | dT = (interTime -lastTime) / (time-lastTime) 47 | interValue = lastValue * (1.0 - dT) + value * dT 48 | keyframe_points.add(1) 49 | item = keyframe_points[-1] 50 | item.co = [interTime, interValue] 51 | item.interpolation = 'CONSTANT' 52 | 53 | keyframe_points.add(1) 54 | item = keyframe_points[-1] 55 | item.co = [time, value] 56 | item.interpolation = 'CONSTANT' 57 | 58 | def AddKeysList_Value(interpolation, keyframe_points, data): 59 | if len(data) < 2: 60 | return 61 | keyframe_points.add(len(data) // 2) 62 | keyframe_points.foreach_set("co", data) 63 | if keyframe_points[0].interpolation != interpolation: 64 | if NEWER_THAN_290: 65 | interpolation_value = bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items[interpolation].value 66 | keyframe_points.foreach_set("interpolation", [interpolation_value] * len(keyframe_points)) 67 | else: 68 | for item in keyframe_points: 69 | item.interpolation = interpolation 70 | 71 | def AppendInterKeys_Value(time, value, data): 72 | if 0 == len(data): 73 | return 74 | lastValue = data[-1] 75 | lastTime = data[-2] 76 | for interTime in GetInterKeyRange(lastTime, time): 77 | dT = (interTime-lastTime) / (time-lastTime) 78 | interValue = lastValue * (1.0 - dT) + value * dT 79 | data.extend((interTime, interValue)) 80 | 81 | def AddKey_Visible(interKey, keyframe_points_hide_render, time, visible): 82 | if(interKey and 0 < len(keyframe_points_hide_render)): 83 | lastItem = keyframe_points_hide_render[-1] 84 | lastTime = lastItem.co[0] 85 | lastVisible = 0 == lastItem.co[1] 86 | 87 | for interTime in GetInterKeyRange(lastTime, time): 88 | keyframe_points_hide_render.add(1) 89 | item = keyframe_points_hide_render[-1] 90 | item.co = [interTime, 0.0 if( lastVisible and visible ) else 1.0] 91 | item.interpolation = 'CONSTANT' 92 | 93 | keyframe_points_hide_render.add(1) 94 | item = keyframe_points_hide_render[-1] 95 | item.co = [time, 0.0 if( visible ) else 1.0] 96 | item.interpolation = 'CONSTANT' 97 | 98 | def AddKeysList_Visible(keyframe_points, data): 99 | keyframe_points.add(len(data) // 2) 100 | keyframe_points.foreach_set("co", data) 101 | if NEWER_THAN_290: 102 | interpolation_value = bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items['CONSTANT'].value 103 | keyframe_points.foreach_set("interpolation", [interpolation_value] * len(keyframe_points)) 104 | else: 105 | for item in keyframe_points: 106 | item.interpolation = 'CONSTANT' 107 | 108 | def AppendInterKeys_Visible(time, invisible, data): 109 | if 0 == len(data): 110 | return 111 | lastInvisible = data[-1] 112 | lastTime = data[-2] 113 | for interTime in GetInterKeyRange(lastTime, time): 114 | data.extend((interTime, 0 if lastInvisible == 0 and invisible == 0 else 1)) 115 | 116 | def AddKey_Location(interKey, keyframe_points_location_x, keyframe_points_location_y, keyframe_points_location_z, time, location): 117 | if(interKey and 0 < len(keyframe_points_location_x) and 0 < len(keyframe_points_location_y) and 0 < len(keyframe_points_location_z)): 118 | lastItemX = keyframe_points_location_x[-1] 119 | lastItemY = keyframe_points_location_y[-1] 120 | lastItemZ = keyframe_points_location_z[-1] 121 | lastTime = lastItemX.co[0] 122 | lastLocation = mathutils.Vector((lastItemX.co[1], lastItemY.co[1], lastItemZ.co[1])) 123 | 124 | for interTime in GetInterKeyRange(lastTime, time): 125 | interLocation = lastLocation.lerp(location, (interTime -lastTime) / (time-lastTime)) 126 | keyframe_points_location_x.add(1) 127 | keyframe_points_location_y.add(1) 128 | keyframe_points_location_z.add(1) 129 | itemX = keyframe_points_location_x[-1] 130 | itemY = keyframe_points_location_y[-1] 131 | itemZ = keyframe_points_location_z[-1] 132 | itemX.co = [interTime, interLocation.x] 133 | itemY.co = [interTime, interLocation.y] 134 | itemZ.co = [interTime, interLocation.z] 135 | itemX.interpolation = 'CONSTANT' 136 | itemY.interpolation = 'CONSTANT' 137 | itemZ.interpolation = 'CONSTANT' 138 | 139 | keyframe_points_location_x.add(1) 140 | keyframe_points_location_y.add(1) 141 | keyframe_points_location_z.add(1) 142 | itemX = keyframe_points_location_x[-1] 143 | itemY = keyframe_points_location_y[-1] 144 | itemZ = keyframe_points_location_z[-1] 145 | itemX.co = [time, location.x] 146 | itemY.co = [time, location.y] 147 | itemZ.co = [time, location.z] 148 | itemX.interpolation = 'CONSTANT' 149 | itemY.interpolation = 'CONSTANT' 150 | itemZ.interpolation = 'CONSTANT' 151 | 152 | def AddKeysList_Location(interpolation, keyframe_points_x, keyframe_points_y, keyframe_points_z, data_x, data_y, data_z): 153 | if len(data_x) < 2: 154 | return 155 | keyframe_points_x.add(len(data_x) // 2) 156 | keyframe_points_x.foreach_set("co", data_x) 157 | keyframe_points_y.add(len(data_y) // 2) 158 | keyframe_points_y.foreach_set("co", data_y) 159 | keyframe_points_z.add(len(data_z) // 2) 160 | keyframe_points_z.foreach_set("co", data_z) 161 | if keyframe_points_x[0].interpolation != interpolation: 162 | if NEWER_THAN_290: 163 | interpolation_value = bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items[interpolation].value 164 | keyframe_points_x.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_x)) 165 | keyframe_points_y.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_y)) 166 | keyframe_points_z.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_z)) 167 | else: 168 | for item in keyframe_points_x: 169 | item.interpolation = interpolation 170 | for item in keyframe_points_y: 171 | item.interpolation = interpolation 172 | for item in keyframe_points_z: 173 | item.interpolation = interpolation 174 | 175 | def AppendInterKeys_Location(time, location, data_x, data_y, data_z): 176 | if 0 == len(data_x): 177 | return 178 | lastX = data_x[-1] 179 | lastY = data_y[-1] 180 | lastZ = data_z[-1] 181 | lastTime = data_x[-2] 182 | lastLocation = mathutils.Vector((lastX, lastY, lastZ)) 183 | for interTime in GetInterKeyRange(lastTime, time): 184 | interLocation = lastLocation.lerp(location, (interTime-lastTime) / (time-lastTime)) 185 | data_x.extend((interTime, interLocation.x)) 186 | data_y.extend((interTime, interLocation.y)) 187 | data_z.extend((interTime, interLocation.z)) 188 | 189 | def AddKey_Scale(interKey, keyframe_points_scale_x, keyframe_points_scale_y, keyframe_points_scale_z, time, scale): 190 | AddKey_Location(interKey, keyframe_points_scale_x, keyframe_points_scale_y, keyframe_points_scale_z, time, scale) 191 | 192 | def AddKey_Rotation(interKey, keyframe_points_rotation_quaternion_w, keyframe_points_rotation_quaternion_x, keyframe_points_rotation_quaternion_y, keyframe_points_rotation_quaternion_z, time, rotation): 193 | if(interKey and 0 < len(keyframe_points_rotation_quaternion_w) and 0 < len(keyframe_points_rotation_quaternion_x) and 0 < len(keyframe_points_rotation_quaternion_y) and 0 < len(keyframe_points_rotation_quaternion_z)): 194 | lastItemW = keyframe_points_rotation_quaternion_w[-1] 195 | lastItemX = keyframe_points_rotation_quaternion_x[-1] 196 | lastItemY = keyframe_points_rotation_quaternion_y[-1] 197 | lastItemZ = keyframe_points_rotation_quaternion_z[-1] 198 | lastTime = lastItemW.co[0] 199 | lastRotation = mathutils.Quaternion((lastItemW.co[1], lastItemX.co[1], lastItemY.co[1], lastItemZ.co[1])) 200 | 201 | for interTime in GetInterKeyRange(lastTime, time): 202 | interRotation = lastRotation.slerp(rotation, (interTime -lastTime) / (time-lastTime)) 203 | keyframe_points_rotation_quaternion_w.add(1) 204 | keyframe_points_rotation_quaternion_x.add(1) 205 | keyframe_points_rotation_quaternion_y.add(1) 206 | keyframe_points_rotation_quaternion_z.add(1) 207 | itemW = keyframe_points_rotation_quaternion_w[-1] 208 | itemX = keyframe_points_rotation_quaternion_x[-1] 209 | itemY = keyframe_points_rotation_quaternion_y[-1] 210 | itemZ = keyframe_points_rotation_quaternion_z[-1] 211 | itemW.co = [interTime, interRotation.w] 212 | itemX.co = [interTime, interRotation.x] 213 | itemY.co = [interTime, interRotation.y] 214 | itemZ.co = [interTime, interRotation.z] 215 | itemW.interpolation = 'CONSTANT' 216 | itemX.interpolation = 'CONSTANT' 217 | itemY.interpolation = 'CONSTANT' 218 | itemZ.interpolation = 'CONSTANT' 219 | 220 | keyframe_points_rotation_quaternion_w.add(1) 221 | keyframe_points_rotation_quaternion_x.add(1) 222 | keyframe_points_rotation_quaternion_y.add(1) 223 | keyframe_points_rotation_quaternion_z.add(1) 224 | itemW = keyframe_points_rotation_quaternion_w[-1] 225 | itemX = keyframe_points_rotation_quaternion_x[-1] 226 | itemY = keyframe_points_rotation_quaternion_y[-1] 227 | itemZ = keyframe_points_rotation_quaternion_z[-1] 228 | itemW.co = [time, rotation.w] 229 | itemX.co = [time, rotation.x] 230 | itemY.co = [time, rotation.y] 231 | itemZ.co = [time, rotation.z] 232 | itemW.interpolation = 'CONSTANT' 233 | itemX.interpolation = 'CONSTANT' 234 | itemY.interpolation = 'CONSTANT' 235 | itemZ.interpolation = 'CONSTANT' 236 | 237 | def AddKeysList_Rotation(interpolation, keyframe_points_w, keyframe_points_x, keyframe_points_y, keyframe_points_z, data_w, data_x, data_y, data_z): 238 | if len(data_w) < 2: 239 | return 240 | keyframe_points_w.add(len(data_w) // 2) 241 | keyframe_points_w.foreach_set("co", data_w) 242 | keyframe_points_x.add(len(data_x) // 2) 243 | keyframe_points_x.foreach_set("co", data_x) 244 | keyframe_points_y.add(len(data_y) // 2) 245 | keyframe_points_y.foreach_set("co", data_y) 246 | keyframe_points_z.add(len(data_z) // 2) 247 | keyframe_points_z.foreach_set("co", data_z) 248 | if keyframe_points_w[0].interpolation != interpolation: 249 | if NEWER_THAN_290: 250 | interpolation_value = bpy.types.Keyframe.bl_rna.properties["interpolation"].enum_items[interpolation].value 251 | keyframe_points_w.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_w)) 252 | keyframe_points_x.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_x)) 253 | keyframe_points_y.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_y)) 254 | keyframe_points_z.foreach_set("interpolation", [interpolation_value] * len(keyframe_points_z)) 255 | else: 256 | for item in keyframe_points_w: 257 | item.interpolation = interpolation 258 | for item in keyframe_points_x: 259 | item.interpolation = interpolation 260 | for item in keyframe_points_y: 261 | item.interpolation = interpolation 262 | for item in keyframe_points_z: 263 | item.interpolation = interpolation 264 | 265 | def AppendInterKeys_Rotation(time, rotation, data_w, data_x, data_y, data_z): 266 | if 0 == len(data_w): 267 | return 268 | lastW = data_w[-1] 269 | lastX = data_x[-1] 270 | lastY = data_y[-1] 271 | lastZ = data_z[-1] 272 | lastTime = data_w[-2] 273 | lastRotation = mathutils.Quaternion((lastW, lastX, lastY, lastZ)) 274 | for interTime in GetInterKeyRange(lastTime, time): 275 | interRotation = lastRotation.slerp(rotation, (interTime-lastTime) / (time-lastTime)) 276 | data_w.extend((interTime, interRotation.w)) 277 | data_x.extend((interTime, interRotation.x)) 278 | data_y.extend((interTime, interRotation.y)) 279 | data_z.extend((interTime, interRotation.z)) 280 | --------------------------------------------------------------------------------