Compare commits

...

10 Commits

@ -1,27 +1,59 @@
# geopy
Python tools for manipulating .geo files, and Blender add-on for exporting .geo files.
A Blender add-on and Python tools for manipulating .geo and .anim files.
# Blender Add-On
The Blender add-on allows you to export .geo files.
The Blender add-on allows you to:
- export .geo files
- import .geo files
- import skeletons from skel_*.anim files
## Installing the Add-On
1. Download geopy-v0.2.1.zip
1. Download geopy-v0.2.5.zip
2. In Blender open "User Preferences" then select the "Add-ons" tab.
3. Click the "Install Add-on from File..." button and select the downloaded zip.
4. Enable the add-on and click the "Save User Preferences" button.
## Using the Add-On
### Exporting models
1. Select the meshes to export in object mode.
1. Select 1 or more meshes to export in object mode.
2. From the menu select File->Export->"City of Heroes (Feet) (.geo)" (or "City of Heroes (Meters) (.geo)" if your meshes have been scaled in meters).
3. Browse to the file you want to create and click "Export GEO".
## Notes
Notes: Models are exported with the name of the mesh they come from, so having the meshes named correctly is recommended.
Models are exported with the name of the mesh they come from, so having the meshes named correctly is recommended.
### Importing models
1. From the menu select File->Import->"City of Heroes (Feet) (.geo)" (or "City of Heroes (Meters) (.geo)" if your converting to meters).
2. Browse to the file you want to import and click "Import GEO". All models in the .geo file will then be imported.
Note: A model's name is expected to be in the format of "GEO_<bone>_<name>", where <bone> is the bone the model will be anchored to.
Note: If multiple armatures exist, the armature you wish to use must be selected. Otherwise it will use the first armature it will find.
Note: Presently LOD models are excluded from imports.
### Importing skeletons
1. From the menu select File->Import->"City of Heroes Skeleton (skel_*.anim).
2. Browse to the file you want to import and click "Import".
Note: A new aramature will be created using the name found inside the file.
Note: The import will fail if the .anim file is missing a skeleton hierarchy.
### Importing animations
1. From the menu select File->Import->"City of Heroes Animation (*.anim).
2. Browse to the file you want to import and click "Import".
Note: It will import as a NLA track to the currently selected armature.
Note: If bone in the animation is missing from armature, the import will fail.
Note: If there is an existing animation in the armature with the same name as what you're trying to import, the import will fail. Rename your existing animation, and retry the import.
# Tools

@ -2,7 +2,7 @@
bl_info = {
"name": "City of Heroes (.geo)",
"author": "TigerKat",
"version": (0, 2, 3),
"version": (0, 2, 9),
"blender": (2, 80, 0),
"location": "File > Import/Export,",
"description": "City of Heroes (.geo)",
@ -10,18 +10,29 @@ bl_info = {
"warning": "",
"category": "Import-Export"}
if "bpy" in locals():
def check_reload():
if "bpy" in locals():
import importlib
if "import_geo" in locals():
importlib.reload(import_geo)
if "import_skel" in locals():
importlib.reload(import_skel)
if "import_anim" in locals():
importlib.reload(import_anim)
if "export_geo" in locals():
importlib.reload(export_geo)
if "export_skel" in locals():
importlib.reload(export_skel)
if "export_anim" in locals():
importlib.reload(export_anim)
if "geo" in locals():
importlib.reload(geo)
if "geomesh" in locals():
importlib.reload(geomesh)
if "vec_math" in locals():
importlib.reload(vec_math)
check_reload()
## Python doesn't reload package sub-modules at the same time as __init__.py!
#import os.path
#import imp, sys
@ -68,6 +79,28 @@ class ImportGeoMetric(bpy.types.Operator, ImportHelper):
keywords = self.as_keywords(ignore=("filter_glob",))
return import_geo.load(self, context, 0.30480000376701355, **keywords)
class ImportSkel(bpy.types.Operator, ImportHelper):
bl_idname = "import_scene.geo_skel"
bl_label = "Import GEO Skeleton"
filename_ext = ".anim"
filter_glob = StringProperty(default="skel_*.anim", options={'HIDDEN'})
def execute(self, context):
from . import import_skel
keywords = self.as_keywords(ignore=("filter_glob",))
return import_skel.load(self, context, 1.0, **keywords)
class ImportAnim(bpy.types.Operator, ImportHelper):
bl_idname = "import_scene.geo_anim"
bl_label = "Import GEO Animation"
filename_ext = ".anim"
filter_glob = StringProperty(default="*.anim", options={'HIDDEN'})
def execute(self, context):
from . import import_anim
keywords = self.as_keywords(ignore=("filter_glob",))
return import_anim.load(self, context, 1.0, **keywords)
class ExportGeo(bpy.types.Operator, ExportHelper):
bl_idname = "export_scene.geo"
bl_label = "Export GEO"
@ -94,17 +127,51 @@ class ExportGeoMetric(bpy.types.Operator, ExportHelper):
))
return export_geo.save(self, context, 1.0 / 0.30480000376701355, **keywords)
class ExportSkel(bpy.types.Operator, ExportHelper):
bl_idname = "export_scene.geo_skel"
bl_label = "Export GEO Skeleton"
filename_ext = ".anim"
filter_glob = StringProperty(default="skel_*.anim", options={'HIDDEN'})
def execute(self, context):
from . import export_skel
keywords = self.as_keywords(ignore=("filter_glob",
"check_existing",
))
return export_skel.save_skel(self, context, 1.0, **keywords)
class ExportAnim(bpy.types.Operator, ExportHelper):
bl_idname = "export_scene.geo_anim"
bl_label = "Export GEO Animation"
filename_ext = ".anim"
filter_glob = StringProperty(default="*.anim", options={'HIDDEN'})
def execute(self, context):
from . import export_anim
keywords = self.as_keywords(ignore=("filter_glob",
"check_existing",
))
return export_anim.save(self, context, 1.0, **keywords)
def menu_func_import(self, context):
self.layout.operator(ImportGeo.bl_idname,
text="City of Heroes (Feet) (.geo)")
self.layout.operator(ImportGeoMetric.bl_idname,
text="City of Heroes (Meters) (.geo)")
self.layout.operator(ImportSkel.bl_idname,
text="City of Heroes Skeleton (skel_*.anim)")
self.layout.operator(ImportAnim.bl_idname,
text="City of Heroes Animation (.anim)")
def menu_func_export(self, context):
self.layout.operator(ExportGeo.bl_idname,
text="City of Heroes (Feet) (.geo)")
self.layout.operator(ExportGeoMetric.bl_idname,
text="City of Heroes (Meters) (.geo)")
self.layout.operator(ExportSkel.bl_idname,
text="City of Heroes Skeleton (skel_*.anim)")
self.layout.operator(ExportAnim.bl_idname,
text="City of Heroes Animation (.anim)")
def make_annotations(cls):
"""Converts class fields to annotations if running with Blender 2.8"""
@ -122,8 +189,12 @@ def make_annotations(cls):
classes = (
ImportGeo,
ImportGeoMetric,
ImportSkel,
ImportAnim,
ExportGeo,
ExportGeoMetric
ExportGeoMetric,
ExportSkel,
ExportAnim,
)
def register():
#bpy.utils.register_module(__name__)

@ -3,17 +3,19 @@ import sys
try:
from .bones import *
from .util import Data
from .util import *
from .geomesh import GeoMesh, GeoFace, GeoVertex
from .compression_anim import *
except:
from bones import *
from util import *
from geomesh import GeoMesh, GeoFace, GeoVertex
from compression_anim import *
#.anim header
#Offset Size Description
#0 4 (int32) headerSize Size of this header plus BoneAnimTrack data.
#0 4 (int32) headerSize Size of this header plus BoneAnimTrack data and skeleton hierarchy data.
#4 256 (asciiz) name
#260 256 (asciiz) baseAnimName
#516 4 (float32) max_hip_displacement
@ -44,7 +46,7 @@ except:
#20 -
#SkeletonHeirarchy
#0 4 (int) heirarchy_root ?? First bone in the heirarchy.
#0 4 (int) heirarchy_root ?? First bone in the hierarchy.
#4 12*100 (BoneLink[BONES_ON_DISK])
#1204 -
#BONES_ON_DISK = 100
@ -64,6 +66,107 @@ ROTATION_DELTACODED = 1 << 5
POSITION_DELTACODED = 1 << 6
ROTATION_COMPRESSED_NONLINEAR = 1 << 7
ROTATION_MASK = ROTATION_UNCOMPRESSED | ROTATION_COMPRESSED_TO_5_BYTES | ROTATION_COMPRESSED_TO_8_BYTES | ROTATION_DELTACODED | ROTATION_COMPRESSED_NONLINEAR
POSITION_MASK = POSITION_UNCOMPRESS | POSITION_COMPRESSED_TO_6_BYTES | POSITION_DELTACODED
class BoneLink:
def __init__(self, data_or_child = None, next = None, boneid = None):
self.child = -1
self.next = -1
self.boneid = -1
if data_or_child is None:
return
if next is None:
(self.child, self.next, self.boneid) = data_or_child.decode("<iii")
if self.child == 0 and self.next == 0 and self.boneid == 0:
#All zero indicates an unused bone when written to disk.
#Change it to all -1 make it clear it's unused.
(self.child, self.next, self.boneid) = (-1, -1, -1)
else:
self.child = data_or_child
self.next = next
self.boneid = boneid
def encode(self):
if self.boneid == -1:
return struct.pack("<iii", 0, 0, 0)
return struct.pack("<iii", self.child, self.next, self.boneid)
def dump(self):
print("bone: %s (%s)" % (self.boneid, BONES_LIST[self.boneid]))
print(" child: %s (%s)" % (self.child, BONES_LIST[self.child]))
print(" next: %s (%s)" % (self.next, BONES_LIST[self.next]))
class SkeletonHierarchy:
def __init__(self):
self.root = -1
self.bones = []
def clear(self):
self.root = -1
self.bones = []
def parseData(self, data, size = None):
self.root = data.decode("<i")[0]
self.bones = []
if size is None:
size = len(data)
count = int(math.floor((size - 4) / 12.0))
for i in range(count): #BONES_ON_DISK):
self.bones.append(BoneLink(data))
def encode(self):
bone_max = len(self.bones)
data = struct.pack("<i", self.root)
blank_bone = BoneLink().encode()
for bl in self.bones:
if bl is None:
data += blank_bone
else:
data += bl.encode()
for i in range(bone_max, BONES_ON_DISK):
data += blank_bone
return data
def initBones(self, bone_id):
if bone_id == -1 or bone_id is None:
return
#Ensure the array is sized up to bone_id, and it's non-None.
if len(self.bones) <= bone_id:
#print("initBones(%s): %s: %s" % (bone_id, len(self.bones), self.bones,))
self.bones += [None] * (bone_id - len(self.bones) + 1)
#print("initBones(%s): %s: %s" % (bone_id, len(self.bones), self.bones,))
if self.bones[bone_id] is None or self.bones[bone_id].boneid == -1:
self.bones[bone_id] = BoneLink(-1, -1, bone_id)
def addBone(self, parent_id, bone_id):
self.initBones(bone_id)
self.initBones(parent_id)
if parent_id == -1 or parent_id is None:
#New bone is a root bone.
if self.root == -1:
#It's the first root bone.
self.root = bone_id
else:
#Find the last root bone, and add it to the end of the next chain.
last_root_id = self.root
while self.bones[last_root_id].next != -1:
last_root_id = self.bones[last_root_id].next
self.bones[last_root_id].next = bone_id
else:
#New bone has a parent.
if self.bones[parent_id].child != -1:
#Parent already has children, add it to the end of the next chain.
last_sibling_id = self.bones[parent_id].child
while self.bones[last_sibling_id].next != -1:
last_sibling_id = self.bones[last_sibling_id].next
self.bones[last_sibling_id].next = bone_id
else:
#It's the first child bone.
self.bones[parent_id].child = bone_id
def dump(self):
print("SkeletonHierarchy: root: %s (%s) bones: %s" % (self.root, BONES_LIST[self.root], len(self.bones)))
for i, b in enumerate(self.bones):
if b is None:
print("No bone: %s (%s)" % (i, BONES_LIST[i]))
else:
b.dump()
class BoneAnimTrack:
def __init__(self, data = b"", srcdata = None):
self.rawdata = data
@ -92,6 +195,118 @@ class BoneAnimTrack:
self.bone_id = self.data.decode("B")[0]
self.flags = self.data.decode("B")[0]
self.padding = self.data.read(2)
self.parseRotations()
self.parsePositions()
def parseRotations(self):
self.rotations = []
offset = self.srcdata.offset
self.srcdata.seek(self.rotations_data_offset)
method = self.flags & ROTATION_MASK
if method == ROTATION_UNCOMPRESSED:
for i in range(self.rot_count):
self.rotations.append(self.srcdata.decode("<ffff"))
pass
elif method == ROTATION_COMPRESSED_TO_5_BYTES:
for i in range(self.rot_count):
self.rotations.append(decompressQuaternion_5Byte(self.srcdata.read(5)))
pass
elif method == ROTATION_COMPRESSED_TO_8_BYTES:
for i in range(self.rot_count):
self.rotations.append(decompressQuaternion_8Byte(self.srcdata.read(8)))
pass
elif method == ROTATION_DELTACODED:
raise
pass
elif method == ROTATION_COMPRESSED_NONLINEAR:
raise Exception("Unsupported rotation compression: ROTATION_COMPRESSED_NONLINEAR")
pass
else:
#raise Exception("bone track rotation type 0x%2.2x unknown" % (self.flags, ))
pass
self.srcdata.seek(offset)
def parsePositions(self):
self.positions = []
offset = self.srcdata.offset
self.srcdata.seek(self.positions_data_offset)
method = self.flags & POSITION_MASK
if method == POSITION_UNCOMPRESS:
for i in range(self.pos_count):
self.positions.append(self.srcdata.decode("<fff"))
pass
elif method == POSITION_COMPRESSED_TO_6_BYTES:
for i in range(self.pos_count):
self.positions.append(decompressVector3_6Byte(self.srcdata.read(6)))
pass
elif method == POSITION_DELTACODED:
raise
pass
else:
#raise Exception("bone track position type 0x%2.2x unknown" % (self.flags, ))
pass
self.srcdata.seek(offset)
def encode(self, out_offset, out_data):
self.encodePositions()
self.encodeRotations()
self.data = Data()
self.rotations_data_offset = len(out_data) + out_offset
out_data.write(self.rotation_data)
self.positions_data_offset = len(out_data) + out_offset
out_data.write(self.position_data)
self.rot_count = self.rot_fullkeycount = len(self.rotations)
self.pos_count = self.pos_fullkeycount = len(self.positions)
self.data.encode("<I", self.rotations_data_offset)
self.data.encode("<I", self.positions_data_offset)
self.data.encode("<H", self.rot_fullkeycount)
self.data.encode("<H", self.pos_fullkeycount)
self.data.encode("<H", self.rot_count)
self.data.encode("<H", self.pos_count)
self.data.encode("<BBBB", self.bone_id, self.flags, 0, 0)
self.rawdata = self.data.data
return self.rawdata
def encodePositions(self):
#Choose encoding.
require_full = False
for pos in self.positions:
for v in pos:
if abs(v) > 1.0:
require_full = True
self.flags = self.flags & ~POSITION_MASK
#Set flags and endode data
self.position_data = b""
if require_full:
self.flags = self.flags | POSITION_UNCOMPRESS
for pos in self.positions:
self.position_data += struct.pack("<fff", pos[0], pos[1], pos[2])
else:
self.flags = self.flags | POSITION_COMPRESSED_TO_6_BYTES
for pos in self.positions:
self.position_data += compressVector3_6Byte(pos)
pass
def encodeRotations(self):
#Choose encoding.
require_full = False
#for pos in self.positions:
# for v in pos:
# if abs(v) > 1.0:
# require_full = True
self.flags = self.flags & ~ROTATION_MASK
#Set flags and endode data
self.rotation_data = b""
if require_full:
self.flags = self.flags | ROTATION_UNCOMPRESS
for rot in self.rotattions:
self.rotation_data += struct.pack("<ffff", rot[0], rot[1], rot[2], rot[3])
else:
self.flags = self.flags | ROTATION_COMPRESSED_TO_5_BYTES
for rot in self.rotations:
self.rotation_data += compressQuaternion_5Byte(rot)
pass
def dump(self):
print("rotations_data_offset: %s" % (self.rotations_data_offset, ))
print("positions_data_offset: %s" % (self.positions_data_offset, ))
@ -99,9 +314,14 @@ class BoneAnimTrack:
print("pos_fullkeycount: %s" % (self.pos_fullkeycount, ))
print("rot_count: %s" % (self.rot_count, ))
print("pos_count: %s" % (self.pos_count, ))
if self.bone_id > len(BONES_LIST):
print("bone_id: %s (?)" % (self.bone_id, ))
else:
print("bone_id: %s %s" % (self.bone_id, BONES_LIST[self.bone_id]))
print("flags: 0x%2.2x" % (self.flags, ))
print("padding: %s" % (self.padding, ))
print("rotations: %s" % (self.rotations, ))
print("positions: %s" % (self.positions, ))
class Anim:
@ -119,7 +339,7 @@ class Anim:
self.header_bone_track_count = 0
self.header_rotation_compression_type = 0
self.header_position_compression_type = 0
self.header_skeleton_heirarchy_offset = 0
self.header_skeleton_hierarchy_offset = 0
self.header_backup_anim_track = 0
self.header_load_state = 0
self.header_last_time_used = 0
@ -127,7 +347,7 @@ class Anim:
self.header_spare_room = b""
self.bone_tracks = []
self.skeleton_heirarchy = []
self.skeleton_hierarchy = None
pass
def loadFromData(self, data):
@ -142,10 +362,10 @@ class Anim:
pass
def saveToData(self):
self.encode()
return self.data
return self.data.data
def saveToFile(self, fileout):
self.encode()
fileout.write(self.data)
fileout.write(self.data.data)
def parseData(self):
self.data.seek(0)
self.header_size = self.data.decode("i")[0]
@ -157,7 +377,7 @@ class Anim:
self.header_bone_track_count = self.data.decode("i")[0]
self.header_rotation_compression_type = self.data.decode("i")[0]
self.header_position_compression_type = self.data.decode("i")[0]
self.header_skeleton_heirarchy_offset = self.data.decode("I")[0]
self.header_skeleton_hierarchy_offset = self.data.decode("I")[0]
self.header_backup_anim_track = self.data.decode("I")[0]
self.header_load_state = self.data.decode("i")[0]
self.header_last_time_used = self.data.decode("f")[0]
@ -169,9 +389,77 @@ class Anim:
self.data.seek(self.header_bone_tracks_offset)
for i in range(self.header_bone_track_count):
self.bone_tracks.append(BoneAnimTrack(self.data.read(20), self.data))
self.skeleton_hierarchy = None
if self.header_skeleton_hierarchy_offset > 0:
self.data.seek(self.header_skeleton_hierarchy_offset)
self.skeleton_hierarchy = SkeletonHierarchy()
if self.header_skeleton_hierarchy_offset > self.header_bone_tracks_offset:
skel_size = self.header_size - self.header_skeleton_hierarchy_offset
else:
skel_size = self.header_bone_tracks_offset - self.header_skeleton_hierarchy_offset
self.skeleton_hierarchy.parseData(self.data, skel_size)
pass
def encode(self):
self.data = None
if isinstance(self.header_name, str):
self.header_name = bytes(self.header_name, "utf-8")
if isinstance(self.header_base_anim_name, str):
self.header_base_anim_name = bytes(self.header_base_anim_name, "utf-8")
header_base_size = 596
header_bone_tracks_size = 20 * len(self.bone_tracks)
if self.skeleton_hierarchy is None:
skel_data = b""
header_skeleton_hierarhy_size = 0
else:
skel_data = self.skeleton_hierarchy.encode()
header_skeleton_hierarhy_size = len(skel_data)
#todo: verify all defined bones are present in the track data
header_total_size = header_base_size + header_bone_tracks_size + header_skeleton_hierarhy_size
header_bone_track_data = b""
bone_track_data = Data()
for bt in self.bone_tracks:
header_bone_track_data += bt.encode(header_total_size, bone_track_data)
if len(header_bone_track_data) != header_bone_tracks_size:
#Verify the header is the right length.
raise Exception("Internal error: Header bone tracks size mismatch! Got: %s Expected: %s" % (len(header_bone_tracks), header_bone_tracks_size))
self.header_size = header_total_size
#self.header_max_hip_displacement = 0
self.header_length = 0
for bt in self.bone_tracks:
self.header_length = max(self.header_length, len(bt.positions), len(bt.rotations))
self.header_length = max(0, self.header_length - 1)
self.header_bone_tracks_offset = header_base_size + header_skeleton_hierarhy_size
self.header_bone_track_count = len(self.bone_tracks)
self.header_rotation_compression_type = 0
self.header_position_compression_type = 0
if self.skeleton_hierarchy is None:
self.header_skeleton_hierarchy_offset = 0
else:
self.header_skeleton_hierarchy_offset = header_base_size
self
header_data = (
struct.pack("<i256s256sff",
header_total_size,
storeString(self.header_name, 256),
storeString(self.header_base_anim_name, 256),
self.header_max_hip_displacement,
self.header_length) +
struct.pack("<IiiiI",
self.header_bone_tracks_offset,
self.header_bone_track_count,
self.header_rotation_compression_type,
self.header_position_compression_type,
self.header_skeleton_hierarchy_offset) +
ZERO_BYTE * (4 * 13))
#Verify header lengths
if len(header_data) != header_base_size:
raise Exception("Internal error: Header base size mismatch! Got: %s Expected: %s" % (len(header_data), header_base_size))
#todo: total header length
rawdata = header_data + skel_data + header_bone_track_data + bone_track_data.data
self.data = Data(rawdata)
return rawdata
def dump(self):
print("header_size: %s" % (self.header_size, ))
@ -183,7 +471,7 @@ class Anim:
print("header_bone_track_count: %s" % (self.header_bone_track_count, ))
print("header_rotation_compression_type: %s" % (self.header_rotation_compression_type, ))
print("header_position_compression_type: %s" % (self.header_position_compression_type, ))
print("header_skeleton_heirarchy_offset: %s" % (self.header_skeleton_heirarchy_offset, ))
print("header_skeleton_hierarchy_offset: %s" % (self.header_skeleton_hierarchy_offset, ))
print("header_backup_anim_track: %s" % (self.header_backup_anim_track, ))
print("header_load_state: %s" % (self.header_load_state, ))
print("header_last_time_used: %s" % (self.header_last_time_used, ))
@ -193,8 +481,39 @@ class Anim:
print("Bone Tracks:")
for bt in self.bone_tracks:
bt.dump()
if self.skeleton_hierarchy is not None:
self.skeleton_hierarchy.dump()
pass
def checkSkeletonHierarchy(self):
if self.skeleton_hierarchy is None:
return False
return True
def checkSkeletonBones(self):
if self.skeleton_hierarchy is None:
return False
seen = []
if self.checkSkeletonBonesBody(self.skeleton_hierarchy.root, seen):
return True
return False
def checkSkeletonBonesBody(self, bone_id, seen):
if bone_id == -1:
return True
if bone_id < -1:
return False
if bone_id in seen:
return False
seen.append(bone_id)
bones = self.skeleton_hierarchy.bones
if bone_id >= len(bones):
return False
if bones[bone_id].boneid != bone_id:
return False
if not self.checkSkeletonBonesBody(bones[bone_id].next, seen):
return False
if not self.checkSkeletonBonesBody(bones[bone_id].child, seen):
return False
return True
if __name__ == "__main__":
if len(sys.argv) <= 1 or len(sys.argv) > 3:
print("Usage:")

@ -114,6 +114,8 @@ BONES_LIST = [
"Leg_R_Jet2",
]
BONES_ON_DISK = 100
BONES_LEFT = []
BONES_RIGHT = []
BONES_SWAP = []

@ -0,0 +1,107 @@
import math
import struct
MAX_5_BYTE_QUATERNION = 1 / (2 ** 0.5)
SCALE_8_BYTE_QUATERNION_COMPRESS = 10000
SCALE_8_BYTE_QUATERNION_DECOMPRESS = 1.0 / SCALE_8_BYTE_QUATERNION_COMPRESS
SCALE_6_BYTE_VECTOR3_COMPRESS = 32000
SCALE_6_BYTE_VECTOR3_DECOMPRESS = 1.0 / SCALE_6_BYTE_VECTOR3_COMPRESS
def findBiggest(quat):
biggest_i = -1
biggest_v = 0
for i, v in enumerate(quat):
if abs(v) > biggest_v:
biggest_v = abs(v)
biggest_i = i
if quat[biggest_i] < 0:
return (biggest_i, (-quat[0], -quat[1], -quat[2], -quat[3]))
else:
return (biggest_i, ( quat[0], quat[1], quat[2], quat[3]))
def compressQuaternion_5Byte(quat):
#Find biggset value and negate the quaternion to make it positive.
missing, q = findBiggest(quat)
d = []
for i in range(4):
if i == missing:
continue
v = int(math.floor(0.5 + q[i] / MAX_5_BYTE_QUATERNION * 2048))
if v < -2048:
v = -2048
elif v > 2047:
v = 2047
d.append(v + 2048)
v = (
(missing << 36) |
(d[0] << 24) |
(d[1] << 12) |
d[2]
)
s = struct.pack("<Q", v)
#Return with the top most byte first, but remaining bytes in little endian order.
return s[4:5] + s[0:4]
def decompressQuaternion_5Byte(data):
#Source data has the top most byte first, but remaining bytes are little endian.
#print("decompressQuaternion_5Byte(data): %s" % ([data],))
#Rearrange byte data into a little endian uint64.
s = data[1:5] + data[0:1] + b"\x00\x00\x00"
(v, ) = struct.unpack("<Q", s)
#Parse out the data.
missing = (v >> 36) & 0x3
d = [
(v >> 24) & 0xfff,
(v >> 12) & 0xfff,
v & 0xfff,
]
#Rescale values and summing the squares into x.
x = 0
#print("%s" % (d, ))
for i in range(3):
d[i] = (d[i] - 2048) * MAX_5_BYTE_QUATERNION / 2048.0
x += d[i] ** 2.0
#print("%s -> %s" % (d, x))
#Use pythagoras to compute the missing field into x.
x = (1 - x) ** 0.5
#Rebuild the quaternion.
d_i = 0;
q = []
for i in range(4):
if i == missing:
q.append(x)
else:
q.append(d[d_i])
d_i += 1
return q
def compressQuaternion_8Byte(quat):
return struct.pack("<hhhh",
int(math.floor(0.5 + quat[0] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(math.floor(0.5 + quat[1] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(math.floor(0.5 + quat[2] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(math.floor(0.5 + quat[3] * SCALE_8_BYTE_QUATERNION_COMPRESS))
)
def decompressQuaternion_8Byte(data):
d = struct.unpack("<hhhh", data[0:8])
return [
d[0] * SCALE_8_BYTE_QUATERNION_DECOMPRESS,
d[1] * SCALE_8_BYTE_QUATERNION_DECOMPRESS,
d[2] * SCALE_8_BYTE_QUATERNION_DECOMPRESS,
d[3] * SCALE_8_BYTE_QUATERNION_DECOMPRESS
]
def compressVector3_6Byte(vec):
return struct.pack("<hhh",
int(math.floor(0.5 + vec[0] * SCALE_6_BYTE_VECTOR3_COMPRESS)),
int(math.floor(0.5 + vec[1] * SCALE_6_BYTE_VECTOR3_COMPRESS)),
int(math.floor(0.5 + vec[2] * SCALE_6_BYTE_VECTOR3_COMPRESS))
)
def decompressVector3_6Byte(data):
d = struct.unpack("<hhh", data[0:6])
return [(x * SCALE_6_BYTE_VECTOR3_DECOMPRESS) for x in d]

@ -0,0 +1,212 @@
import bpy.path
import bpy
import math
from mathutils import Vector, Quaternion
try:
from .anim import *
from .bones import *
except:
from anim import *
from bones import *
def export_fix_coord(v):
return Vector((-v[0], v[2], -v[1]))
def export_fix_normal(v):
return Vector(( v[0], -v[2], v[1]))
def export_fix_quaternion(quat):
return Quaternion((-quat[1], quat[3], -quat[2], quat[0]))
def getBoneRotation(bone, bone_tracks, index):
if bone is None:
#rot_p = Quaternion()
return Quaternion()
else:
rot_p = getBoneRotation(bone.parent, bone_tracks, index)
if bone.name in bone_tracks:
trk = bone_tracks[bone.name]
chn = trk["rotation_quaternion"]
if index >= len(chn):
rot_s = chn[-1].copy()
else:
rot_s = chn[index].copy()
else:
rot_s = Quaternion()
rot_p.rotate(rot_s)
return rot_p
#rot_s.rotate(rot_p)
#return rot_s
def convert_animation(context, arm_obj, arm_data, nla_track, anim, save_skel):
bone_tracks = {}
for nla_strip in nla_track.strips:
#todo: what's the proper way to handle multiple strips?
#Presently later strips will overwrite earlier strips.
#>>> [x.data_path for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves]
#>>> [x.array_index for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves]
for crv in nla_strip.action.fcurves:
#>>> [y.co for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves for y in x.sampled_points]
dp = crv.data_path
idx = crv.array_index
print("crv: %s : %s" % (dp, idx))
#Naively convert data_path into bone, and transform type
parts_a = dp.split('["')
parts_b = parts_a[1].split('"].')
bone_name = parts_b[0]
transform = parts_b[1]
if bone_name not in bone_tracks:
bone_tracks[bone_name] = {}
bone_track = bone_tracks[bone_name]
if transform == "location":
data_type = Vector
elif transform == "rotation_quaternion":
data_type = Quaternion
else:
#todo:
raise
data_type = None
if transform not in bone_track:
bone_track[transform] = []
bone_track_channel = bone_track[transform]
for pnt in crv.sampled_points:
k = int(math.floor(pnt.co[0] + 0.5))
if k < 0:
#ignore samples before 0
continue
while len(bone_track_channel) <= k:
bone_track_channel.append(data_type())
bone_track_channel[k][idx] = pnt.co[1]
#print("bone_tracks: %s" % bone_tracks)
print("bone_tracks['Head']: %s" % bone_tracks.get("Head", None))
#todo: convert FCurve data to track positions and rotations
#todo: Get bones required for export.
if save_skel:
#If we need to save the skeleton, ensure we have values for the T-pose loaded into bones that haven't been referenced yet.
for bn in arm_data.bones.keys():
if bn not in bone_tracks:
bone_tracks[bn] = {
"location": [Vector()],
"rotation_quaternion": [Quaternion()],
}
#todo: trim back tracks that have duplicates on their tail.
for bn, bt in bone_tracks.items():
#ensure missing channels have a T-pose value.
if "location" not in bt:
bt["location"] = [Vector()]
if "rotation_quaternion" not in bt:
bt["rotation_quaternion"] = [Quaternion()]
#Trim back duplicates at the end of a track.
for cn in ("location", "rotation_quaternion"):
chn = bt[cn]
while len(chn) >= 2:
if chn[-1] == chn[-2]:
chn.pop()
else:
break
#Convert track positions and rotations into a more convenient form.
for bn, bt in bone_tracks.items():
bone = arm_data.bones[bn]
#Get position of bone, relative to parent (or armature origin for root bones).
if bone.parent is None:
bone_position = bone.head
else:
bone_position = bone.head + (bone.parent.tail - bone.parent.head)
#print("parent[%s]: %s %s" % (bone.parent.name, bone.parent.head, bone.parent.tail))
print("bone_position[%s(%s)]: %s" % (bn, bone.name, bone_position))
bt["net_location"] = [bone_position]
bt["net_rotation_quaternion"] = []
rot_chn = bt["rotation_quaternion"]
for i in range(len(rot_chn)):
bt["net_rotation_quaternion"].append(getBoneRotation(bone, bone_tracks, i))
for i in range(1, len(bt["location"])):
if i >= len(bt["net_rotation_quaternion"]):
rot = bt["net_rotation_quaternion"][-1]
else:
rot = bt["net_rotation_quaternion"][i]
pos = bone_position + bt["location"][i]
print(" pos[%s]: %s" % (i, pos))
pos.rotate(rot)
bt["net_location"].append(pos)
#print("bone_tracks: %s" % bone_tracks)
#Store bone track information in the Anim
for bn, bt in bone_tracks.items():
bat = BoneAnimTrack()
bat.bone_id = BONES_LOOKUP[bn]
bat.rotations = [export_fix_quaternion(x) for x in bt["rotation_quaternion"]]
bat.positions = [export_fix_coord(x) for x in bt["net_location"]]
anim.bone_tracks.append(bat)
#Save the skeleton (if flagged).
if save_skel:
anim.skeleton_hierarchy = SkeletonHierarchy()
for bn in BONES_LIST:
if bn in arm_data.bones:
bone = arm_data.bones[bn]
if bone.parent is None:
parent_id = None
else:
parent_id = BONES_LOOKUP[bone.parent.name]
bone_id = BONES_LOOKUP[bn]
anim.skeleton_hierarchy.addBone(parent_id, bone_id)
anim.dump()
pass
def save(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True, save_skeleton = False):
#todo: prefer armature name that matches import?
#Choose the first selected armature as the armature to attach to.
armature = None
for ob in context.selected_objects:
if ob.type == "ARMATURE":
armature_obj = ob
armature = ob.data
break
else:
#If none found try again with all objects.
for ob in bpy.data.objects:
if ob.type == "ARMATURE":
armature_obj = ob
armature = ob.data
break
#todo:
track = None
skel_track = None
#todo: error/warning if multiple tracks are selected.
#todo: error/warning if multiple skel_ tracks are found
#todo: error if no skel_tracks are found
for t in armature_obj.animation_data.nla_tracks:
if t.select:
track = t
if t.name.lower().startswith("skel_"):
skel_track = t
if save_skeleton:
skel_track = track
arm_name = armature_obj.name
track_name = bpy.path.display_name_from_filepath(filepath)
skel_track_name = skel_track.name
#Get name and base name
anim_name = "%s/%s" % (arm_name, track_name)
anim_base_name = "%s/%s" % (arm_name, skel_track_name)
#todo: warning if anim_name doesn't match file path
anim = Anim()
anim.header_name = anim_name
anim.header_base_anim_name = anim_base_name
save_skel = save_skeleton or anim_name == anim_base_name
convert_animation(context, armature_obj, armature, track, anim, save_skel)
data = anim.saveToData()
fh = open(filepath, "wb")
fh.write(data)
fh.close()
return {'FINISHED'}

@ -0,0 +1,12 @@
import bpy.path
import bpy
try:
from .export_anim import *
except:
from export_anim import *
def save_skel(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True):
return save(operator, context, scale = scale, filepath = filepath, global_matrix = global_matrix, use_mesh_modifiers = use_mesh_modifiers, save_skeleton = True)

@ -947,6 +947,7 @@ class Model:
v = GeoVertex(coord, normal, uv, weights)
geomesh.getGeoVertexIndexNew(v)
texture_indexes = []
#print("Geo.tex_idx: %s Geo.geo.header_texnames: %s" % (self.tex_idx, self.geo.header_texnames))
for t in self.tex_idx:
texture_indexes += [geomesh.getTextureIndex(self.geo.header_texnames[t[0]])] * t[1]
#print("len(self.verts): %s" % (len(self.verts), ))

@ -73,7 +73,11 @@ class GeoMesh:
self.geovertex.append(gv)
return index
def getTextureIndex(self, name):
if isinstance(name, int):
#Convenience, assume ints are from a previous look up.
return name
index = self.textures_map.get(name, len(self.textures))
#print("GeoMesh.getTextureIndex(%s): %s" % (name, index))
if index == len(self.textures):
self.textures_map[name] = index
self.textures.append(name)

@ -0,0 +1,191 @@
import bpy.path
import bpy
from mathutils import Vector, Quaternion
try:
from .anim import *
from .bones import *
except:
from anim import *
from bones import *
def import_fix_coord(v):
return Vector((-v[0], -v[2], v[1]))
def import_fix_normal(v):
return Vector(( v[0], v[2], -v[1]))
def import_fix_quaternion(quat):
return Quaternion((quat[3], -quat[0], -quat[2], quat[1]))
def getBoneLength(arm_data, bone_name):
bl = arm_data.bones[bone_name]
if bl.parent is None:
return bl.head
def getBoneRotation(bone, bone_trk_lookup, trk_rot_list, index):
if bone is None:
#rot_p = Quaternion()
return Quaternion()
else:
rot_p = getBoneRotation(bone.parent, bone_trk_lookup, trk_rot_list, index)
if bone.name in bone_trk_lookup:
trk_index = bone_trk_lookup[bone.name]
rot_list = trk_rot_list[trk_index]
if index >= len(rot_list):
rot_s = rot_list[-1].copy()
else:
rot_s = rot_list[index].copy()
else:
rot_s = Quaternion()
rot_p.rotate(rot_s)
return rot_p
#rot_s.rotate(rot_p)
#return rot_s
def convertAnimation(context, arm_obj, arm_data, anim, rescale = True):
full_name = anim.header_name.decode("utf-8")
anim_name = full_name.split("/")[1]#.lstrip("skel_")
#get all bones used in animation, and maximum fram count
max_frames = 0
bone_ids = []
bone_names = []
bone_trk_lengths = []
bone_arm_lengths = []
bone_scales = []
bone_trk_lookup = {}
for i, bt in enumerate(anim.bone_tracks):
#get maximum frame count
max_frames = max(max_frames, len(bt.positions), len(bt.rotations))
#get IDs and names
bone_id = bt.bone_id
bone_name = BONES_LIST[bone_id]
bone_trk_lookup[bone_name] = i
bone_ids.append(bone_id)
bone_names.append(bone_name)
#get animation's T-pose length
bone_trk_len = Vector(bt.positions[0]).length
bone_trk_lengths.append(bone_trk_len)
#get armature's T-pose length
bone_arm_len = getBoneLength(arm_data, bone_name)
bone_arm_lengths.append(bone_arm_len)
#determine scale
bone_scale = rescale and (bone_arm_len / bone_trk_len) or (1.0)
bone_scales.append(bone_scale)
#create animation
if arm_obj.animation_data is None:
arm_obj.animation_data_create()
if anim_name in arm_obj.animation_data.nla_tracks:
#todo: properly handle cases where the name already exists
nla_track = arm_obj.animation_data.nla_tracks[anim_name]
pass
else:
nla_track = arm_obj.animation_data.nla_tracks.new()
nla_track.name = anim_name
action = bpy.data.actions.new(anim_name)
action.use_fake_user = True
nla_strip = nla_track.strips.new(anim_name, 0, action)
nla_strip.action_frame_start = 0
nla_strip.action_frame_end = max_frames
#Extract all position and rotation track data in blender coordinates.
trk_pos_list = []
trk_rot_list = []
for i, bt in enumerate(anim.bone_tracks):
#pos_start = (len(bt.positions) > 1) and 1 or 0
pos_start = 0
pos_stop = len(bt.positions)
#rot_start = (len(bt.rotations) > 1) and 1 or 0
rot_start = 0
rot_stop = len(bt.rotations)
pos_list = []
rot_list = []
for j in range(pos_start, pos_stop):
v = import_fix_coord(bt.positions[j])
pos_list.append(v)
pass
for j in range(rot_start, rot_stop):
v = import_fix_quaternion(bt.rotations[j])
rot_list.append(v)
pass
trk_pos_list.append(pos_list)
trk_rot_list.append(rot_list)
#Iterate over bone tracks and generate FCurves for each of them.
for i, bt in enumerate(anim.bone_tracks):
bone_name = bone_names[i]
bone = arm_data.bones[bone_name]
pose_bone = arm_obj.pose.bones[bone_name]
#pos_start = (len(bt.positions) > 1) and 1 or 0
pos_start = 0
pos_stop = len(bt.positions)
#rot_start = (len(bt.rotations) > 1) and 1 or 0
rot_start = 0
rot_stop = len(bt.rotations)
pos_list = trk_pos_list[i]
rot_list = trk_rot_list[i]
props = [(pose_bone.path_from_id("location"), 3, bone_name), #"Location"),
(pose_bone.path_from_id("rotation_quaternion"), 4, bone_name), #"Quaternion Rotation"),
#(pose_bone.path_from_id("rotation_axis_angle"), 4, "Axis Angle Rotation"),
#(pose_bone,path_from_id("rotatin_euler"), 3, "Euler Rotation"),
#(pose_bone.path_from_id("scale"), 3, "Scale"),
]
curves = [action.fcurves.new(prop, index = cidx, action_group = agrp)
for prop, channel_count, agrp in props
for cidx in range(channel_count)]
pos_curves = curves[0:3]
rot_curves = curves[3:7]
for j, pos in enumerate(pos_list):
#Remove the bone component from the position.
if bone.parent is None:
#Only compute it for root bones.
rot = getBoneRotation(bone.parent, bone_trk_lookup, trk_rot_list, j)
#rot.invert()
pos0 = pos_list[0].copy()
pos0.rotate(rot)
l = (pos - pos0).length
if l >= 0.001:# and (bone.name in ["Hips", "Waist","Chest"]):
print("%s, %s: %s: pos: %s : %s ::: %s : %s" % (bone.name, j, l, pos, pos0, pos_list[0], rot))
pos = pos - pos0
if l < 0.001:
#Distance is close to zero, force position adjustment to zero.
pos = Vector()
else:
#assume 0,0,0 correction in other nodes.
pos = Vector()
for k, crv in enumerate(pos_curves):
crv.keyframe_points.insert(j, pos[k], options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
for j, rot in enumerate(rot_list):
for k, crv in enumerate(rot_curves):
crv.keyframe_points.insert(j, rot[k], options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
for crv in curves:
crv.update()
#todo: delete mid points for simple motions?
pass
def load(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True, ignore_lod = True):
#Load .anim file
fh_in = open(filepath, "rb")
anim = Anim()
anim.loadFromFile(fh_in)
fh_in.close()
#todo: prefer armature name that matches import?
#Choose the first selected armature as the armature to attach to.
armature = None
for ob in context.selected_objects:
if ob.type == "ARMATURE":
armature_obj = ob
armature = ob.data
break
else:
#If none found try again with all objects.
for ob in bpy.data.objects:
if ob.type == "ARMATURE":
armature_obj = ob
armature = ob.data
break
convertAnimation(context, armature_obj, armature, anim, False)
return {'FINISHED'}

@ -26,6 +26,17 @@ else:
l.reverse()
return l
def extractRootBoneFromName(name):
if name.startswith("GEO_"):
name = name[4:]
if name.startswith("N_"):
name = name[2:]
bone = name.split("_")[0]
if bone in BONES_LOOKUP:
return bone
else:
return None
def convert_model(geo_model, mesh_data, obj, scale):
#Convert the geo_model into a GeoMesh.
geomesh = geo_model.saveToGeoMesh()
@ -33,6 +44,15 @@ def convert_model(geo_model, mesh_data, obj, scale):
indices = [i for face in geomesh.face for i in import_fix_winding(face.vert_indexes)]
texture_indices = [face.texture_index for face in geomesh.face]
#Create materials for textures.
mesh_data.materials.clear()
#print("geomesh.textures: %s" % (geomesh.textures,))
for i, tex_name in enumerate(geomesh.textures):
#if isinstance(tex_name, int):
# continue
#print("tex_name: %s" % tex_name)
mesh_data.materials.append(bpy.data.materials.new(tex_name.decode("utf-8")))
mesh_data.vertices.add(len(geomesh.geovertex))
mesh_data.loops.add(len(indices))
mesh_data.polygons.add(len(geomesh.face))
@ -82,9 +102,10 @@ def convert_model(geo_model, mesh_data, obj, scale):
#todo: attempt to load textures/images
def getBonePositionBody(bone):
#todo: fix this: Presently assumes that the head to tail will always be the same direction and length.
if bone.parent is None:
p = bone.tail
print("root: %s: %s (%s)" % (bone, p, bone.head))
p = bone.head
print("root: %s: %s (%s)" % (bone, p, bone.tail))
return p
else:
p = bone.tail + getBonePositionBody(bone.parent)
@ -142,9 +163,17 @@ def load(operator, context, scale = 1.0, filepath = "", global_matrix = None, us
if bone_name is None:
#No bones, don't attach.
pass
else:
preferred_bone_name = extractRootBoneFromName(model_name)
arm_pos = armature_obj.matrix_world.translation
if preferred_bone_name is not None:
bone_pos = getBonePosition(armature, preferred_bone_name)
obj.matrix_world = mathutils.Matrix.Translation(arm_pos + bone_pos)
obj.modifiers.new(name = 'Armature', type = 'ARMATURE')
obj.modifiers['Armature'].object = armature_obj
elif bone_name in armature.bones:
bone_pos = getBonePosition(armature, bone_name)
obj.matrix_world = mathutils.Matrix.Translation(bone_pos)
obj.matrix_world = mathutils.Matrix.Translation(arm_pos + bone_pos)
obj.modifiers.new(name = 'Armature', type = 'ARMATURE')
obj.modifiers['Armature'].object = armature_obj
else:

@ -0,0 +1,110 @@
import bpy.path
import bpy
from mathutils import Vector, Quaternion
try:
from .anim import *
from .bones import *
from .import_anim import convertAnimation
except:
from anim import *
from bones import *
from import_anim import convertAnimation
def import_fix_coord(v):
return Vector((-v[0], -v[2], v[1]))
def import_fix_normal(v):
return Vector(( v[0], v[2], -v[1]))
def import_fix_quaternion(quat):
return Quaternion((quat[3], -quat[0], -quat[2], quat[1]))
TAIL_LENGTH = 0.05 #0.204377
TAIL_VECTOR = Vector((0, 1, 0))
def getTposeOffset(anim, bone_id):
offset = Vector((0, 0, 0))
for bt in anim.bone_tracks:
if bt.bone_id == bone_id:
return import_fix_coord(bt.positions[0])
return offset
def convertBone(anim, arm_obj, arm_data, bone_id, parent, parent_position, tail_nub):
bone_link = anim.skeleton_hierarchy.bones[bone_id]
#if bone_link.boneid != bone_id:
# raise Exception("")
bone_name = BONES_LIST[bone_id]
# Create the (edit)bone.
new_bone = arm_data.edit_bones.new(name=bone_name)
new_bone.select = True
new_bone.name = bone_name
bone_offset = getTposeOffset(anim, bone_id)
bone_position = parent_position + bone_offset
if parent is None:
if tail_nub:
#new_bone.head = bone_offset + TAIL_VECTOR * TAIL_LENGTH
#new_bone.tail = bone_offset
new_bone.head = bone_position
new_bone.tail = bone_position + TAIL_VECTOR * TAIL_LENGTH
else:
raise
pass
else:
new_bone.parent = parent
if tail_nub:
#new_bone.head = bone_offset - TAIL_VECTOR * TAIL_LENGTH
#new_bone.tail = bone_offset
new_bone.head = bone_position
new_bone.tail = bone_position + TAIL_VECTOR * TAIL_LENGTH
else:
raise
#visit children
if bone_link.next != -1:
convertBone(anim, arm_obj, arm_data, bone_link.next, parent, parent_position, tail_nub)
if bone_link.child != -1:
convertBone(anim, arm_obj, arm_data, bone_link.child, new_bone, bone_position, tail_nub)
def convertSkeleton(context, anim):
full_name = anim.header_name.decode("utf-8")
skeleton_name = full_name.split("/")[0]
#create armature
arm_data = bpy.data.armatures.new(name=skeleton_name)
arm_obj = bpy.data.objects.new(name=skeleton_name, object_data=arm_data)
# instance in scene
context.view_layer.active_layer_collection.collection.objects.link(arm_obj)
arm_obj.select_set(True)
# Switch to Edit mode.
context.view_layer.objects.active = arm_obj
is_hidden = arm_obj.hide_viewport
arm_obj.hide_viewport = False # Can't switch to Edit mode hidden objects...
bpy.ops.object.mode_set(mode='EDIT')
#Traverse tree, creating bones, and position heads and tails.
convertBone(anim, arm_obj, arm_data, anim.skeleton_hierarchy.root, None, Vector(), True)
#Switch to Object mode.
bpy.ops.object.mode_set(mode='OBJECT')
arm_obj.hide_viewport = is_hidden
convertAnimation(context, arm_obj, arm_data, anim, rescale = False)
def load(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True, ignore_lod = True):
#Load .anim file
fh_in = open(filepath, "rb")
anim = Anim()
anim.loadFromFile(fh_in)
fh_in.close()
#Check for a skeleton hierarchy
if not anim.checkSkeletonHierarchy():
raise Exception("Animation does not have a skeleton. (Or it has errors.)")
#todo: check for a T-pose
convertSkeleton(context, anim)
return {'FINISHED'}

@ -1,6 +1,8 @@
import struct
import sys
ZERO_BYTE = struct.pack("B", 0)
if sys.version_info[0] < 3:
byte = chr
unbyte = ord

Loading…
Cancel
Save