Cleanup, reader and extraction code commented

This commit is contained in:
Will Snyder 2021-01-07 01:05:09 -05:00
parent e67e675ee7
commit e0a71bc899
2 changed files with 108 additions and 94 deletions

View File

@ -1,3 +1,11 @@
"""
Basically the same as msh reader but with a couple additional
methods for making TADA easier to navigate and treats the whole
file as an initial dummy chunk to avoid the oddities of SMNA and
to handle both zaa and zaabin.
"""
import io import io
import struct import struct

View File

@ -1,3 +1,11 @@
"""
Script for reading zaabin/zaa files and applying the unmunged animation
to the currently selected armature.
As regards decompress_curves, I should really make a separate AnimationSet
dataclass instead of returning a convoluted nested dict.
"""
import os import os
import bpy import bpy
import re import re
@ -19,54 +27,50 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]
decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {} decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {}
with ZAAReader(input_file) as head: with ZAAReader(input_file) as head:
# Dont read SMNA as child, since it has a length field always set to 0...
head.skip_until("SMNA") head.skip_until("SMNA")
head.skip_bytes(20) head.skip_bytes(20)
num_anims = head.read_u16() num_anims = head.read_u16()
print("\nFile contains {} animations\n".format(num_anims)) #print("\nFile contains {} animations\n".format(num_anims))
head.skip_bytes(2) head.skip_bytes(2)
anim_crcs = [] anim_crcs = []
anim_metadata = {} anim_metadata = {}
# Read metadata (crc, num frames, num bones) for each anim
with head.read_child() as mina: with head.read_child() as mina:
for i in range(num_anims): for i in range(num_anims):
mina.skip_bytes(8) mina.skip_bytes(8)
anim_hash = mina.read_u32() anim_crc = mina.read_u32()
anim_crcs += [anim_hash] anim_crcs.append(anim_crc)
anim_data = {}
anim_data["num_frames"] = mina.read_u16()
anim_data["num_bones"] = mina.read_u16()
anim_metadata[anim_hash] = anim_data
anim_metadata[anim_crc] = {"num_frames" : mina.read_u16(), "num_bones" : mina.read_u16()}
# Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim
with head.read_child() as tnja: with head.read_child() as tnja:
for i,anim_crc in enumerate(anim_crcs): for i, anim_crc in enumerate(anim_crcs):
bone_params = {} bone_params = {}
for _ in range(anim_metadata[anim_crc]["num_bones"]): for _ in range(anim_metadata[anim_crc]["num_bones"]):
bone_hash = tnja.read_u32() bone_crc = tnja.read_u32()
rot_offsets = [tnja.read_u32() for _ in range(4)] bone_params[bone_crc] = {
loc_offsets = [tnja.read_u32() for _ in range(3)] "rot_offsets" : [tnja.read_u32() for _ in range(4)], # Offsets into TADA for rotation
"loc_offsets" : [tnja.read_u32() for _ in range(3)], # and translation curves
qparams = [tnja.read_f32() for _ in range(4)] "qparams" : [tnja.read_f32() for _ in range(4)], # Translation quantization parameters, 3 biases, 1 multiplier
}
params = {"rot_offsets" : rot_offsets, "loc_offsets" : loc_offsets, "qparams" : qparams}
bone_params[bone_hash] = params
anim_metadata[anim_crc]["bone_params"] = bone_params anim_metadata[anim_crc]["bone_params"] = bone_params
# Decompress/dequantize frame data into discrete per-component curves
with head.read_child() as tada: with head.read_child() as tada:
for anim_crc in anim_crcs: for anim_crc in anim_crcs:
@ -78,107 +82,100 @@ def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]
#print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones)) #print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones))
#num_frames = 5 for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_params"]):
for bone_num, bone_hash in enumerate(anim_metadata[anim_crc]["bone_params"]): bone_curves = []
params_bone = anim_metadata[anim_crc]["bone_params"][bone_crc]
keyframes = []
params_bone = anim_metadata[anim_crc]["bone_params"][bone_hash]
offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"] offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"]
qparams = params_bone["qparams"] qparams = params_bone["qparams"]
#print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_hash))) #print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc)))
#print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams)) #print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams))
for o,start_offset in enumerate(offsets_list): for o, start_offset in enumerate(offsets_list):
# Skip to start of compressed data for component, as specified in TNJA
tada.skip_bytes(start_offset) tada.skip_bytes(start_offset)
curve = {} # Init curve dict
val = 0.0 curve : Dict[int,float] = {}
# Init accumulator
accumulator = 0.0
# 2047 = max val of signed 12 bit int, the (overwhelmingly) common compression amount.
# This is used for all rotation components in the file, with no offset
if o < 4: if o < 4:
mult = 1 / 2047 mult = 1 / 2047
offset = 0.0 bias = 0.0
# Translations have specific quantization parameters; biases for each component and
# a single multiplier for all three
else: else:
mult = qparams[-1] mult = qparams[-1]
offset = qparams[o - 4] bias = qparams[o - 4]
#print("\n\t\t\tBias = {}, multiplier = {}".format(offset, mult)) #print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult))
#print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos()))) #print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos())))
j = 0 j = 0
exit_loop = False while (j < num_frames):
while (j < num_frames and not exit_loop): accumulator = bias + mult * tada.read_i16()
val = offset + mult * tada.read_i16() curve[j if j < num_frames else num_frames] = accumulator
curve[j if j < num_frames else num_frames] = val
#print("\t\t\t\t{}: {}".format(j, val)) #print("\t\t\t\t{}: {}".format(j, accumulator))
j+=1 j+=1
if (j >= num_frames): while (j < num_frames):
break
while (True):
if (j >= num_frames):
exit_loop = True
break
control = tada.read_i8() control = tada.read_i8()
if control == 0x00: # Reset the accumulator to next dequantized i16
#curve[j if j < num_frames else num_frames] = val if control == -0x7f:
#print("\t\t\t\tControl: HOLDING FOR A FRAME")
#print("\t\t\t\t{}: {}".format(j, val))
j+=1
if (j >= num_frames):
break
elif control == -0x7f:
#print("\t\t\t\tControl: READING NEXT FRAME") #print("\t\t\t\tControl: READING NEXT FRAME")
break #get ready for new frame break
# RLE: hold current accumulator for the next u8 frames
elif control == -0x80: elif control == -0x80:
num_skips = tada.read_u8() num_skips = tada.read_u8()
#print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips)) #print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips))
j += num_skips
for _ in range(num_skips): # If not a special value, increment accumulator by the dequantized i8
j+=1 # The bias is NOT applied here, only for accumulator resets
if (j >= num_frames):
break
else: else:
val += mult * float(control) accumulator += mult * float(control)
curve[j if j < num_frames else num_frames] = val curve[j if j < num_frames else num_frames] = accumulator
#print("\t\t\t\t{}: {}".format(j, val)) #print("\t\t\t\t{}: {}".format(j, accumulator))
j+=1 j+=1
curve[num_frames - 1] = val curve[num_frames - 1] = accumulator
tada.reset_pos() tada.reset_pos()
keyframes.append(curve) bone_curves.append(curve)
decompressed_anims[anim_crc][bone_hash] = keyframes decompressed_anims[anim_crc][bone_crc] = bone_curves
return decompressed_anims return decompressed_anims
'''
Gets the animation names from the supplied
.anims file. Handy since .zaabin files often
share a dir with a .anims file.
'''
def read_anims_file(anims_file_path): def read_anims_file(anims_file_path):
if not os.path.exists(anims_file_path): if not os.path.exists(anims_file_path):
return None return []
anims_text = ""
with open(anims_file_path, 'r') as file: with open(anims_file_path, 'r') as file:
anims_text = file.read() anims_text = file.read()
@ -187,27 +184,27 @@ def read_anims_file(anims_file_path):
if len(splits) > 1: if len(splits) > 1:
return splits[1:-1:2] return splits[1:-1:2]
return None return []
'''
Unmunge the .zaa(bin) file and apply the resulting animation
to the currently selected armature object.
Contains some bloated code for calculating the world transforms of each bone,
for now this will work ONLY if the model was directly imported from a .msh file.
'''
def extract_and_apply_munged_anim(input_file_path): def extract_and_apply_munged_anim(input_file_path):
with open(input_file_path,"rb") as input_file: with open(input_file_path,"rb") as input_file:
discrete_curves = decompress_curves(input_file) animation_set = decompress_curves(input_file)
anim_names = None anim_names = []
if input_file_path.endswith(".zaabin"): if input_file_path.endswith(".zaabin"):
anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims")) anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims"))
arma = bpy.context.view_layer.objects.active arma = bpy.context.view_layer.objects.active
if arma.type != 'ARMATURE': if arma.type != 'ARMATURE':
raise Exception("Select an armature to attach the imported animation to!") raise Exception("Select an armature to attach the imported animation to!")
@ -217,6 +214,17 @@ def extract_and_apply_munged_anim(input_file_path):
arma.animation_data_create() arma.animation_data_create()
"""
When directly imported from .msh files,
all skeleton models are saved as emptys, since
some are excluded from the actual armature (effectors, roots, eg...).
bond_bind_poses contains matrices for converting the transform of
bones found in .msh/.zaabin files to ones that'll fit the extracted armature.
This will be replaced with the eventual importer release.
"""
bone_bind_poses = {} bone_bind_poses = {}
for bone in arma.data.bones: for bone in arma.data.bones:
@ -238,16 +246,13 @@ def extract_and_apply_munged_anim(input_file_path):
for anim_crc in discrete_curves: for anim_crc in animation_set:
anim_str = str(hex(anim_crc)) found_anim = [anim_name for anim_name in anim_names if crc(anim_name) == anim_crc]
if anim_names is not None: if found_anim:
for anim_name in anim_names: anim_str = found_anim[0]
if anim_crc == crc(anim_name): else:
anim_str = anim_name anim_str = str(hex(anim_crc))
#if crc(anim_name) not in discrete_curves:
# continue
#print("\nExtracting anim: " + anim_crc_str) #print("\nExtracting anim: " + anim_crc_str)
@ -258,21 +263,21 @@ def extract_and_apply_munged_anim(input_file_path):
action = bpy.data.actions.new(anim_str) action = bpy.data.actions.new(anim_str)
action.use_fake_user = True action.use_fake_user = True
anim_curves = discrete_curves[anim_crc] animation = animation_set[anim_crc]
for bone in arma.pose.bones: for bone in arma.pose.bones:
bone_crc = crc(bone.name) bone_crc = crc(bone.name)
#print("\tGetting curves for bone: " + bone.name) #print("\tGetting curves for bone: " + bone.name)
if bone_crc not in anim_curves: if bone_crc not in animation:
continue; continue;
bind_mat = bone_bind_poses[bone.name] bind_mat = bone_bind_poses[bone.name]
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name) loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name) rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
bone_curves = anim_curves[bone_crc] bone_curves = animation[bone_crc]
num_frames = max(bone_curves[0]) num_frames = max(bone_curves[0])
#print("\t\tNum frames: " + str(num_frames)) #print("\t\tNum frames: " + str(num_frames))
@ -284,7 +289,6 @@ def extract_and_apply_munged_anim(input_file_path):
q = Quaternion() q = Quaternion()
valmap = [1,2,3,0] valmap = [1,2,3,0]
#valmap = [0,1,2,3]
has_key = False has_key = False
@ -326,6 +330,7 @@ def extract_and_apply_munged_anim(input_file_path):
q = get_quat(frame) q = get_quat(frame)
if q is not None: if q is not None:
# Very bloated, but works for now
q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion() q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion()
fcurve_rot_w.keyframe_points.insert(frame,q.w) fcurve_rot_w.keyframe_points.insert(frame,q.w)
fcurve_rot_x.keyframe_points.insert(frame,q.x) fcurve_rot_x.keyframe_points.insert(frame,q.x)
@ -334,6 +339,7 @@ def extract_and_apply_munged_anim(input_file_path):
t = get_vec(frame) t = get_vec(frame)
if t is not None: if t is not None:
# ''
t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation
fcurve_loc_x.keyframe_points.insert(frame,t.x) fcurve_loc_x.keyframe_points.insert(frame,t.x)
fcurve_loc_y.keyframe_points.insert(frame,t.y) fcurve_loc_y.keyframe_points.insert(frame,t.y)