1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-23 22:40:59 +01:00

* Fixed a bug where the action name prefix could be applied even if the checkbox was deselected

* Fixed a typo in the PSA reader
* Added the ability to define the bone length of imported PSK armatures
* The PSK import options (extra UVs, vertex colors etc.) are now actually respected if turned off
* Ran automated formatting on all the code to quell the PEP8 gods
* Incremented version to 3.0.0
This commit is contained in:
Colin Basnett 2022-04-24 22:08:36 -07:00
parent d56aa3ab65
commit 1eafb71dce
13 changed files with 147 additions and 109 deletions

View File

@ -1,7 +1,7 @@
bl_info = {
"name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett",
"version": (2, 1, 0),
"version": (3, 0, 0),
"blender": (2, 80, 0),
# "location": "File > Export > PSK Export (.psk)",
"description": "PSK/PSA Import/Export (.psk/.psa)",

View File

@ -1,7 +1,8 @@
from bpy.types import NlaStrip
from typing import List, Tuple, Optional
from collections import Counter
import datetime
from collections import Counter
from typing import List
from bpy.types import NlaStrip
class Timer:

View File

@ -1,7 +1,9 @@
from typing import Dict, Iterable
from bpy.types import Action
from .data import *
from ..helpers import *
from typing import Dict, Iterable
from bpy.types import Action
class PsaBuilderOptions(object):
@ -37,13 +39,13 @@ class PsaBuilder(object):
return options.fps_custom
elif options.fps_source == 'ACTION_METADATA':
# Get the minimum value of action metadata FPS values.
psa_fps_list = []
for action in filter(lambda x: 'psa_fps' in x, actions):
psa_fps = action['psa_fps']
if type(psa_fps) == int or type(psa_fps) == float:
psa_fps_list.append(psa_fps)
if len(psa_fps_list) > 0:
return min(psa_fps_list)
fps_list = []
for action in filter(lambda x: 'psa_sequence_fps' in x, actions):
fps = action['psa_sequence_fps']
if type(fps) == int or type(fps) == float:
fps_list.append(fps)
if len(fps_list) > 0:
return min(fps_list)
else:
# No valid action metadata to use, fallback to scene FPS
return context.scene.render.fps
@ -166,7 +168,8 @@ class PsaBuilder(object):
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(active_object, frame_min, frame_max)))
nla_strips_actions = set(
map(lambda x: x.action, get_nla_strips_in_timeframe(active_object, frame_min, frame_max)))
export_sequence.fps = self.get_sequence_fps(context, options, nla_strips_actions)
export_sequences.append(export_sequence)
else:

View File

@ -1,6 +1,7 @@
import typing
from typing import List
from collections import OrderedDict
from typing import List
from ..data import *
"""

View File

@ -1,16 +1,19 @@
import bpy
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel, TimelineMarker
from bpy.props import CollectionProperty, IntProperty, FloatProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
from bpy_extras.io_utils import ExportHelper
from typing import Type
from .builder import PsaBuilder, PsaBuilderOptions
from .data import *
from ..types import BoneGroupListItem
from ..helpers import *
from collections import Counter
import fnmatch
import re
import sys
import fnmatch
from collections import Counter
from typing import Type
import bpy
from bpy.props import BoolProperty, CollectionProperty, EnumProperty, FloatProperty, IntProperty, PointerProperty, \
StringProperty
from bpy.types import Action, Operator, PropertyGroup, UIList
from bpy_extras.io_utils import ExportHelper
from .builder import PsaBuilder, PsaBuilderOptions
from .data import *
from ..helpers import *
from ..types import BoneGroupListItem
class PsaExporter(object):
@ -57,7 +60,7 @@ def update_action_names(context):
item.action_name = get_psa_sequence_name(action, pg.should_use_original_sequence_names)
def should_use_original_sequence_names_updated(property, context):
def should_use_original_sequence_names_updated(_, context):
update_action_names(context)
@ -68,7 +71,8 @@ class PsaExportPropertyGroup(PropertyGroup):
description='',
items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers', 'MARKER_HLT', 1),
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers', 'MARKER_HLT',
1),
)
)
fps_source: EnumProperty(
@ -77,11 +81,14 @@ class PsaExportPropertyGroup(PropertyGroup):
description='',
items=(
('SCENE', 'Scene', '', 'SCENE_DATA', 0),
('ACTION_METADATA', 'Action Metadata', 'The frame rate will be determined by action\'s "psa_fps" custom property, if it exists. If the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used. If no metadata is available, the scene\'s frame rate will be used.', 'PROPERTIES', 1),
('ACTION_METADATA', 'Action Metadata',
'The frame rate will be determined by action\'s "psa_sequence_fps" custom property, if it exists. If the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used. If no metadata is available, the scene\'s frame rate will be used.',
'PROPERTIES', 1),
('CUSTOM', 'Custom', '', 2)
)
)
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=set(), step=100, soft_max=60.0)
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=set(), step=100,
soft_max=60.0)
action_list: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem)
@ -117,7 +124,8 @@ class PsaExportPropertyGroup(PropertyGroup):
sequence_name_suffix: StringProperty(name='Suffix', options=set())
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
sequence_use_filter_invert: BoolProperty(default=False, options=set())
sequence_filter_asset: BoolProperty(default=False, name='Show assets', description='Show actions that belong to an asset library', options=set())
sequence_filter_asset: BoolProperty(default=False, name='Show assets',
description='Show actions that belong to an asset library', options=set())
sequence_use_filter_sort_reverse: BoolProperty(default=True, options=set())
@ -178,7 +186,8 @@ class PsaExportOperator(Operator, ExportHelper):
elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10))
layout.template_list('PSA_UL_ExportTimelineMarkerList', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
layout.template_list('PSA_UL_ExportTimelineMarkerList', '', pg, 'marker_list', pg, 'marker_list_index',
rows=rows)
col = layout.column()
col.use_property_split = True
@ -208,7 +217,8 @@ class PsaExportOperator(Operator, ExportHelper):
row.operator(PsaExportBoneGroupsSelectAll.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PsaExportBoneGroupsDeselectAll.bl_idname, text='None', icon='CHECKBOX_DEHLT')
rows = max(3, min(len(pg.bone_group_list), 10))
layout.template_list('PSX_UL_BoneGroupList', '', pg, 'bone_group_list', pg, 'bone_group_list_index', rows=rows)
layout.template_list('PSX_UL_BoneGroupList', '', pg, 'bone_group_list', pg, 'bone_group_list_index',
rows=rows)
def should_action_be_selected_by_default(self, action):
return action is not None and action.asset_data is None
@ -336,7 +346,8 @@ def filter_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_c
return flt_flags
def get_visible_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[PsaExportActionListItem]:
def get_visible_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[
PsaExportActionListItem]:
visible_sequences = []
for i, flag in enumerate(filter_sequences(pg, sequences)):
if bool(flag & (1 << 30)):

View File

@ -1,14 +1,16 @@
import bpy
import os
import numpy as np
import re
import fnmatch
from mathutils import Vector, Quaternion, Matrix
from .data import Psa
from typing import List, AnyStr, Optional
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams
from bpy_extras.io_utils import ImportHelper
import os
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
from mathutils import Vector, Quaternion
from .data import Psa
from .reader import PsaReader
@ -18,7 +20,6 @@ class PsaImportOptions(object):
self.should_use_fake_user = False
self.should_stash = False
self.sequence_names = []
self.should_use_action_name_prefix = False
self.should_overwrite = False
self.should_write_keyframes = True
self.should_write_metadata = True
@ -76,7 +77,8 @@ class PsaImporter(object):
# Report if there are missing bones in the target armature.
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))
if len(missing_bone_names) > 0:
print(f'The armature object \'{armature_object.name}\' is missing the following bones that exist in the PSA:')
print(
f'The armature object \'{armature_object.name}\' is missing the following bones that exist in the PSA:')
print(list(sorted(missing_bone_names)))
del armature_bone_names
@ -192,14 +194,16 @@ class PsaImporter(object):
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):
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'})
# Write
if options.should_write_metadata:
action['psa_sequence_name'] = sequence_name
action['psa_fps'] = sequence.fps
action['psa_sequence_fps'] = sequence.fps
action.use_fake_user = options.should_use_fake_user
@ -259,18 +263,28 @@ 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=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=set())
should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track', options=set())
should_clean_keys: BoolProperty(default=True, name='Clean Keyframes',
description='Exclude unnecessary keyframes from being written to the actions',
options=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=set())
should_stash: BoolProperty(default=False, name='Stash',
description='Stash each imported action as a strip on a new non-contributing NLA track',
options=set())
should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name', options=set())
should_overwrite: BoolProperty(default=False, name='Reuse Existing Datablocks', options=set())
should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=set())
should_write_metadata: BoolProperty(default=True, name='Metadata', options=set(), description='Additional data will be written to the custom properties of the Action (e.g., frame rate)')
action_name_prefix: StringProperty(default='', name='Prefix', options=set())
should_overwrite: BoolProperty(default=False, name='Reuse Existing Actions', options=set(),
description='If an action with a matching name already exists, the existing action will have it\'s data overwritten instead of a new action being created')
should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=set())
should_write_metadata: BoolProperty(default=True, name='Metadata', options=set(),
description='Additional data will be written to the custom properties of the Action (e.g., frame rate)')
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
sequence_filter_is_selected: BoolProperty(default=False, options=set(), name='Only Show Selected', description='Only show selected sequences')
sequence_filter_is_selected: BoolProperty(default=False, options=set(), name='Only Show Selected',
description='Only show selected sequences')
sequence_use_filter_invert: BoolProperty(default=False, options=set())
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression', description='Filter using regular expressions', options=set())
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
description='Filter using regular expressions', options=set())
select_text: PointerProperty(type=bpy.types.Text)
@ -290,7 +304,7 @@ def filter_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_c
except re.error:
pass
else:
# User regular matching
# User regular text matching.
for i, sequence in enumerate(sequences):
if not fnmatch.fnmatch(sequence.action_name, f'*{pg.sequence_filter_name}*'):
flt_flags[i] &= ~bitflag_filter_item
@ -308,7 +322,8 @@ def filter_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_c
return flt_flags
def get_visible_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[PsaImportActionListItem]:
def get_visible_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[
PsaImportActionListItem]:
bitflag_filter_item = 1 << 30
visible_sequences = []
for i, flag in enumerate(filter_sequences(pg, sequences)):
@ -451,21 +466,6 @@ class PSA_PT_ImportPanel_Advanced(Panel):
col.prop(pg, 'action_name_prefix')
class PSA_PT_ImportPanel_PsaData(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_label = 'PSA Info'
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = 'PSA_PT_ImportPanel'
def draw(self, context):
layout = self.layout
pg = context.scene.psa_import.psa
layout.label(text=f'{len(pg.bones)} Bones', icon='BONE_DATA')
layout.label(text=f'{pg.sequence_count} Sequences', icon='SEQUENCE')
class PSA_PT_ImportPanel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
@ -584,7 +584,7 @@ class PsaImportOperator(Operator):
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
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
@ -632,7 +632,6 @@ classes = (
PsaImportFileReload,
PSA_PT_ImportPanel,
PSA_PT_ImportPanel_Advanced,
PSA_PT_ImportPanel_PsaData,
PsaImportOperator,
PsaImportFileSelectOperator,
PsaImportSelectFile,

View File

@ -1,14 +1,17 @@
from .data import *
import ctypes
import numpy as np
from .data import *
class PsaReader(object):
"""
This class will read the sequences and bone information immediately upon instantiation and hold onto a file handle.
This class reads the sequences and bone information immediately upon instantiation and hold onto a file handle.
The key data is not read into memory upon instantiation due to it's potentially very large size.
To read the key data for a particular sequence, call `read_sequence_keys`.
"""
def __init__(self, path):
self.keys_data_offset: int = 0
self.fp = open(path, 'rb')
@ -22,15 +25,6 @@ class PsaReader(object):
def sequences(self) -> OrderedDict[Psa.Sequence]:
return self.psa.sequences
@staticmethod
def _read_types(fp, data_class: ctypes.Structure, section: Section, data):
buffer_length = section.data_size * section.data_count
buffer = fp.read(buffer_length)
offset = 0
for _ in range(section.data_count):
data.append(data_class.from_buffer_copy(buffer, offset))
offset += section.data_size
def read_sequence_data_matrix(self, sequence_name: str):
sequence = self.psa.sequences[sequence_name]
keys = self.read_sequence_keys(sequence_name)
@ -65,6 +59,15 @@ class PsaReader(object):
offset += data_size
return keys
@staticmethod
def _read_types(fp, data_class: ctypes.Structure, section: Section, data):
buffer_length = section.data_size * section.data_count
buffer = fp.read(buffer_length)
offset = 0
for _ in range(section.data_count):
data.append(data_class.from_buffer_copy(buffer, offset))
offset += section.data_size
def _read(self, fp) -> Psa:
psa = Psa()
while fp.read(1):
@ -88,4 +91,3 @@ class PsaReader(object):
else:
raise RuntimeError(f'Unrecognized section "{section.name}"')
return psa
1

View File

@ -1,6 +1,5 @@
import bpy
import bmesh
from collections import OrderedDict
from .data import *
from ..helpers import *

View File

@ -1,9 +1,9 @@
from typing import List
from ..data import *
class Psk(object):
class Wedge(object):
def __init__(self):
self.point_index: int = 0

View File

@ -1,11 +1,13 @@
from .data import *
from ..types import BoneGroupListItem
from ..helpers import populate_bone_group_list
from .builder import PskBuilder, PskBuilderOptions
from typing import Type
from bpy.props import StringProperty, CollectionProperty, IntProperty, EnumProperty
from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, CollectionProperty, IntProperty, BoolProperty, EnumProperty
from .builder import PskBuilder, PskBuilderOptions
from .data import *
from ..helpers import populate_bone_group_list
from ..types import BoneGroupListItem
MAX_WEDGE_COUNT = 65536
MAX_POINT_COUNT = 4294967296
@ -144,7 +146,8 @@ class PskExportPropertyGroup(PropertyGroup):
description='',
items=(
('ALL', 'All', 'All bones will be exported.'),
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.')
('BONE_GROUPS', 'Bone Groups',
'Only bones belonging to the selected bone groups and their ancestors will be exported.')
)
)
bone_group_list: CollectionProperty(type=BoneGroupListItem)

View File

@ -1,16 +1,19 @@
import os
import bpy
import bmesh
import numpy as np
import sys
from math import inf
from typing import Optional
from .data import Psk
from ..helpers import rgb_to_srgb
from mathutils import Quaternion, Vector, Matrix
from .reader import PskReader
from bpy.props import StringProperty, EnumProperty, BoolProperty
import bmesh
import bpy
import numpy as np
from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty
from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ImportHelper
from mathutils import Quaternion, Vector, Matrix
from .data import Psk
from .reader import PskReader
from ..helpers import rgb_to_srgb
class PskImportOptions(object):
@ -20,6 +23,7 @@ class PskImportOptions(object):
self.vertex_color_space = 'sRGB'
self.should_import_vertex_normals = True
self.should_import_extra_uvs = True
self.bone_length = 1.0
class PskImporter(object):
@ -60,7 +64,6 @@ class PskImporter(object):
self.post_quat: Quaternion = Quaternion()
import_bones = []
new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones):
import_bone = ImportBone(bone_index, psk_bone)
@ -93,7 +96,7 @@ class PskImporter(object):
else:
import_bone.local_rotation.conjugate()
edit_bone.tail = Vector((0.0, new_bone_size, 0.0))
edit_bone.tail = Vector((0.0, options.bone_length, 0.0))
edit_bone_matrix = import_bone.local_rotation.conjugated()
edit_bone_matrix.rotate(import_bone.world_matrix)
edit_bone_matrix = edit_bone_matrix.to_matrix().to_4x4()
@ -209,7 +212,8 @@ class PskImporter(object):
# Get a list of all bones that have weights associated with them.
vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights))
for import_bone in map(lambda x: import_bones[x], sorted(list(vertex_group_bone_indices))):
import_bone.vertex_group = mesh_object.vertex_groups.new(name=import_bone.psk_bone.name.decode('windows-1252'))
import_bone.vertex_group = mesh_object.vertex_groups.new(
name=import_bone.psk_bone.name.decode('windows-1252'))
for weight in psk.weights:
import_bones[weight.bone_index].vertex_group.add((weight.point_index,), weight.weight, 'ADD')
@ -256,6 +260,15 @@ class PskImportPropertyGroup(PropertyGroup):
options=set(),
description='Import extra UV maps from PSKX files, if available'
)
bone_length: FloatProperty(
default=1.0,
min=sys.float_info.epsilon,
step=100,
soft_min=1.0,
name='Bone Length',
options=set(),
description='Length of the bones'
)
class PskImportOperator(Operator, ImportHelper):
@ -277,7 +290,11 @@ class PskImportOperator(Operator, ImportHelper):
psk = reader.read(self.filepath)
options = PskImportOptions()
options.name = os.path.splitext(os.path.basename(self.filepath))[0]
options.should_import_extra_uvs = pg.should_import_extra_uvs
options.should_import_vertex_colors = pg.should_import_vertex_colors
options.should_import_vertex_normals = pg.should_import_vertex_normals
options.vertex_color_space = pg.vertex_color_space
options.bone_length = pg.bone_length
PskImporter().import_psk(psk, context, options)
return {'FINISHED'}
@ -291,6 +308,7 @@ class PskImportOperator(Operator, ImportHelper):
layout.prop(pg, 'should_import_vertex_colors')
if pg.should_import_vertex_colors:
layout.prop(pg, 'vertex_color_space')
layout.prop(pg, 'bone_length')
classes = (

View File

@ -1,6 +1,7 @@
from .data import *
import ctypes
from .data import *
class PskReader(object):

View File

@ -1,5 +1,5 @@
from bpy.types import PropertyGroup, UIList
from bpy.props import StringProperty, IntProperty, BoolProperty
from bpy.types import PropertyGroup, UIList
class PSX_UL_BoneGroupList(UIList):