├── .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('', buf)[0]
90 |
91 | def ReadInt(file):
92 | buf = file.read(4)
93 | if(len(buf) < 4):
94 | return None
95 | 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 |
--------------------------------------------------------------------------------