From bdfc38e82ed48175fe968fafffd1008205161799 Mon Sep 17 00:00:00 2001 From: TigerKat Date: Sun, 28 Mar 2021 16:51:11 +0930 Subject: [PATCH] Early work for perfroming animation importing and exporting. --- anim.py | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ util.py | 26 +++++++ 2 files changed, 246 insertions(+) create mode 100644 anim.py diff --git a/anim.py b/anim.py new file mode 100644 index 0000000..ce0e0a0 --- /dev/null +++ b/anim.py @@ -0,0 +1,220 @@ +#! /usr/bin/python3 +import sys + +try: + from .bones import * + from .util import Data + from .geomesh import GeoMesh, GeoFace, GeoVertex +except: + from bones import * + from util import * + from geomesh import GeoMesh, GeoFace, GeoVertex + + +#.anim header +#Offset Size Description +#0 4 (int32) headerSize Size of this header plus BoneAnimTrack data. +#4 256 (asciiz) name +#260 256 (asciiz) baseAnimName +#516 4 (float32) max_hip_displacement +#520 4 (float32) length +#524 4 (ptr u32) bone_tracks Offset to array of BoneAnimTrack structures. +#528 4 (int32) bone_track_count +#532 4 (int32) rotation_compression_type ?? +#536 4 (int32) position_compression_type ?? +#540 4 (ptr u32) skeletonHeirarchy ?? SkeletonHeirarchy +#Below are reserved for use by the game. +#544 4 (ptr u32) backupAnimTrack *SkeletonAnimTrack +#548 4 (int32) loadstate +#552 4 (float32) lasttimeused +#556 4 (int32) fileAge +#560 4*9 (int32[9]) spare_room +#596 - + +#BoneAnimTrack +#0 4 (ptr u32) rot_idx ?? Offset to the rotation data. Format depends on compression method. +#4 4 (ptr u32) pos_idx ?? Offset to the position data. Format depends on compression method. +#8 2 (u16) rot_fullkeycount ?? +#10 2 (u16) pos_fullkeycount ?? +#12 2 (u16) rot_count ?? +#14 2 (u16) pos_count ?? +#16 1 (char) id Bone ID. +#17 1 (char) flags ?? +#18 2 (u16) pack_pad Padding to push the structure to 4 byte alignment. +#20 - + +#SkeletonHeirarchy +#0 4 (int) heirarchy_root ?? First bone in the heirarchy. +#4 12*100 (BoneLink[BONES_ON_DISK]) +#1204 - +#BONES_ON_DISK = 100 + +#BoneLink +#0 4 (int) child ?? First child bone? +#4 4 (int) next ?? The next sibling bone? +#8 4 (int/BoneId) id The ID of this bone. +#12 - + +ROTATION_UNCOMPRESSED = 1 << 0 +ROTATION_COMPRESSED_TO_5_BYTES = 1 << 1 +ROTATION_COMPRESSED_TO_8_BYTES = 1 << 2 +POSITION_UNCOMPRESS = 1 << 3 +POSITION_COMPRESSED_TO_6_BYTES = 1 << 4 +ROTATION_DELTACODED = 1 << 5 +POSITION_DELTACODED = 1 << 6 +ROTATION_COMPRESSED_NONLINEAR = 1 << 7 + +class BoneAnimTrack: + def __init__(self, data = b"", srcdata = None): + self.rawdata = data + self.srcdata = srcdata + self.rotations_data_offset = 0 + self.positions_data_offset = 0 + self.rotations = [] + self.positions = [] + self.rot_fullkeycount = 0 + self.pos_fullkeycount = 0 + self.rot_count = 0 + self.pos_count = 0 + self.bone_id = 0 + self.flags = 0 + self.padding = b"" + if len(data): + self.parseData() + def parseData(self): + self.data = Data(self.rawdata) + self.rotations_data_offset = self.data.decode("I")[0] + self.positions_data_offset = self.data.decode("I")[0] + self.rot_fullkeycount = self.data.decode("H")[0] + self.pos_fullkeycount = self.data.decode("H")[0] + self.rot_count = self.data.decode("H")[0] + self.pos_count = self.data.decode("H")[0] + self.bone_id = self.data.decode("B")[0] + self.flags = self.data.decode("B")[0] + self.padding = self.data.read(2) + def dump(self): + print("rotations_data_offset: %s" % (self.rotations_data_offset, )) + print("positions_data_offset: %s" % (self.positions_data_offset, )) + print("rot_fullkeycount: %s" % (self.rot_fullkeycount, )) + print("pos_fullkeycount: %s" % (self.pos_fullkeycount, )) + print("rot_count: %s" % (self.rot_count, )) + print("pos_count: %s" % (self.pos_count, )) + 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, )) + + +class Anim: + def __init__(self): + self.data = b"" + self.offset = 0 + self.version = -1 + + self.header_size = -1 + self.header_name = "" + self.header_base_anim_name = "" + self.header_max_hip_displacement = 0 + self.header_length = 0 + self.header_bone_tracks_offset = 0 + 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_backup_anim_track = 0 + self.header_load_state = 0 + self.header_last_time_used = 0 + self.header_file_age = 0 + self.header_spare_room = b"" + + self.bone_tracks = [] + self.skeleton_heirarchy = [] + + pass + def loadFromData(self, data): + self.data = Data(data) + self.offset = 0 + self.parseData() + pass + def loadFromFile(self, filein): + self.data = Data(filein.read()) + self.offset = 0 + self.parseData() + pass + def saveToData(self): + self.encode() + return self.data + def saveToFile(self, fileout): + self.encode() + fileout.write(self.data) + def parseData(self): + self.data.seek(0) + self.header_size = self.data.decode("i")[0] + self.header_name = extractString(self.data.read(256)) + self.header_base_anim_name = extractString(self.data.read(256)) + self.header_max_hip_displacement = self.data.decode("f")[0] + self.header_length = self.data.decode("f")[0] + self.header_bone_tracks_offset = self.data.decode("I")[0] + 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_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] + self.header_file_age = self.data.decode("f")[0] + self.header_spare_room = self.data.read(4 * 9) + + self.bone_tracks = [] + if self.header_bone_tracks_offset > 0: + 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)) + pass + def encode(self): + self.data = None + + def dump(self): + print("header_size: %s" % (self.header_size, )) + print("header_name: %s" % (self.header_name, )) + print("header_base_anim_name: %s" % (self.header_base_anim_name, )) + print("header_max_hip_displacement: %s" % (self.header_max_hip_displacement, )) + print("header_length: %s" % (self.header_length, )) + print("header_bone_tracks_offset: %s" % (self.header_bone_tracks_offset, )) + 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_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, )) + print("header_file_age: %s" % (self.header_file_age, )) + print("header_spare_room: %s" % (self.header_spare_room, )) + + print("Bone Tracks:") + for bt in self.bone_tracks: + bt.dump() + pass + +if __name__ == "__main__": + if len(sys.argv) <= 1 or len(sys.argv) > 3: + print("Usage:") + print(" %s []" % (sys.argv[0], )) + print("Test loads a .anim file, dumps its content, and optionally writes its content out.") + exit(0) + fh = open(sys.argv[1], "rb") + anim = Anim() + anim.loadFromFile(fh) + fh.close() + #print(sys.argv) + if len(sys.argv) <= 2: + fho = None + else: + fho = open(sys.argv[2], "wb") + if fho is not None: + data = anim.saveToData() + #anim.dump() + fho.write(data) + else: + anim.dump() + #print("%s" % [anim.header_data]) + diff --git a/util.py b/util.py index 7bb53d1..43240d1 100644 --- a/util.py +++ b/util.py @@ -1,4 +1,16 @@ import struct +import sys + +if sys.version_info[0] < 3: + byte = chr + unbyte = ord +else: + def byte(v): + #return struct.pack("B", v) + return bytes((v,)) + def unbyte(v): + return v + class Data: def __init__(self, rawdata = b"", off = 0): @@ -51,3 +63,17 @@ class Data: def __repr__(self): return repr(self.data) +def extractString(data, offset = 0): + """Extract an ASCIIZ string from a fixed length buffer.""" + for i in range(offset, len(data)): + if unbyte(data[i]) == 0: + return data[offset : i] + pass + return data + +def storeString(string, length): + """Store an ASCIIZ string into a fixed length buffer. If the string is longer than the buffer the output will be truncated, with \\x00 character guarenteed at the end.""" + if len(string) >= length: + return string[0 : length - 1] + ZERO_BYTE + else: + return string + ZERO_BYTE * (length - len(string))