Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
980042fc7f | ||
|
|
02082b9594 | ||
|
|
4181a15d0e | ||
|
|
b6ef3dda44 | ||
|
|
f7290e6808 | ||
|
|
65d3104ea9 | ||
|
|
1a48128cb9 |
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "PSK/PSA Importer/Exporter",
|
||||
"author": "Colin Basnett, Yurii Ti",
|
||||
"version": (4, 0, 1),
|
||||
"version": (4, 1, 0),
|
||||
"blender": (2, 80, 0),
|
||||
# "location": "File > Export > PSK Export (.psk)",
|
||||
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
||||
|
||||
@@ -98,7 +98,7 @@ def check_bone_names(bone_names: Iterable[str]):
|
||||
invalid_bone_names = [x for x in bone_names if pattern.match(x) is None]
|
||||
if len(invalid_bone_names) > 0:
|
||||
raise RuntimeError(f'The following bone names are invalid: {invalid_bone_names}.\n'
|
||||
f'Bone names must only contain letters, numbers, spaces, and underscores.')
|
||||
f'Bone names must only contain letters, numbers, spaces, hyphens and underscores.')
|
||||
|
||||
|
||||
def get_export_bone_names(armature_object, bone_filter_mode, bone_group_indices: List[int]) -> List[str]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict
|
||||
|
||||
from bpy.types import Action
|
||||
from bpy.types import Action, Armature, Bone
|
||||
|
||||
from .data import *
|
||||
from ..helpers import *
|
||||
@@ -19,6 +19,7 @@ class PsaBuildOptions(object):
|
||||
self.bone_group_indices = []
|
||||
self.should_use_original_sequence_names = False
|
||||
self.should_trim_timeline_marker_sequences = True
|
||||
self.should_ignore_bone_name_restrictions = False
|
||||
self.sequence_name_prefix = ''
|
||||
self.sequence_name_suffix = ''
|
||||
self.root_motion = False
|
||||
@@ -87,7 +88,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data, context, options:
|
||||
return sequence_frame_ranges
|
||||
|
||||
|
||||
def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
||||
active_object = context.view_layer.objects.active
|
||||
|
||||
if active_object.type != 'ARMATURE':
|
||||
@@ -112,7 +113,8 @@ def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
psa = Psa()
|
||||
|
||||
armature = active_object
|
||||
bones = list(armature.data.bones)
|
||||
armature_data = typing.cast(Armature, armature)
|
||||
bones: List[Bone] = list(iter(armature_data.bones))
|
||||
|
||||
# The order of the armature bones and the pose bones is not guaranteed to be the same.
|
||||
# As a result, we need to reconstruct the list of pose bones in the same order as the
|
||||
@@ -135,7 +137,8 @@ def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
raise RuntimeError('No bones available for export')
|
||||
|
||||
# Check that all bone names are valid.
|
||||
check_bone_names(map(lambda bone: bone.name, bones))
|
||||
if not options.should_ignore_bone_name_restrictions:
|
||||
check_bone_names(map(lambda bone: bone.name, bones))
|
||||
|
||||
# Build list of PSA bones.
|
||||
for bone in bones:
|
||||
@@ -208,6 +211,7 @@ def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
export_sequence.nla_state.action = None
|
||||
export_sequence.nla_state.frame_min = frame_min
|
||||
export_sequence.nla_state.frame_max = frame_max
|
||||
|
||||
nla_strips_actions = set(
|
||||
map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, frame_min, frame_max)))
|
||||
export_sequence.fps = get_sequence_fps(context, options, nla_strips_actions)
|
||||
@@ -220,6 +224,10 @@ def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'
|
||||
export_sequence.name = export_sequence.name.strip()
|
||||
|
||||
# Save the current action and frame so that we can restore the state once we are done.
|
||||
saved_frame_current = context.scene.frame_current
|
||||
saved_action = animation_data.action
|
||||
|
||||
# Now build the PSA sequences.
|
||||
# We actually alter the timeline frame and simply record the resultant pose bone matrices.
|
||||
frame_start_index = 0
|
||||
@@ -280,4 +288,8 @@ def build_psa(context, options: PsaBuildOptions) -> Psa:
|
||||
|
||||
psa.sequences[export_sequence.name] = psa_sequence
|
||||
|
||||
# Restore the previous action & frame.
|
||||
animation_data.action = saved_action
|
||||
context.scene.frame_set(saved_frame_current)
|
||||
|
||||
return psa
|
||||
|
||||
@@ -6,7 +6,7 @@ from ..data import *
|
||||
|
||||
"""
|
||||
Note that keys are not stored within the Psa object.
|
||||
Use the PsaReader::get_sequence_keys to get a the keys for a sequence.
|
||||
Use the PsaReader::get_sequence_keys to get the keys for a sequence.
|
||||
"""
|
||||
|
||||
|
||||
@@ -60,5 +60,5 @@ class Psa(object):
|
||||
|
||||
def __init__(self):
|
||||
self.bones: List[Psa.Bone] = []
|
||||
self.sequences: typing.OrderedDict[Psa.Sequence] = OrderedDict()
|
||||
self.sequences: typing.OrderedDict[str, Psa.Sequence] = OrderedDict()
|
||||
self.keys: List[Psa.Key] = []
|
||||
|
||||
@@ -136,6 +136,12 @@ class PsaExportPropertyGroup(PropertyGroup):
|
||||
description='Frames without NLA track information at the boundaries of timeline markers will be excluded from '
|
||||
'the exported sequences '
|
||||
)
|
||||
should_ignore_bone_name_restrictions: BoolProperty(
|
||||
default=False,
|
||||
name='Ignore Bone Name Restrictions',
|
||||
description='Bone names restrictions will be ignored. Note that bone names without properly formatted names '
|
||||
'cannot be referenced in scripts.'
|
||||
)
|
||||
sequence_name_prefix: StringProperty(name='Prefix', options=empty_set)
|
||||
sequence_name_suffix: StringProperty(name='Suffix', options=empty_set)
|
||||
sequence_filter_name: StringProperty(
|
||||
@@ -262,6 +268,8 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
layout.template_list('PSX_UL_BoneGroupList', '', pg, 'bone_group_list', pg, 'bone_group_list_index',
|
||||
rows=rows)
|
||||
|
||||
layout.prop(pg, 'should_ignore_bone_name_restrictions')
|
||||
|
||||
layout.separator()
|
||||
|
||||
# ROOT MOTION
|
||||
@@ -346,6 +354,7 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
|
||||
options.should_use_original_sequence_names = pg.should_use_original_sequence_names
|
||||
options.should_trim_timeline_marker_sequences = pg.should_trim_timeline_marker_sequences
|
||||
options.should_ignore_bone_name_restrictions = pg.should_ignore_bone_name_restrictions
|
||||
options.sequence_name_prefix = pg.sequence_name_prefix
|
||||
options.sequence_name_suffix = pg.sequence_name_suffix
|
||||
options.root_motion = pg.root_motion
|
||||
|
||||
@@ -4,7 +4,6 @@ import re
|
||||
from typing import List, Optional
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
|
||||
from bpy.types import Operator, UIList, PropertyGroup, Panel
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
@@ -16,7 +15,6 @@ from .reader import PsaReader
|
||||
|
||||
class PsaImportOptions(object):
|
||||
def __init__(self):
|
||||
self.should_clean_keys = True
|
||||
self.should_use_fake_user = False
|
||||
self.should_stash = False
|
||||
self.sequence_names = []
|
||||
@@ -24,6 +22,7 @@ class PsaImportOptions(object):
|
||||
self.should_write_keyframes = True
|
||||
self.should_write_metadata = True
|
||||
self.action_name_prefix = ''
|
||||
self.should_convert_to_samples = False
|
||||
|
||||
|
||||
class ImportBone(object):
|
||||
@@ -154,7 +153,6 @@ def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions
|
||||
|
||||
# Read the sequence data matrix from the PSA.
|
||||
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
|
||||
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
|
||||
|
||||
# Convert the sequence's data from world-space to local-space.
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
@@ -166,39 +164,18 @@ def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions
|
||||
# Calculate the local-space key data for the bone.
|
||||
sequence_data_matrix[frame_index, bone_index] = calculate_fcurve_data(import_bone, key_data)
|
||||
|
||||
# Clean the keyframe data. This is accomplished by writing zeroes to the write matrix when there is an
|
||||
# insufficiently large change in the data from the last written frame.
|
||||
if options.should_clean_keys:
|
||||
threshold = 0.001
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for fcurve_index in range(len(import_bone.fcurves)):
|
||||
# Get all the keyframe data for the bone's f-curve data from the sequence data matrix.
|
||||
fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
last_written_datum = 0
|
||||
for frame_index, datum in enumerate(fcurve_frame_data):
|
||||
# If the f-curve data is not different enough to the last written frame,
|
||||
# un-mark this data for writing.
|
||||
if frame_index > 0 and abs(datum - last_written_datum) < threshold:
|
||||
keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0
|
||||
else:
|
||||
last_written_datum = datum
|
||||
|
||||
# Write the keyframes out!
|
||||
for frame_index in range(sequence.frame_count):
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
bone_has_writeable_keyframes = any(keyframe_write_matrix[frame_index, bone_index])
|
||||
if bone_has_writeable_keyframes:
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
for fcurve, should_write, datum in zip(import_bone.fcurves,
|
||||
keyframe_write_matrix[frame_index, bone_index],
|
||||
key_data):
|
||||
if should_write:
|
||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
for fcurve, datum in zip(import_bone.fcurves, key_data):
|
||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
||||
|
||||
if options.should_convert_to_samples:
|
||||
for fcurve in action.fcurves:
|
||||
fcurve.convert_to_samples(start=0, end=sequence.frame_count)
|
||||
|
||||
# Write
|
||||
if options.should_write_metadata:
|
||||
@@ -266,9 +243,6 @@ class PsaImportPropertyGroup(PropertyGroup):
|
||||
psa: PointerProperty(type=PsaDataPropertyGroup)
|
||||
sequence_list: CollectionProperty(type=PsaImportActionListItem)
|
||||
sequence_list_index: IntProperty(name='', default=0)
|
||||
should_clean_keys: BoolProperty(default=True, name='Clean Keyframes',
|
||||
description='Exclude unnecessary keyframes from being written to the actions',
|
||||
options=empty_set)
|
||||
should_use_fake_user: BoolProperty(default=True, name='Fake User',
|
||||
description='Assign each imported action a fake user so that the data block is saved even it has no users',
|
||||
options=empty_set)
|
||||
@@ -289,6 +263,11 @@ class PsaImportPropertyGroup(PropertyGroup):
|
||||
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
||||
description='Filter using regular expressions', options=empty_set)
|
||||
select_text: PointerProperty(type=bpy.types.Text)
|
||||
should_convert_to_samples: BoolProperty(
|
||||
default=True,
|
||||
name='Convert to Samples',
|
||||
description='Convert keyframes to read-only samples. Recommended if you do not plan on editing the actions directly'
|
||||
)
|
||||
|
||||
|
||||
def filter_sequences(pg: PsaImportPropertyGroup, sequences) -> List[int]:
|
||||
@@ -457,11 +436,14 @@ class PSA_PT_ImportPanel_Advanced(Panel):
|
||||
layout = self.layout
|
||||
pg = getattr(context.scene, 'psa_import')
|
||||
|
||||
col = layout.column(heading="Options")
|
||||
col = layout.column(heading='Keyframes')
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
col.prop(pg, 'should_clean_keys')
|
||||
col.prop(pg, 'should_convert_to_samples')
|
||||
col.separator()
|
||||
col = layout.column(heading='Options')
|
||||
col.use_property_split = True
|
||||
col.use_property_decorate = False
|
||||
col.prop(pg, 'should_use_fake_user')
|
||||
col.prop(pg, 'should_stash')
|
||||
col.prop(pg, 'should_use_action_name_prefix')
|
||||
@@ -585,13 +567,13 @@ class PsaImportOperator(Operator):
|
||||
|
||||
options = PsaImportOptions()
|
||||
options.sequence_names = sequence_names
|
||||
options.should_clean_keys = pg.should_clean_keys
|
||||
options.should_use_fake_user = pg.should_use_fake_user
|
||||
options.should_stash = pg.should_stash
|
||||
options.action_name_prefix = pg.action_name_prefix if pg.should_use_action_name_prefix else ''
|
||||
options.should_overwrite = pg.should_overwrite
|
||||
options.should_write_metadata = pg.should_write_metadata
|
||||
options.should_write_keyframes = pg.should_write_keyframes
|
||||
options.should_convert_to_samples = pg.should_convert_to_samples
|
||||
|
||||
import_psa(psa_reader, context.view_layer.objects.active, options)
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ class PsaReader(object):
|
||||
def sequences(self):
|
||||
return self.psa.sequences
|
||||
|
||||
def read_sequence_data_matrix(self, sequence_name: str):
|
||||
def read_sequence_data_matrix(self, sequence_name: str) -> np.ndarray:
|
||||
"""
|
||||
Reads and returns the data matrix for the given sequence.
|
||||
@param sequence_name: The name of the sequence.
|
||||
@return: An FxBx7 matrix where F is the number of frames, B is the number of bones.
|
||||
"""
|
||||
sequence = self.psa.sequences[sequence_name]
|
||||
keys = self.read_sequence_keys(sequence_name)
|
||||
bone_count = len(self.bones)
|
||||
@@ -41,8 +46,8 @@ class PsaReader(object):
|
||||
"""
|
||||
Reads and returns the key data for a sequence.
|
||||
|
||||
:param sequence_name: The name of the sequence.
|
||||
:return: A list of Psa.Keys.
|
||||
@param sequence_name: The name of the sequence.
|
||||
@return: A list of Psa.Keys.
|
||||
"""
|
||||
# Set the file reader to the beginning of the keys data
|
||||
sequence = self.psa.sequences[sequence_name]
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List
|
||||
import bmesh
|
||||
import bpy
|
||||
|
||||
from .data import *
|
||||
from ..helpers import *
|
||||
import bmesh
|
||||
import bpy
|
||||
|
||||
|
||||
class PskInputObjects(object):
|
||||
@@ -19,6 +17,7 @@ class PskBuildOptions(object):
|
||||
self.bone_group_indices: List[int] = []
|
||||
self.use_raw_mesh_data = True
|
||||
self.material_names: List[str] = []
|
||||
self.should_ignore_bone_name_restrictions = False
|
||||
|
||||
|
||||
def get_psk_input_objects(context) -> PskInputObjects:
|
||||
@@ -81,7 +80,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
||||
bones = [armature_object.data.bones[bone_name] for bone_name in bone_names]
|
||||
|
||||
# Check that all bone names are valid.
|
||||
check_bone_names(map(lambda x: x.name, bones))
|
||||
if not options.should_ignore_bone_name_restrictions:
|
||||
check_bone_names(map(lambda x: x.name, bones))
|
||||
|
||||
for bone in bones:
|
||||
psk_bone = Psk.Bone()
|
||||
@@ -133,16 +133,17 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
||||
# MATERIALS
|
||||
material_indices = [material_names.index(material.name) for material in input_mesh_object.data.materials]
|
||||
|
||||
# MESH DATA
|
||||
if options.use_raw_mesh_data:
|
||||
mesh_object = input_mesh_object
|
||||
mesh_data = input_mesh_object.data
|
||||
else:
|
||||
# Create a copy of the mesh object after non-armature modifiers are applied.
|
||||
|
||||
# Temporarily deactivate any armature modifiers on the input mesh object.
|
||||
active_armature_modifiers = [x for x in filter(lambda x: x.type == 'ARMATURE' and x.is_active, input_mesh_object.modifiers)]
|
||||
for modifier in active_armature_modifiers:
|
||||
modifier.show_viewport = False
|
||||
# Temporarily force the armature into the rest position.
|
||||
# We will undo this later.
|
||||
old_pose_position = armature_object.data.pose_position
|
||||
armature_object.data.pose_position = 'REST'
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
bm = bmesh.new()
|
||||
@@ -157,9 +158,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
||||
for vertex_group in input_mesh_object.vertex_groups:
|
||||
mesh_object.vertex_groups.new(name=vertex_group.name)
|
||||
|
||||
# Reactivate previously active armature modifiers
|
||||
for modifier in active_armature_modifiers:
|
||||
modifier.show_viewport = True
|
||||
# Restore the previous pose position on the armature.
|
||||
armature_object.data.pose_position = old_pose_position
|
||||
|
||||
vertex_offset = len(psk.points)
|
||||
|
||||
|
||||
@@ -208,6 +208,10 @@ class PskExportOperator(Operator, ExportHelper):
|
||||
col.operator(PskMaterialListItemMoveUp.bl_idname, text='', icon='TRIA_UP')
|
||||
col.operator(PskMaterialListItemMoveDown.bl_idname, text='', icon='TRIA_DOWN')
|
||||
|
||||
layout.separator()
|
||||
|
||||
layout.prop(pg, 'should_ignore_bone_name_restrictions')
|
||||
|
||||
def execute(self, context):
|
||||
pg = context.scene.psk_export
|
||||
options = PskBuildOptions()
|
||||
@@ -215,6 +219,7 @@ class PskExportOperator(Operator, ExportHelper):
|
||||
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
|
||||
options.use_raw_mesh_data = pg.use_raw_mesh_data
|
||||
options.material_names = [m.material_name for m in pg.material_list]
|
||||
options.should_ignore_bone_name_restrictions = pg.should_ignore_bone_name_restrictions
|
||||
|
||||
try:
|
||||
psk = build_psk(context, options)
|
||||
@@ -242,6 +247,12 @@ class PskExportPropertyGroup(PropertyGroup):
|
||||
use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh')
|
||||
material_list: CollectionProperty(type=MaterialListItem)
|
||||
material_list_index: IntProperty(default=0)
|
||||
should_ignore_bone_name_restrictions: BoolProperty(
|
||||
default=False,
|
||||
name='Ignore Bone Name Restrictions',
|
||||
description='Bone names restrictions will be ignored. Note that bone names without properly formatted names '
|
||||
'cannot be referenced in scripts.'
|
||||
)
|
||||
|
||||
|
||||
classes = (
|
||||
|
||||
Reference in New Issue
Block a user