SWBF-msh-Blender-IO/addons/io_scene_swbf_msh/zaa_to_blend.py

434 lines
14 KiB
Python
Raw Permalink Normal View History

"""
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.
"""
2021-01-07 05:26:19 +00:00
import os
import bpy
import re
from .chunked_file_reader import Reader
2021-01-07 05:26:19 +00:00
from .crc import *
from .msh_model import *
from .msh_model_utilities import *
from .msh_utilities import *
from typing import List, Set, Dict, Tuple
debug = False
2021-01-07 05:26:19 +00:00
#anims #bones #components #keyframes: index,value
2021-01-07 05:26:19 +00:00
def decompress_curves(input_file) -> Dict[int, Dict[int, List[ Dict[int,float]]]]:
global debug
2021-01-07 05:26:19 +00:00
decompressed_anims: Dict[int, Dict[int, List[ Dict[int,float]]]] = {}
with Reader(input_file, debug=debug) as head:
# Dont read SMNA as child, since it has a length field always set to 0...
2021-01-07 05:26:19 +00:00
head.skip_until("SMNA")
head.skip_bytes(20)
num_anims = head.read_u16()
if debug:
print("\nFile contains {} animations\n".format(num_anims))
2021-01-07 05:26:19 +00:00
head.skip_bytes(2)
anim_crcs = []
anim_metadata = {}
head.skip_until("MINA")
# Read metadata (crc, num frames, num bones) for each anim
2021-01-07 05:26:19 +00:00
with head.read_child() as mina:
for i in range(num_anims):
transBitFlags = mina.read_u32()
mina.skip_bytes(4)
2021-01-07 05:26:19 +00:00
anim_crc = mina.read_u32()
anim_crcs.append(anim_crc)
2021-01-07 05:26:19 +00:00
anim_metadata[anim_crc] = {
"num_frames" : mina.read_u16(),
"num_bones" : mina.read_u16(),
"transBitFlags" : transBitFlags,
}
2021-01-07 05:26:19 +00:00
head.skip_until("TNJA")
# Read TADA offsets and quantization parameters for each rot + loc component, for each bone, for each anim
2021-01-07 05:26:19 +00:00
with head.read_child() as tnja:
for i, anim_crc in enumerate(anim_crcs):
2021-01-07 05:26:19 +00:00
bone_params = {}
bone_list = []
2021-01-07 05:26:19 +00:00
for _ in range(anim_metadata[anim_crc]["num_bones"]):
bone_crc = tnja.read_u32()
bone_list.append(bone_crc)
2021-01-07 05:26:19 +00:00
bone_params[bone_crc] = {
"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)], # Translation quantization parameters, 3 biases, 1 multiplier
}
2021-01-07 05:26:19 +00:00
anim_metadata[anim_crc]["bone_params"] = bone_params
anim_metadata[anim_crc]["bone_list"] = bone_list
2021-01-07 05:26:19 +00:00
head.skip_until("TADA")
# Decompress/dequantize frame data into discrete per-component curves
2021-01-07 05:26:19 +00:00
with head.read_child() as tada:
for anim_crc in anim_crcs:
decompressed_anims[anim_crc] = {}
num_frames = anim_metadata[anim_crc]["num_frames"]
num_bones = anim_metadata[anim_crc]["num_bones"]
transBitFlags = anim_metadata[anim_crc]["transBitFlags"]
if debug:
print("\n\tAnim hash: {} Num frames: {} Num joints: {}".format(hex(anim_crc), num_frames, num_bones))
2021-01-07 05:26:19 +00:00
for bone_num, bone_crc in enumerate(anim_metadata[anim_crc]["bone_list"]):
2021-01-07 05:26:19 +00:00
bone_curves = []
2021-01-07 05:26:19 +00:00
params_bone = anim_metadata[anim_crc]["bone_params"][bone_crc]
2021-01-07 05:26:19 +00:00
offsets_list = params_bone["rot_offsets"] + params_bone["loc_offsets"]
qparams = params_bone["qparams"]
if debug:
print("\n\t\tBone #{} hash: {}".format(bone_num,hex(bone_crc)))
print("\n\t\tQParams: {}, {}, {}, {}".format(*qparams))
2021-01-07 05:26:19 +00:00
for o, start_offset in enumerate(offsets_list):
# Init curve dict
curve : Dict[int,float] = {}
# Init accumulator
accumulator = 0.0
2021-01-07 05:26:19 +00:00
# 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
2021-01-07 05:26:19 +00:00
if o < 4:
mult = 1 / 2047
bias = 0.0
# Translations have specific quantization parameters; biases for each component and
# a single multiplier for all three
2021-01-07 05:26:19 +00:00
else:
2021-01-07 05:26:19 +00:00
mult = qparams[-1]
bias = qparams[o - 4]
2021-01-07 05:26:19 +00:00
if debug:
print("\n\t\t\tBias = {}, multiplier = {}".format(bias, mult))
2021-01-07 05:26:19 +00:00
if debug:
print("\n\t\t\tOffset {}: {} ({}, {} remaining)".format(o,start_offset, tada.get_current_pos(), tada.how_much_left(tada.get_current_pos())))
2021-01-07 05:26:19 +00:00
# Skip to start of compressed data for component, as specified in TNJA
tada.skip_bytes(start_offset)
2021-01-07 05:26:19 +00:00
j = 0
while (j < num_frames):
accumulator = bias + mult * tada.read_i16()
curve[j if j < num_frames else num_frames] = accumulator
2021-01-07 05:26:19 +00:00
if debug:
print("\t\t\t\t{}: {}".format(j, accumulator))
2021-01-07 05:26:19 +00:00
j+=1
while (j < num_frames):
2021-01-07 05:26:19 +00:00
control = tada.read_i8()
# Reset the accumulator to next dequantized i16
if control == -0x7f:
if debug:
print("\t\t\t\tControl: READING NEXT FRAME")
break
2021-01-07 05:26:19 +00:00
# RLE: hold current accumulator for the next u8 frames
2021-01-07 05:26:19 +00:00
elif control == -0x80:
num_skips = tada.read_u8()
if debug:
print("\t\t\t\tControl: HOLDING FOR {} FRAMES".format(num_skips))
j += num_skips
2021-01-07 05:26:19 +00:00
# If not a special value, increment accumulator by the dequantized i8
# The bias is NOT applied here, only for accumulator resets
2021-01-07 05:26:19 +00:00
else:
accumulator += mult * float(control)
curve[j if j < num_frames else num_frames] = accumulator
if debug:
print("\t\t\t\t{}: {}".format(j, accumulator))
2021-01-07 05:26:19 +00:00
j+=1
curve[num_frames - 1] = accumulator
2021-01-07 05:26:19 +00:00
tada.reset_pos()
bone_curves.append(curve)
2021-01-07 05:26:19 +00:00
decompressed_anims[anim_crc][bone_crc] = bone_curves
2021-01-07 05:26:19 +00:00
return decompressed_anims
'''
Gets the animation names from the supplied
.anims file. Handy since .zaabin files often
share a dir with a .anims file.
'''
2021-01-07 05:26:19 +00:00
def read_anims_file(anims_file_path):
if not os.path.exists(anims_file_path):
return []
2021-01-07 05:26:19 +00:00
with open(anims_file_path, 'r') as file:
anims_text = file.read()
splits = anims_text.split('"')
if len(splits) > 1:
return splits[1:-1:2]
return []
2021-01-07 05:26:19 +00:00
'''
Unmunge the .zaa(bin) file and apply the resulting animation
to the currently selected armature object.
2021-01-07 05:26:19 +00:00
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.
'''
2021-01-07 05:26:19 +00:00
def extract_and_apply_munged_anim(input_file_path):
global debug
2021-01-07 05:26:19 +00:00
with open(input_file_path,"rb") as input_file:
animation_set = decompress_curves(input_file)
2021-01-07 05:26:19 +00:00
anim_names = []
2021-01-07 05:26:19 +00:00
if input_file_path.endswith(".zaabin"):
anim_names = read_anims_file(input_file_path.replace(".zaabin", ".anims"))
arma = bpy.context.view_layer.objects.active
if arma.type != 'ARMATURE':
raise Exception("Select an armature to attach the imported animation to!")
if arma.animation_data is not None:
arma.animation_data_clear()
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.
"""
animated_bones = set()
for anim_crc in animation_set:
for bone_crc in animation_set[anim_crc]:
animated_bones.add(bone_crc)
bpy.context.view_layer.objects.active = arma
bpy.ops.object.mode_set(mode='EDIT')
2021-01-07 05:26:19 +00:00
bone_bind_poses = {}
for edit_bone in arma.data.edit_bones:
if to_crc(edit_bone.name) not in animated_bones:
continue
2021-01-07 05:26:19 +00:00
curr_ancestor = edit_bone.parent
while curr_ancestor is not None and to_crc(curr_ancestor.name) not in animated_bones:
curr_ancestor = curr_ancestor.parent
2021-01-07 05:26:19 +00:00
if curr_ancestor:
bind_mat = curr_ancestor.matrix.inverted() @ edit_bone.matrix
else:
bind_mat = arma.matrix_local @ edit_bone.matrix
2021-01-07 05:26:19 +00:00
bone_bind_poses[edit_bone.name] = bind_mat.inverted()
2021-01-07 05:26:19 +00:00
bpy.ops.object.mode_set(mode='OBJECT')
2021-01-07 05:26:19 +00:00
if debug:
print("Extracting {} animations from {}:".format(len(animation_set), input_file_path))
for anim_crc in animation_set:
2021-01-07 05:26:19 +00:00
2021-01-07 06:25:09 +00:00
found_anim = [anim_name for anim_name in anim_names if to_crc(anim_name) == anim_crc]
if found_anim:
anim_str = found_anim[0]
else:
anim_str = str(hex(anim_crc))
2021-01-07 05:26:19 +00:00
if debug:
print("\tExtracting anim {}:".format(anim_str))
#if anim_str in bpy.data.actions:
# bpy.data.actions[anim_str].use_fake_user = False
# bpy.data.actions.remove(bpy.data.actions[anim_str])
2021-01-07 05:26:19 +00:00
action = bpy.data.actions.new(anim_str)
action.use_fake_user = True
animation = animation_set[anim_crc]
2021-01-07 05:26:19 +00:00
bone_crcs_list = [bone_crc_ for bone_crc_ in animation]
for bone_crc in sorted(bone_crcs_list):
bone_name = next((bone.name for bone in arma.pose.bones if to_crc(bone.name) == bone_crc), None)
if bone_name is None:
continue
bone = arma.pose.bones[bone_name]
2021-01-07 06:25:09 +00:00
bone_crc = to_crc(bone.name)
2021-01-07 05:26:19 +00:00
if bone_crc not in animation:
2021-01-07 05:26:19 +00:00
continue;
bind_mat = bone_bind_poses[bone.name]
loc_data_path = "pose.bones[\"{}\"].location".format(bone.name)
rot_data_path = "pose.bones[\"{}\"].rotation_quaternion".format(bone.name)
bone_curves = animation[bone_crc]
2021-01-07 05:26:19 +00:00
num_frames = max(bone_curves[0])
has_translation = bone_curves[4] is not None
if debug:
print("\t\tBone {} has {} frames: ".format(bone_name, num_frames))
2021-01-07 05:26:19 +00:00
last_values = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
def get_quat(index):
nonlocal bone_curves, last_values
q = Quaternion()
valmap = [1,2,3,0]
has_key = False
for i in range(4):
curve = bone_curves[i]
if index in curve:
has_key = True
last_values[i] = curve[index]
q[valmap[i]] = last_values[i]
return q if has_key else None
def get_vec(index):
nonlocal bone_curves, last_values
v = Vector()
has_key = False
for i in range(4,7):
curve = bone_curves[i]
if index in curve:
has_key = True
last_values[i] = curve[index]
v[i - 4] = last_values[i]
return v if has_key else None
fcurve_rot_w = action.fcurves.new(rot_data_path, index=0, action_group=bone.name)
fcurve_rot_x = action.fcurves.new(rot_data_path, index=1, action_group=bone.name)
fcurve_rot_y = action.fcurves.new(rot_data_path, index=2, action_group=bone.name)
fcurve_rot_z = action.fcurves.new(rot_data_path, index=3, action_group=bone.name)
if has_translation:
fcurve_loc_x = action.fcurves.new(loc_data_path, index=0, action_group=bone.name)
fcurve_loc_y = action.fcurves.new(loc_data_path, index=1, action_group=bone.name)
fcurve_loc_z = action.fcurves.new(loc_data_path, index=2, action_group=bone.name)
2021-01-07 05:26:19 +00:00
for frame in range(num_frames):
q = get_quat(frame)
if q is not None:
if debug:
print("\t\t\tRot key: ({}, {})".format(frame, quat_to_str(q)))
# Very bloated, but works for now
2021-01-07 05:26:19 +00:00
q = (bind_mat @ convert_rotation_space(q).to_matrix().to_4x4()).to_quaternion()
fcurve_rot_w.keyframe_points.insert(frame,q.w)
fcurve_rot_x.keyframe_points.insert(frame,q.x)
fcurve_rot_y.keyframe_points.insert(frame,q.y)
fcurve_rot_z.keyframe_points.insert(frame,q.z)
if has_translation:
t = get_vec(frame)
if t is not None:
if debug:
print("\t\t\tPos key: ({}, {})".format(frame, vec_to_str(t)))
t = (bind_mat @ Matrix.Translation(convert_vector_space(t))).translation
fcurve_loc_x.keyframe_points.insert(frame,t.x)
fcurve_loc_y.keyframe_points.insert(frame,t.y)
fcurve_loc_z.keyframe_points.insert(frame,t.z)
2021-01-07 05:26:19 +00:00
arma.animation_data.action = action