Now able to parse bone track data and skeleton hierarchies from .anim files.

master
TigerKat 4 years ago
parent 3c469c0276
commit 545f6f203d

@ -5,15 +5,17 @@ try:
from .bones import * from .bones import *
from .util import Data from .util import Data
from .geomesh import GeoMesh, GeoFace, GeoVertex from .geomesh import GeoMesh, GeoFace, GeoVertex
from .compression_anim import *
except: except:
from bones import * from bones import *
from util import * from util import *
from geomesh import GeoMesh, GeoFace, GeoVertex from geomesh import GeoMesh, GeoFace, GeoVertex
from compression_anim import *
#.anim header #.anim header
#Offset Size Description #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 #4 256 (asciiz) name
#260 256 (asciiz) baseAnimName #260 256 (asciiz) baseAnimName
#516 4 (float32) max_hip_displacement #516 4 (float32) max_hip_displacement
@ -64,6 +66,43 @@ ROTATION_DELTACODED = 1 << 5
POSITION_DELTACODED = 1 << 6 POSITION_DELTACODED = 1 << 6
ROTATION_COMPRESSED_NONLINEAR = 1 << 7 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")
else:
self.child = data_or_child
self.next = next
self.boneid = 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 parseData(self, data):
self.root = data.decode("<i")[0]
self.bones = []
for i in range(70): #BONES_ON_DISK):
self.bones.append(BoneLink(data))
def dump(self):
print("SkeletonHierarchy: root: %s (%s)" % (self.root, BONES_LIST[self.root]))
for b in self.bones:
b.dump()
class BoneAnimTrack: class BoneAnimTrack:
def __init__(self, data = b"", srcdata = None): def __init__(self, data = b"", srcdata = None):
self.rawdata = data self.rawdata = data
@ -92,6 +131,55 @@ class BoneAnimTrack:
self.bone_id = self.data.decode("B")[0] self.bone_id = self.data.decode("B")[0]
self.flags = self.data.decode("B")[0] self.flags = self.data.decode("B")[0]
self.padding = self.data.read(2) 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 dump(self): def dump(self):
print("rotations_data_offset: %s" % (self.rotations_data_offset, )) print("rotations_data_offset: %s" % (self.rotations_data_offset, ))
print("positions_data_offset: %s" % (self.positions_data_offset, )) print("positions_data_offset: %s" % (self.positions_data_offset, ))
@ -99,9 +187,14 @@ class BoneAnimTrack:
print("pos_fullkeycount: %s" % (self.pos_fullkeycount, )) print("pos_fullkeycount: %s" % (self.pos_fullkeycount, ))
print("rot_count: %s" % (self.rot_count, )) print("rot_count: %s" % (self.rot_count, ))
print("pos_count: %s" % (self.pos_count, )) print("pos_count: %s" % (self.pos_count, ))
print("bone_id: %s %s" % (self.bone_id, BONES_LIST[self.bone_id])) 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("flags: 0x%2.2x" % (self.flags, ))
print("padding: %s" % (self.padding, )) print("padding: %s" % (self.padding, ))
print("rotations: %s" % (self.rotations, ))
print("positions: %s" % (self.positions, ))
class Anim: class Anim:
@ -169,6 +262,11 @@ class Anim:
self.data.seek(self.header_bone_tracks_offset) self.data.seek(self.header_bone_tracks_offset)
for i in range(self.header_bone_track_count): for i in range(self.header_bone_track_count):
self.bone_tracks.append(BoneAnimTrack(self.data.read(20), self.data)) self.bone_tracks.append(BoneAnimTrack(self.data.read(20), self.data))
self.skeleton_heirarchy = None
if self.header_skeleton_heirarchy_offset > 0:
self.data.seek(self.header_skeleton_heirarchy_offset)
self.skeleton_heirarchy = SkeletonHierarchy()
self.skeleton_heirarchy.parseData(self.data)
pass pass
def encode(self): def encode(self):
self.data = None self.data = None
@ -193,8 +291,11 @@ class Anim:
print("Bone Tracks:") print("Bone Tracks:")
for bt in self.bone_tracks: for bt in self.bone_tracks:
bt.dump() bt.dump()
if self.skeleton_heirarchy is not None:
self.skeleton_heirarchy.dump()
pass pass
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) <= 1 or len(sys.argv) > 3: if len(sys.argv) <= 1 or len(sys.argv) > 3:
print("Usage:") print("Usage:")

@ -114,6 +114,8 @@ BONES_LIST = [
"Leg_R_Jet2", "Leg_R_Jet2",
] ]
BONES_ON_DISK = 100
BONES_LEFT = [] BONES_LEFT = []
BONES_RIGHT = [] BONES_RIGHT = []
BONES_SWAP = [] 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.round(q[i] / MAX_5_BYTE_QUATERNION * 2048))
if v < -2048:
v = -2048
elif v > 2047:
v = 2047
d.append(q[i] * 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] + 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(floor(0.5 + quat[0] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(floor(0.5 + quat[1] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(floor(0.5 + quat[2] * SCALE_8_BYTE_QUATERNION_COMPRESS)),
int(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(floor(0.5 + vec[0] * SCALE_6_BYTE_VECTOR3_COMPRESS)),
int(floor(0.5 + vec[1] * SCALE_6_BYTE_VECTOR3_COMPRESS)),
int(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]
Loading…
Cancel
Save