1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-28 00:20:48 +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 = { bl_info = {
"name": "PSK/PSA Importer/Exporter", "name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett", "author": "Colin Basnett",
"version": (2, 1, 0), "version": (3, 0, 0),
"blender": (2, 80, 0), "blender": (2, 80, 0),
# "location": "File > Export > PSK Export (.psk)", # "location": "File > Export > PSK Export (.psk)",
"description": "PSK/PSA Import/Export (.psk/.psa)", "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 import datetime
from collections import Counter
from typing import List
from bpy.types import NlaStrip
class Timer: class Timer:

View File

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

View File

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

View File

@ -1,16 +1,19 @@
import bpy import fnmatch
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 re import re
import sys 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): 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) 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) update_action_names(context)
@ -68,7 +71,8 @@ class PsaExportPropertyGroup(PropertyGroup):
description='', description='',
items=( items=(
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), ('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( fps_source: EnumProperty(
@ -77,11 +81,14 @@ class PsaExportPropertyGroup(PropertyGroup):
description='', description='',
items=( items=(
('SCENE', 'Scene', '', 'SCENE_DATA', 0), ('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) ('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: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(default=0) action_list_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem) marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem)
@ -117,7 +124,8 @@ class PsaExportPropertyGroup(PropertyGroup):
sequence_name_suffix: StringProperty(name='Suffix', options=set()) sequence_name_suffix: StringProperty(name='Suffix', options=set())
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'}) sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
sequence_use_filter_invert: BoolProperty(default=False, options=set()) 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()) sequence_use_filter_sort_reverse: BoolProperty(default=True, options=set())
@ -178,7 +186,8 @@ class PsaExportOperator(Operator, ExportHelper):
elif pg.sequence_source == 'TIMELINE_MARKERS': elif pg.sequence_source == 'TIMELINE_MARKERS':
rows = max(3, min(len(pg.marker_list), 10)) 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 = layout.column()
col.use_property_split = True 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(PsaExportBoneGroupsSelectAll.bl_idname, text='All', icon='CHECKBOX_HLT')
row.operator(PsaExportBoneGroupsDeselectAll.bl_idname, text='None', icon='CHECKBOX_DEHLT') row.operator(PsaExportBoneGroupsDeselectAll.bl_idname, text='None', icon='CHECKBOX_DEHLT')
rows = max(3, min(len(pg.bone_group_list), 10)) 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): def should_action_be_selected_by_default(self, action):
return action is not None and action.asset_data is None 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 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 = [] visible_sequences = []
for i, flag in enumerate(filter_sequences(pg, sequences)): for i, flag in enumerate(filter_sequences(pg, sequences)):
if bool(flag & (1 << 30)): if bool(flag & (1 << 30)):

View File

@ -1,14 +1,16 @@
import bpy
import os
import numpy as np
import re
import fnmatch import fnmatch
from mathutils import Vector, Quaternion, Matrix import os
from .data import Psa import re
from typing import List, AnyStr, Optional from typing import List, Optional
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams
from bpy_extras.io_utils import ImportHelper import bpy
import numpy as np
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty 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 from .reader import PsaReader
@ -18,7 +20,6 @@ class PsaImportOptions(object):
self.should_use_fake_user = False self.should_use_fake_user = False
self.should_stash = False self.should_stash = False
self.sequence_names = [] self.sequence_names = []
self.should_use_action_name_prefix = False
self.should_overwrite = False self.should_overwrite = False
self.should_write_keyframes = True self.should_write_keyframes = True
self.should_write_metadata = True self.should_write_metadata = True
@ -76,7 +77,8 @@ class PsaImporter(object):
# Report if there are missing bones in the target armature. # Report if there are missing bones in the target armature.
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names)) missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))
if len(missing_bone_names) > 0: 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))) print(list(sorted(missing_bone_names)))
del armature_bone_names del armature_bone_names
@ -192,14 +194,16 @@ class PsaImporter(object):
if bone_has_writeable_keyframes: if bone_has_writeable_keyframes:
# This bone has writeable keyframes for this frame. # This bone has writeable keyframes for this frame.
key_data = sequence_data_matrix[frame_index, bone_index] 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: if should_write:
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'}) fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
# Write # Write
if options.should_write_metadata: if options.should_write_metadata:
action['psa_sequence_name'] = sequence_name 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 action.use_fake_user = options.should_use_fake_user
@ -259,18 +263,28 @@ class PsaImportPropertyGroup(PropertyGroup):
psa: PointerProperty(type=PsaDataPropertyGroup) psa: PointerProperty(type=PsaDataPropertyGroup)
sequence_list: CollectionProperty(type=PsaImportActionListItem) sequence_list: CollectionProperty(type=PsaImportActionListItem)
sequence_list_index: IntProperty(name='', default=0) 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_clean_keys: BoolProperty(default=True, name='Clean Keyframes',
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()) description='Exclude unnecessary keyframes from being written to the actions',
should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track', options=set()) 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_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()) 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_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_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) 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: except re.error:
pass pass
else: else:
# User regular matching # User regular text matching.
for i, sequence in enumerate(sequences): for i, sequence in enumerate(sequences):
if not fnmatch.fnmatch(sequence.action_name, f'*{pg.sequence_filter_name}*'): if not fnmatch.fnmatch(sequence.action_name, f'*{pg.sequence_filter_name}*'):
flt_flags[i] &= ~bitflag_filter_item flt_flags[i] &= ~bitflag_filter_item
@ -308,7 +322,8 @@ def filter_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_c
return flt_flags 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 bitflag_filter_item = 1 << 30
visible_sequences = [] visible_sequences = []
for i, flag in enumerate(filter_sequences(pg, 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') 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): class PSA_PT_ImportPanel(Panel):
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
@ -584,7 +584,7 @@ class PsaImportOperator(Operator):
options.should_clean_keys = pg.should_clean_keys options.should_clean_keys = pg.should_clean_keys
options.should_use_fake_user = pg.should_use_fake_user options.should_use_fake_user = pg.should_use_fake_user
options.should_stash = pg.should_stash 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_overwrite = pg.should_overwrite
options.should_write_metadata = pg.should_write_metadata options.should_write_metadata = pg.should_write_metadata
options.should_write_keyframes = pg.should_write_keyframes options.should_write_keyframes = pg.should_write_keyframes
@ -632,7 +632,6 @@ classes = (
PsaImportFileReload, PsaImportFileReload,
PSA_PT_ImportPanel, PSA_PT_ImportPanel,
PSA_PT_ImportPanel_Advanced, PSA_PT_ImportPanel_Advanced,
PSA_PT_ImportPanel_PsaData,
PsaImportOperator, PsaImportOperator,
PsaImportFileSelectOperator, PsaImportFileSelectOperator,
PsaImportSelectFile, PsaImportSelectFile,

View File

@ -1,14 +1,17 @@
from .data import *
import ctypes import ctypes
import numpy as np import numpy as np
from .data import *
class PsaReader(object): 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. 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`. To read the key data for a particular sequence, call `read_sequence_keys`.
""" """
def __init__(self, path): def __init__(self, path):
self.keys_data_offset: int = 0 self.keys_data_offset: int = 0
self.fp = open(path, 'rb') self.fp = open(path, 'rb')
@ -22,15 +25,6 @@ class PsaReader(object):
def sequences(self) -> OrderedDict[Psa.Sequence]: def sequences(self) -> OrderedDict[Psa.Sequence]:
return self.psa.sequences 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): def read_sequence_data_matrix(self, sequence_name: str):
sequence = self.psa.sequences[sequence_name] sequence = self.psa.sequences[sequence_name]
keys = self.read_sequence_keys(sequence_name) keys = self.read_sequence_keys(sequence_name)
@ -65,6 +59,15 @@ class PsaReader(object):
offset += data_size offset += data_size
return keys 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: def _read(self, fp) -> Psa:
psa = Psa() psa = Psa()
while fp.read(1): while fp.read(1):
@ -88,4 +91,3 @@ class PsaReader(object):
else: else:
raise RuntimeError(f'Unrecognized section "{section.name}"') raise RuntimeError(f'Unrecognized section "{section.name}"')
return psa return psa
1

View File

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

View File

@ -1,9 +1,9 @@
from typing import List from typing import List
from ..data import * from ..data import *
class Psk(object): class Psk(object):
class Wedge(object): class Wedge(object):
def __init__(self): def __init__(self):
self.point_index: int = 0 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 typing import Type
from bpy.props import StringProperty, CollectionProperty, IntProperty, EnumProperty
from bpy.types import Operator, PropertyGroup from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ExportHelper 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_WEDGE_COUNT = 65536
MAX_POINT_COUNT = 4294967296 MAX_POINT_COUNT = 4294967296
@ -144,7 +146,8 @@ class PskExportPropertyGroup(PropertyGroup):
description='', description='',
items=( items=(
('ALL', 'All', 'All bones will be exported.'), ('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) bone_group_list: CollectionProperty(type=BoneGroupListItem)

View File

@ -1,16 +1,19 @@
import os import os
import bpy import sys
import bmesh
import numpy as np
from math import inf from math import inf
from typing import Optional from typing import Optional
from .data import Psk
from ..helpers import rgb_to_srgb import bmesh
from mathutils import Quaternion, Vector, Matrix import bpy
from .reader import PskReader import numpy as np
from bpy.props import StringProperty, EnumProperty, BoolProperty from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty
from bpy.types import Operator, PropertyGroup from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ImportHelper 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): class PskImportOptions(object):
@ -20,6 +23,7 @@ class PskImportOptions(object):
self.vertex_color_space = 'sRGB' self.vertex_color_space = 'sRGB'
self.should_import_vertex_normals = True self.should_import_vertex_normals = True
self.should_import_extra_uvs = True self.should_import_extra_uvs = True
self.bone_length = 1.0
class PskImporter(object): class PskImporter(object):
@ -60,7 +64,6 @@ class PskImporter(object):
self.post_quat: Quaternion = Quaternion() self.post_quat: Quaternion = Quaternion()
import_bones = [] import_bones = []
new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones): for bone_index, psk_bone in enumerate(psk.bones):
import_bone = ImportBone(bone_index, psk_bone) import_bone = ImportBone(bone_index, psk_bone)
@ -93,7 +96,7 @@ class PskImporter(object):
else: else:
import_bone.local_rotation.conjugate() 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 = import_bone.local_rotation.conjugated()
edit_bone_matrix.rotate(import_bone.world_matrix) edit_bone_matrix.rotate(import_bone.world_matrix)
edit_bone_matrix = edit_bone_matrix.to_matrix().to_4x4() 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. # 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)) 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))): 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: for weight in psk.weights:
import_bones[weight.bone_index].vertex_group.add((weight.point_index,), weight.weight, 'ADD') import_bones[weight.bone_index].vertex_group.add((weight.point_index,), weight.weight, 'ADD')
@ -256,6 +260,15 @@ class PskImportPropertyGroup(PropertyGroup):
options=set(), options=set(),
description='Import extra UV maps from PSKX files, if available' 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): class PskImportOperator(Operator, ImportHelper):
@ -277,7 +290,11 @@ class PskImportOperator(Operator, ImportHelper):
psk = reader.read(self.filepath) psk = reader.read(self.filepath)
options = PskImportOptions() options = PskImportOptions()
options.name = os.path.splitext(os.path.basename(self.filepath))[0] 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.vertex_color_space = pg.vertex_color_space
options.bone_length = pg.bone_length
PskImporter().import_psk(psk, context, options) PskImporter().import_psk(psk, context, options)
return {'FINISHED'} return {'FINISHED'}
@ -291,6 +308,7 @@ class PskImportOperator(Operator, ImportHelper):
layout.prop(pg, 'should_import_vertex_colors') layout.prop(pg, 'should_import_vertex_colors')
if pg.should_import_vertex_colors: if pg.should_import_vertex_colors:
layout.prop(pg, 'vertex_color_space') layout.prop(pg, 'vertex_color_space')
layout.prop(pg, 'bone_length')
classes = ( classes = (

View File

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

View File

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