mirror of https://github.com/AxioDL/metaforce.git
210 lines
8.5 KiB
Python
210 lines
8.5 KiB
Python
'''
|
|
This file provides a means to encode animation key-channels
|
|
in an interleaved, sparse array for use by the runtime
|
|
'''
|
|
|
|
import re
|
|
import hashlib
|
|
import struct
|
|
import mathutils
|
|
|
|
# Regex RNA path matchers
|
|
scale_matcher = re.compile(r'pose.bones\["(\S+)"\].scale')
|
|
rotation_matcher = re.compile(r'pose.bones\["(\S+)"\].rotation')
|
|
location_matcher = re.compile(r'pose.bones\["(\S+)"\].location')
|
|
|
|
# Effect transform modes
|
|
EFFECT_XF_MODES = {'STATIONARY':0, 'WORLD':1, 'LOCAL':2}
|
|
|
|
# Generate animation info
|
|
def generate_animation_info(action, res_db, rani_db_id, arg_package, endian_char='<'):
|
|
|
|
# Set of frame indices
|
|
frame_set = set()
|
|
|
|
# Set of unique bone names
|
|
bone_set = set()
|
|
|
|
# Scan through all fcurves to build animated bone set
|
|
for fcurve in action.fcurves:
|
|
data_path = fcurve.data_path
|
|
scale_match = scale_matcher.match(data_path)
|
|
rotation_match = rotation_matcher.match(data_path)
|
|
location_match = location_matcher.match(data_path)
|
|
|
|
if scale_match:
|
|
bone_set.add(scale_match.group(1))
|
|
elif rotation_match:
|
|
bone_set.add(rotation_match.group(1))
|
|
elif location_match:
|
|
bone_set.add(location_match.group(1))
|
|
else:
|
|
continue
|
|
|
|
# Count unified keyframes for interleaving channel data
|
|
for key in fcurve.keyframe_points:
|
|
frame_set.add(int(key.co[0]))
|
|
|
|
# Relate fcurves per-frame / per-bone and assemble data
|
|
key_stream = bytearray()
|
|
key_stream += struct.pack(endian_char + 'II', len(frame_set), len(bone_set))
|
|
duration = action.frame_range[1] / action.hecl_fps
|
|
interval = 1.0 / action.hecl_fps
|
|
key_stream += struct.pack(endian_char + 'ff', duration, interval)
|
|
|
|
# Generate keyframe bitmap
|
|
fr = int(round(action.frame_range[1]))
|
|
key_stream += struct.pack(endian_char + 'I', fr)
|
|
bitmap_words = [0] * (fr // 32)
|
|
if fr % 32:
|
|
bitmap_words.append(0)
|
|
for i in range(fr):
|
|
if i in frame_set:
|
|
bitmap_words[i//32] |= 1 << i%32
|
|
for word in bitmap_words:
|
|
key_stream += struct.pack(endian_char + 'I', word)
|
|
|
|
|
|
# Build bone table
|
|
bone_list = []
|
|
for bone in bone_set:
|
|
fc_dict = dict()
|
|
rotation_mode = None
|
|
property_bits = 0
|
|
for fcurve in action.fcurves:
|
|
if fcurve.data_path == 'pose.bones["'+bone+'"].scale':
|
|
if 'scale' not in fc_dict:
|
|
fc_dict['scale'] = [None, None, None]
|
|
property_bits |= 1
|
|
fc_dict['scale'][fcurve.array_index] = fcurve
|
|
elif fcurve.data_path == 'pose.bones["'+bone+'"].rotation_euler':
|
|
if 'rotation_euler' not in fc_dict:
|
|
fc_dict['rotation_euler'] = [None, None, None]
|
|
rotation_mode = 'rotation_euler'
|
|
property_bits |= 2
|
|
fc_dict['rotation_euler'][fcurve.array_index] = fcurve
|
|
elif fcurve.data_path == 'pose.bones["'+bone+'"].rotation_quaternion':
|
|
if 'rotation_quaternion' not in fc_dict:
|
|
fc_dict['rotation_quaternion'] = [None, None, None, None]
|
|
rotation_mode = 'rotation_quaternion'
|
|
property_bits |= 2
|
|
fc_dict['rotation_quaternion'][fcurve.array_index] = fcurve
|
|
elif fcurve.data_path == 'pose.bones["'+bone+'"].rotation_axis_angle':
|
|
if 'rotation_axis_angle' not in fc_dict:
|
|
fc_dict['rotation_axis_angle'] = [None, None, None, None]
|
|
rotation_mode = 'rotation_axis_angle'
|
|
property_bits |= 2
|
|
fc_dict['rotation_axis_angle'][fcurve.array_index] = fcurve
|
|
elif fcurve.data_path == 'pose.bones["'+bone+'"].location':
|
|
if 'location' not in fc_dict:
|
|
fc_dict['location'] = [None, None, None]
|
|
property_bits |= 4
|
|
fc_dict['location'][fcurve.array_index] = fcurve
|
|
bone_list.append((bone, rotation_mode, fc_dict))
|
|
bone_head = hashbone(bone)
|
|
bone_head |= (property_bits << 28)
|
|
key_stream += struct.pack(endian_char + 'I', bone_head)
|
|
|
|
# Interleave / interpolate keyframe data
|
|
for frame in sorted(frame_set):
|
|
for bone in bone_list:
|
|
|
|
bone_name = bone[0]
|
|
rotation_mode = bone[1]
|
|
fc_dict = bone[2]
|
|
|
|
# Scale curves
|
|
if 'scale' in fc_dict:
|
|
for comp in range(3):
|
|
if fc_dict['scale'][comp]:
|
|
key_stream += struct.pack(endian_char + 'f', fc_dict['scale'][comp].evaluate(frame))
|
|
else:
|
|
key_stream += struct.pack(endian_char + 'f', 0.0)
|
|
|
|
# Rotation curves
|
|
if rotation_mode == 'rotation_quaternion':
|
|
for comp in range(4):
|
|
if fc_dict['rotation_quaternion'][comp]:
|
|
key_stream += struct.pack(endian_char + 'f', fc_dict['rotation_quaternion'][comp].evaluate(frame))
|
|
else:
|
|
key_stream += struct.pack(endian_char + 'f', 0.0)
|
|
|
|
elif rotation_mode == 'rotation_euler':
|
|
euler = [0.0, 0.0, 0.0]
|
|
for comp in range(3):
|
|
if fc_dict['rotation_euler'][comp]:
|
|
euler[comp] = fc_dict['rotation_euler'][comp].evaluate(frame)
|
|
euler_o = mathutils.Euler(euler, 'XYZ')
|
|
quat = euler_o.to_quaternion()
|
|
key_stream += struct.pack(endian_char + 'ffff', quat[0], quat[1], quat[2], quat[3])
|
|
|
|
elif rotation_mode == 'rotation_axis_angle':
|
|
axis_angle = [0.0, 0.0, 0.0, 0.0]
|
|
for comp in range(4):
|
|
if fc_dict['rotation_axis_angle'][comp]:
|
|
axis_angle[comp] = fc_dict['rotation_axis_angle'][comp].evaluate(frame)
|
|
quat = mathutils.Quaternion(axis_angle[1:4], axis_angle[0])
|
|
key_stream += struct.pack(endian_char + 'ffff', quat[0], quat[1], quat[2], quat[3])
|
|
|
|
# Location curves
|
|
if 'location' in fc_dict:
|
|
for comp in range(3):
|
|
if fc_dict['location'][comp]:
|
|
key_stream += struct.pack(endian_char + 'f', fc_dict['location'][comp].evaluate(frame))
|
|
else:
|
|
key_stream += struct.pack(endian_char + 'f', 0.0)
|
|
|
|
|
|
# Generate event buffer
|
|
event_buf = bytearray()
|
|
if hasattr(action, 'hecl_events'):
|
|
c1 = 0
|
|
c2 = 0
|
|
c3 = 0
|
|
c4 = 0
|
|
for event in action.hecl_events:
|
|
if event.type == 'LOOP':
|
|
c1 += 1
|
|
elif event.type == 'UEVT':
|
|
c2 += 1
|
|
elif event.type == 'EFFECT':
|
|
c3 += 1
|
|
elif event.type == 'SOUND':
|
|
c4 += 1
|
|
event_buf += struct.pack(endian_char + 'IIII', c1, c2, c3, c4)
|
|
|
|
for event in action.hecl_events:
|
|
if event.type == 'LOOP':
|
|
event_buf += struct.pack(endian_char + 'fi', event.time, event.loop_data.bool)
|
|
|
|
for event in action.hecl_events:
|
|
if event.type == 'UEVT':
|
|
event_buf += struct.pack(endian_char + 'fii', event.time, event.uevt_data.type,
|
|
hashbone(event.uevt_data.bone_name))
|
|
|
|
for event in action.hecl_events:
|
|
if event.type == 'EFFECT':
|
|
effect_db_id, effect_hash = res_db.search_for_resource(event.effect_data.uid, arg_package)
|
|
if effect_hash:
|
|
res_db.register_dependency(rani_db_id, effect_db_id)
|
|
else:
|
|
raise RuntimeError("Error - unable to find effect '{0}'".format(event.effect_data.uid))
|
|
event_buf += struct.pack(endian_char + 'fiifi', event.time, event.effect_data.frame_count,
|
|
hashbone(event.effect_data.bone_name), event.effect_data.scale,
|
|
EFFECT_XF_MODES[event.effect_data.transform_mode])
|
|
event_buf += effect_hash
|
|
|
|
for event in action.hecl_events:
|
|
if event.type == 'SOUND':
|
|
sid = int.from_bytes(event.sound_data.sound_id.encode()[:4], byteorder='big', signed=False)
|
|
event_buf += struct.pack(endian_char + 'fIff', event.time, sid,
|
|
event.sound_data.ref_amp, event.sound_data.ref_dist)
|
|
|
|
else:
|
|
event_buf += struct.pack('IIII',0,0,0,0)
|
|
|
|
|
|
|
|
return key_stream + event_buf
|
|
|