io_anim_seanim/seanim.py

601 lines
19 KiB
Python

import time
import struct
try:
# Try to import the Python 3.x enum module
from enum import IntEnum
except:
# If we're on Python 2.x we need to define
# a dummy replacement
class IntEnum:
pass
# <pep8 compliant>
LOG_READ_TIME = False
LOG_WRITE_TIME = False
LOG_ANIM_HEADER = False
LOG_ANIM_BONES = False
LOG_ANIM_BONE_MODIFIERS = False
LOG_ANIM_BONES_KEYS = False
LOG_ANIM_NOTES = False
class SEANIM_TYPE(IntEnum):
SEANIM_TYPE_ABSOLUTE = 0
SEANIM_TYPE_ADDITIVE = 1
SEANIM_TYPE_RELATIVE = 2
SEANIM_TYPE_DELTA = 3
class SEANIM_PRESENCE_FLAGS(IntEnum):
# These describe what type of keyframe data is present for the bones
SEANIM_BONE_LOC = 1 << 0
SEANIM_BONE_ROT = 1 << 1
SEANIM_BONE_SCALE = 1 << 2
# If any of the above flags are set, then bone keyframe data is present,
# thus this comparing against this mask will return true
SEANIM_PRESENCE_BONE = 1 << 0 | 1 << 1 | 1 << 2
SEANIM_PRESENCE_NOTE = 1 << 6 # The file contains notetrack data
SEANIM_PRESENCE_CUSTOM = 1 << 7 # The file contains a custom data block
class SEANIM_PROPERTY_FLAGS(IntEnum):
SEANIM_PRECISION_HIGH = 1 << 0
class SEANIM_FLAGS(IntEnum):
SEANIM_LOOPED = 1 << 0
class Info(object):
__slots__ = ('version', 'magic')
def __init__(self, file=None):
self.version = 1
self.magic = b'SEAnim'
if file is not None:
self.load(file)
def load(self, file):
bytes = file.read(8)
data = struct.unpack('6ch', bytes)
magic = b''
for i in range(6):
magic += data[i]
version = data[6]
assert magic == self.magic
assert version == self.version
def save(self, file):
bytes = self.magic
bytes += struct.pack('h', self.version)
file.write(bytes)
class Header(object):
__slots__ = (
'animType', 'animFlags',
'dataPresenceFlags', 'dataPropertyFlags',
'framerate', 'frameCount',
'boneCount', 'boneAnimModifierCount',
'noteCount'
)
def __init__(self, file=None):
self.animType = SEANIM_TYPE.SEANIM_TYPE_RELATIVE # Relative is default
self.animFlags = 0x0
self.dataPresenceFlags = 0x0
self.dataPropertyFlags = 0x0
self.framerate = 0
self.frameCount = 0
self.boneCount = 0
self.boneAnimModifierCount = 0
self.noteCount = 0
if file is not None:
self.load(file)
def load(self, file):
bytes = file.read(2)
data = struct.unpack('h', bytes)
headerSize = data[0]
bytes = file.read(headerSize - 2)
# = prefix tell is to ignore C struct packing rules
data = struct.unpack('=6BfII4BI', bytes)
self.animType = data[0]
self.animFlags = data[1]
self.dataPresenceFlags = data[2]
self.dataPropertyFlags = data[3]
# reserved = data[4]
# reserved = data[5]
self.framerate = data[6]
self.frameCount = data[7]
self.boneCount = data[8]
self.boneAnimModifierCount = data[9]
# reserved = data[10]
# reserved = data[11]
# reserved = data[12]
self.noteCount = data[13]
def save(self, file):
bytes = struct.pack('=6BfII4BI',
self.animType, self.animFlags,
self.dataPresenceFlags, self.dataPropertyFlags,
0, 0,
self.framerate,
self.frameCount, self.boneCount,
self.boneAnimModifierCount, 0, 0, 0,
self.noteCount)
size = struct.pack('h', len(bytes) + 2)
file.write(size)
file.write(bytes)
class Frame_t(object):
"""
The Frame_t class is only ever used to get the size
and format character used by frame indices in a given seanim file
"""
__slots__ = ('size', 'char')
def __init__(self, header):
if header.frameCount <= 0xFF:
self.size = 1
self.char = 'B'
elif header.frameCount <= 0xFFFF:
self.size = 2
self.char = 'H'
else: # if header.frameCount <= 0xFFFFFFFF:
self.size = 4
self.char = 'I'
class Bone_t(object):
"""
The Bone_t class is only ever used to get the size
and format character used by frame indices in a given seanim file
"""
__slots__ = ('size', 'char')
def __init__(self, header):
if header.boneCount <= 0xFF:
self.size = 1
self.char = 'B'
elif header.boneCount <= 0xFFFF:
self.size = 2
self.char = 'H'
else: # if header.boneCount <= 0xFFFFFFFF:
self.size = 4
self.char = 'I'
class Precision_t(object):
"""
The Precision_t class is only ever used to get the size
and format character used by vec3_t, quat_t, etc. in a given sanim file
"""
__slots__ = ('size', 'char')
def __init__(self, header):
if (header.dataPropertyFlags &
SEANIM_PROPERTY_FLAGS.SEANIM_PRECISION_HIGH):
self.size = 8
self.char = 'd'
else:
self.size = 4
self.char = 'f'
class KeyFrame(object):
"""
A small class used for holding keyframe data
"""
__slots__ = ('frame', 'data')
def __init__(self, frame, data):
self.frame = frame
self.data = data
class Bone(object):
__slots__ = (
'name', 'flags',
'locKeyCount', 'rotKeyCount', 'scaleKeyCount',
'posKeys', 'rotKeys', 'scaleKeys',
'useModifier', 'modifier'
)
def __init__(self, file=None):
self.name = ""
self.flags = 0x0
self.locKeyCount = 0
self.rotKeyCount = 0
self.scaleKeyCount = 0
self.posKeys = []
self.rotKeys = []
self.scaleKeys = []
self.useModifier = False
self.modifier = 0
if file is not None:
self.load(file)
def load(self, file):
bytes = b''
b = file.read(1)
while not b == b'\x00':
bytes += b
b = file.read(1)
self.name = bytes.decode("utf-8")
def loadData(self, file, frame_t, precision_t,
useLoc=False, useRot=False, useScale=False):
# Read the flags for the bone
bytes = file.read(1)
data = struct.unpack("B", bytes)
self.flags = data[0]
# Load the position keyframes if they are present
if useLoc:
bytes = file.read(frame_t.size)
data = struct.unpack('%c' % frame_t.char, bytes)
self.locKeyCount = data[0]
for _ in range(self.locKeyCount):
bytes = file.read(frame_t.size + 3 * precision_t.size)
data = struct.unpack('=%c3%c' %
(frame_t.char, precision_t.char), bytes)
frame = data[0]
pos = (data[1], data[2], data[3])
self.posKeys.append(KeyFrame(frame, pos))
# Load the rotation keyframes if they are present
if useRot:
bytes = file.read(frame_t.size)
data = struct.unpack('%c' % frame_t.char, bytes)
self.rotKeyCount = data[0]
for _ in range(self.rotKeyCount):
bytes = file.read(frame_t.size + 4 * precision_t.size)
data = struct.unpack('=%c4%c' %
(frame_t.char, precision_t.char), bytes)
frame = data[0]
# Load the quaternion as XYZW
quat = (data[1], data[2], data[3], data[4])
self.rotKeys.append(KeyFrame(frame, quat))
# Load the Scale Keyrames
if useScale:
bytes = file.read(frame_t.size)
data = struct.unpack('%c' % frame_t.char, bytes)
self.scaleKeyCount = data[0]
for _ in range(self.scaleKeyCount):
bytes = file.read(frame_t.size + 3 * precision_t.size)
data = struct.unpack('=%c3%c' %
(frame_t.char, precision_t.char), bytes)
frame = data[0]
scale = (data[1], data[2], data[3])
self.scaleKeys.append(KeyFrame(frame, scale))
def save(self, file, frame_t, bone_t, precision_t,
useLoc=False, useRot=False, useScale=False):
bytes = struct.pack("B", self.flags)
file.write(bytes)
if useLoc:
bytes = struct.pack('%c' % frame_t.char, len(self.posKeys))
file.write(bytes)
for key in self.posKeys:
bytes = struct.pack('=%c3%c' %
(frame_t.char, precision_t.char),
key.frame,
key.data[0], key.data[1], key.data[2])
file.write(bytes)
if useRot:
bytes = struct.pack('%c' % frame_t.char, len(self.rotKeys))
file.write(bytes)
for key in self.rotKeys:
bytes = struct.pack('=%c4%c' %
(frame_t.char, precision_t.char),
key.frame,
key.data[0], key.data[1],
key.data[2], key.data[3])
file.write(bytes)
if useScale:
bytes = struct.pack('%c' % frame_t.char, len(self.scaleKeys))
file.write(bytes)
for key in self.scaleKeys:
bytes = struct.pack('=%c3%c' %
(frame_t.char, precision_t.char),
key.frame,
key.data[0], key.data[1], key.data[2])
file.write(bytes)
class Note(object):
__slots__ = ('frame', 'name')
def __init__(self, file=None, frame_t=None):
self.frame = -1
self.name = ""
if file is not None:
self.load(file, frame_t)
def load(self, file, frame_t):
bytes = file.read(frame_t.size)
data = struct.unpack('%c' % frame_t.char, bytes)
self.frame = data[0]
bytes = b''
b = file.read(1)
while not b == b'\x00':
bytes += b
b = file.read(1)
self.name = bytes.decode("utf-8")
def save(self, file, frame_t):
bytes = struct.pack('%c' % frame_t.char, self.frame)
file.write(bytes)
bytes = struct.pack('%ds' % (len(self.name) + 1), self.name.encode())
file.write(bytes)
class Anim(object):
__slots__ = ('__info', 'info', 'header', 'bones',
'boneAnimModifiers', 'notes')
def __init__(self, path=None):
self.__info = Info()
self.header = Header()
self.bones = []
self.boneAnimModifiers = []
self.notes = []
if path is not None:
self.load(path)
# Update the header flags based on the presence of certain keyframe /
# notetrack data
def update_metadata(self, high_precision=False, looping=False):
anim_locKeyCount = 0
anim_rotKeyCount = 0
anim_scaleKeyCount = 0
header = self.header
header.boneCount = len(self.bones)
dataPresenceFlags = header.dataPresenceFlags
dataPropertyFlags = header.dataPropertyFlags
max_frame_index = 0
for bone in self.bones:
bone.locKeyCount = len(bone.posKeys)
bone.rotKeyCount = len(bone.rotKeys)
bone.scaleKeyCount = len(bone.scaleKeys)
anim_locKeyCount += bone.locKeyCount
anim_rotKeyCount += bone.rotKeyCount
anim_scaleKeyCount += bone.scaleKeyCount
for key in bone.posKeys:
max_frame_index = max(max_frame_index, key.frame)
for key in bone.rotKeys:
max_frame_index = max(max_frame_index, key.frame)
for key in bone.scaleKeys:
max_frame_index = max(max_frame_index, key.frame)
if anim_locKeyCount:
dataPresenceFlags |= SEANIM_PRESENCE_FLAGS.SEANIM_BONE_LOC
if anim_rotKeyCount:
dataPresenceFlags |= SEANIM_PRESENCE_FLAGS.SEANIM_BONE_ROT
if anim_scaleKeyCount:
dataPresenceFlags |= SEANIM_PRESENCE_FLAGS.SEANIM_BONE_SCALE
for note in self.notes:
max_frame_index = max(max_frame_index, note.frame)
header.noteCount = len(self.notes)
if header.noteCount:
dataPresenceFlags |= SEANIM_PRESENCE_FLAGS.SEANIM_PRESENCE_NOTE
if high_precision:
dataPropertyFlags |= SEANIM_PROPERTY_FLAGS.SEANIM_PRECISION_HIGH
if looping:
header.animFlags |= SEANIM_FLAGS.SEANIM_LOOPED
header.dataPresenceFlags = dataPresenceFlags
header.dataPropertyFlags = dataPropertyFlags
# FrameCount represents the length of the animation in frames
# and since all animations start at frame 0 - we simply grab
# the max frame number (from keys / notes / etc.) and add 1 to it
header.frameCount = max_frame_index + 1
def load(self, path):
if LOG_READ_TIME:
time_start = time.time()
print("Loading: '%s'" % path)
try:
file = open(path, "rb")
except IOError:
print("Could not open file for reading:\n %s" % path)
return
self.info = Info(file)
self.header = Header(file)
self.boneAnimModifiers = []
# Init the frame_t, bone_t and precision_t info
frame_t = Frame_t(self.header)
bone_t = Bone_t(self.header)
precision_t = Precision_t(self.header)
dataPresenceFlags = self.header.dataPresenceFlags
if LOG_ANIM_HEADER:
print("Magic: %s" % self.info.magic)
print("Version: %d" % self.info.version)
print("AnimType: %d" % self.header.animType)
print("AnimFlags: %d" % self.header.animFlags)
print("PresenceFlags: %d" % dataPresenceFlags)
print("PropertyFlags: %d" % self.header.dataPropertyFlags)
print("FrameRate: %f" % self.header.framerate)
print("FrameCount: %d" % self.header.frameCount)
print("BoneCount: %d" % self.header.boneCount)
print("NoteCount: %d" % self.header.noteCount)
print("BoneModifierCount: %d" % self.header.boneAnimModifierCount)
print("Frame_t Size: %d" % frame_t.size)
print("Frame_t Char: '%s'" % frame_t.char)
self.bones = []
if dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_PRESENCE_BONE:
useLoc = dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_BONE_LOC
useRot = dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_BONE_ROT
useScale = (dataPresenceFlags &
SEANIM_PRESENCE_FLAGS.SEANIM_BONE_SCALE)
for i in range(self.header.boneCount):
if LOG_ANIM_BONES:
print("Loading Name for Bone[%d]" % i)
self.bones.append(Bone(file))
for i in range(self.header.boneAnimModifierCount):
bytes = file.read(bone_t.size + 1)
data = struct.unpack("%cB" % bone_t.char, bytes)
index = data[0]
self.bones[index].useModifier = True
self.bones[index].modifier = data[1]
self.boneAnimModifiers.append(self.bones[index])
if LOG_ANIM_BONE_MODIFIERS:
print("Loaded Modifier %d for '%s" %
(index, self.bones[index].name))
for i in range(self.header.boneCount):
if LOG_ANIM_BONES:
print("Loading Data For Bone[%d] '%s'" % (
i, self.bones[i].name))
self.bones[i].loadData(
file, frame_t, precision_t, useLoc, useRot, useScale)
if LOG_ANIM_BONES_KEYS:
for key in self.bones[i].posKeys:
print("%s LOC %d %s" %
(self.bones[i].name, key.frame, key.data))
for key in self.bones[i].rotKeys:
print("%s ROT %d %s" %
(self.bones[i].name, key.frame, key.data))
for key in self.bones[i].scaleKeys:
print("%s SCALE %d %s" %
(self.bones[i].name, key.frame, key.data))
self.notes = []
if (self.header.dataPresenceFlags &
SEANIM_PRESENCE_FLAGS.SEANIM_PRESENCE_NOTE):
for i in range(self.header.noteCount):
note = Note(file, frame_t)
self.notes.append(note)
if LOG_ANIM_NOTES:
print("Loaded Note[%d]:" % i)
print(" Frame %d: %s" % (note.frame, note.name))
file.close()
if LOG_READ_TIME:
time_end = time.time()
time_elapsed = time_end - time_start
print("Done! - Completed in %ss" % time_elapsed)
def save(self, filepath="", high_precision=False, looping=False):
if LOG_WRITE_TIME:
time_start = time.time()
print("Saving: '%s'" % filepath)
try:
file = open(filepath, "wb")
except IOError:
print("Could not open file for writing:\n %s" % filepath)
return
# Update the header flags, based on the presence of different keyframe
# types
self.update_metadata(high_precision, looping)
self.__info.save(file)
self.header.save(file)
for bone in self.bones:
bytes = struct.pack(
'%ds' % (len(bone.name) + 1), bone.name.encode())
file.write(bytes)
dataPresenceFlags = self.header.dataPresenceFlags
useLoc = dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_BONE_LOC
useRot = dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_BONE_ROT
useScale = dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_BONE_SCALE
frame_t = Frame_t(self.header)
bone_t = Bone_t(self.header)
precision_t = Precision_t(self.header)
for index, bone in enumerate(self.bones):
if bone.useModifier:
bytes = struct.pack('%cB' % bone_t.char, index, bone.modifier)
file.write(bytes)
for bone in self.bones:
bone.save(file, frame_t, bone_t, precision_t,
useLoc, useRot, useScale)
if dataPresenceFlags & SEANIM_PRESENCE_FLAGS.SEANIM_PRESENCE_NOTE:
for note in self.notes:
note.save(file, frame_t)
file.close()
if LOG_WRITE_TIME:
time_end = time.time()
time_elapsed = time_end - time_start
print("Done! - Completed in %ss" % time_elapsed)