Loads of cleanup
This commit is contained in:
@@ -36,28 +36,40 @@ if 'bpy' in locals():
|
|||||||
else:
|
else:
|
||||||
from .shared import data as shared_data, types as shared_types, helpers as shared_helpers
|
from .shared import data as shared_data, types as shared_types, helpers as shared_helpers
|
||||||
from .shared import dfs as shared_dfs, ui as shared_ui
|
from .shared import dfs as shared_dfs, ui as shared_ui
|
||||||
from .psk import data as psk_data, builder as psk_builder, writer as psk_writer, \
|
from .psk import (
|
||||||
importer as psk_importer, properties as psk_properties
|
builder as psk_builder,
|
||||||
|
data as psk_data,
|
||||||
|
importer as psk_importer,
|
||||||
|
properties as psk_properties,
|
||||||
|
writer as psk_writer,
|
||||||
|
)
|
||||||
from .psk import reader as psk_reader, ui as psk_ui
|
from .psk import reader as psk_reader, ui as psk_ui
|
||||||
from .psk.export import properties as psk_export_properties, ui as psk_export_ui, \
|
from .psk.export import (
|
||||||
operators as psk_export_operators
|
operators as psk_export_operators,
|
||||||
|
properties as psk_export_properties,
|
||||||
|
ui as psk_export_ui,
|
||||||
|
)
|
||||||
from .psk.import_ import operators as psk_import_operators
|
from .psk.import_ import operators as psk_import_operators
|
||||||
|
|
||||||
from .psa import config as psa_config, data as psa_data, writer as psa_writer, reader as psa_reader, \
|
from .psa import (
|
||||||
builder as psa_builder, importer as psa_importer
|
config as psa_config,
|
||||||
from .psa.export import properties as psa_export_properties, ui as psa_export_ui, \
|
data as psa_data,
|
||||||
operators as psa_export_operators
|
writer as psa_writer,
|
||||||
|
reader as psa_reader,
|
||||||
|
builder as psa_builder,
|
||||||
|
importer as psa_importer,
|
||||||
|
)
|
||||||
|
from .psa.export import (
|
||||||
|
properties as psa_export_properties,
|
||||||
|
ui as psa_export_ui,
|
||||||
|
operators as psa_export_operators,
|
||||||
|
)
|
||||||
from .psa.import_ import operators as psa_import_operators
|
from .psa.import_ import operators as psa_import_operators
|
||||||
from .psa.import_ import ui as psa_import_ui, properties as psa_import_properties
|
from .psa.import_ import ui as psa_import_ui, properties as psa_import_properties
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import PointerProperty
|
from bpy.props import PointerProperty
|
||||||
|
|
||||||
# TODO: just here so that it's not unreferenced and removed on save.
|
|
||||||
if [shared_data, shared_helpers, psk_data, psk_reader, psk_writer, psk_builder, psk_importer, psa_data, psa_config,
|
|
||||||
psa_reader, psa_writer, psa_builder, psa_importer]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
classes = shared_types.classes +\
|
classes = shared_types.classes +\
|
||||||
psk_properties.classes +\
|
psk_properties.classes +\
|
||||||
psk_ui.classes +\
|
psk_ui.classes +\
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ paths_exclude_pattern = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[permissions]
|
[permissions]
|
||||||
files = "Import/export PSK and PSA files from/to disk"
|
files = "Read and write PSK and PSA files from and to disk"
|
||||||
|
|||||||
@@ -124,9 +124,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
|||||||
if len(psa.bones) == 0:
|
if len(psa.bones) == 0:
|
||||||
raise RuntimeError('No bones available for export')
|
raise RuntimeError('No bones available for export')
|
||||||
|
|
||||||
# We invert the export space matrix so that we neutralize the transform of the armature object.
|
|
||||||
# export_space_matrix_inverse = get_export_space_matrix(options.export_space, armature_object)
|
|
||||||
|
|
||||||
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
||||||
for export_sequence in options.sequences:
|
for export_sequence in options.sequences:
|
||||||
export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'
|
export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import typing
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import List
|
from typing import List, OrderedDict as OrderedDictType
|
||||||
|
|
||||||
from ctypes import Structure, c_char, c_int32, c_float
|
from ctypes import Structure, c_char, c_int32, c_float
|
||||||
from ..shared.data import PsxBone, Quaternion, Vector3
|
from ..shared.data import PsxBone, Quaternion, Vector3
|
||||||
|
|
||||||
|
|
||||||
class Psa:
|
class Psa:
|
||||||
'''
|
"""
|
||||||
Note that keys are not stored within the Psa object.
|
Note that keys are not stored within the Psa object.
|
||||||
Use the PsaReader::get_sequence_keys to get the keys for a sequence.
|
Use the `PsaReader.get_sequence_keys` to get the keys for a sequence.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
class Sequence(Structure):
|
class Sequence(Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
@@ -50,5 +49,5 @@ class Psa:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bones: List[PsxBone] = []
|
self.bones: List[PsxBone] = []
|
||||||
self.sequences: typing.OrderedDict[str, Psa.Sequence] = OrderedDict()
|
self.sequences: OrderedDictType[str, Psa.Sequence] = OrderedDict()
|
||||||
self.keys: List[Psa.Key] = []
|
self.keys: List[Psa.Key] = []
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ from bpy.props import StringProperty
|
|||||||
from bpy.types import Context, Action, Object, AnimData, TimelineMarker, Operator
|
from bpy.types import Context, Action, Object, AnimData, TimelineMarker, Operator
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences, \
|
from .properties import (
|
||||||
get_sequences_from_name_and_frame_range
|
PSA_PG_export,
|
||||||
|
PSA_PG_export_action_list_item,
|
||||||
|
filter_sequences,
|
||||||
|
get_sequences_from_name_and_frame_range,
|
||||||
|
)
|
||||||
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
||||||
from ..writer import write_psa
|
from ..writer import write_psa
|
||||||
from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range
|
from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range
|
||||||
@@ -98,7 +102,8 @@ def update_actions_and_timeline_markers(context: Context, armature_objects: Iter
|
|||||||
for pose_marker_index, pose_marker in enumerate(pose_markers):
|
for pose_marker_index, pose_marker in enumerate(pose_markers):
|
||||||
if pose_marker.name.strip() == '' or pose_marker.name.startswith('#'):
|
if pose_marker.name.strip() == '' or pose_marker.name.startswith('#'):
|
||||||
continue
|
continue
|
||||||
for (name, frame_start, frame_end) in get_sequences_from_action_pose_markers(action, pose_markers, pose_marker, pose_marker_index):
|
sequences = get_sequences_from_action_pose_markers(action, pose_markers, pose_marker, pose_marker_index)
|
||||||
|
for (name, frame_start, frame_end) in sequences:
|
||||||
item = pg.action_list.add()
|
item = pg.action_list.add()
|
||||||
item.action = action
|
item.action = action
|
||||||
item.name = name
|
item.name = name
|
||||||
@@ -154,7 +159,11 @@ def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actio
|
|||||||
assert False, f'Invalid FPS source: {fps_source}'
|
assert False, f'Invalid FPS source: {fps_source}'
|
||||||
|
|
||||||
|
|
||||||
def get_sequence_compression_ratio(compression_ratio_source: str, compression_ratio_custom: float, actions: Iterable[Action]) -> float:
|
def get_sequence_compression_ratio(
|
||||||
|
compression_ratio_source: str,
|
||||||
|
compression_ratio_custom: float,
|
||||||
|
actions: Iterable[Action],
|
||||||
|
) -> float:
|
||||||
match compression_ratio_source:
|
match compression_ratio_source:
|
||||||
case 'ACTION_METADATA':
|
case 'ACTION_METADATA':
|
||||||
# Get the minimum value of action metadata compression ratio values.
|
# Get the minimum value of action metadata compression ratio values.
|
||||||
@@ -181,7 +190,11 @@ def get_animation_data_object(context: Context) -> Object:
|
|||||||
return animation_data_object
|
return animation_data_object
|
||||||
|
|
||||||
|
|
||||||
def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context: Context, marker_names: List[str]) -> Dict:
|
def get_timeline_marker_sequence_frame_ranges(
|
||||||
|
animation_data: AnimData,
|
||||||
|
context: Context,
|
||||||
|
marker_names: List[str],
|
||||||
|
) -> Dict:
|
||||||
# Timeline markers need to be sorted so that we can determine the sequence start and end positions.
|
# Timeline markers need to be sorted so that we can determine the sequence start and end positions.
|
||||||
sequence_frame_ranges = dict()
|
sequence_frame_ranges = dict()
|
||||||
sorted_timeline_markers = list(sorted(context.scene.timeline_markers, key=lambda x: x.frame))
|
sorted_timeline_markers = list(sorted(context.scene.timeline_markers, key=lambda x: x.frame))
|
||||||
@@ -239,7 +252,12 @@ def get_sequences_from_action(action: Action):
|
|||||||
yield from get_sequences_from_name_and_frame_range(action_name, frame_start, frame_end)
|
yield from get_sequences_from_name_and_frame_range(action_name, frame_start, frame_end)
|
||||||
|
|
||||||
|
|
||||||
def get_sequences_from_action_pose_markers(action: Action, pose_markers: List[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int):
|
def get_sequences_from_action_pose_markers(
|
||||||
|
action: Action,
|
||||||
|
pose_markers: List[TimelineMarker],
|
||||||
|
pose_marker: TimelineMarker,
|
||||||
|
pose_marker_index: int,
|
||||||
|
):
|
||||||
frame_start = pose_marker.frame
|
frame_start = pose_marker.frame
|
||||||
sequence_name = pose_marker.name
|
sequence_name = pose_marker.name
|
||||||
if pose_marker.name.startswith('!'):
|
if pose_marker.name.startswith('!'):
|
||||||
@@ -378,8 +396,10 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
row.operator(PSA_OT_export_bone_collections_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
|
row.operator(PSA_OT_export_bone_collections_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
|
||||||
row.operator(PSA_OT_export_bone_collections_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
|
row.operator(PSA_OT_export_bone_collections_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
|
||||||
rows = max(3, min(len(pg.bone_collection_list), 10))
|
rows = max(3, min(len(pg.bone_collection_list), 10))
|
||||||
bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
|
bones_panel.template_list(
|
||||||
rows=rows)
|
'PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
|
||||||
|
rows=rows
|
||||||
|
)
|
||||||
|
|
||||||
bones_advanced_header, bones_advanced_panel = layout.panel('Advanced', default_closed=False)
|
bones_advanced_header, bones_advanced_panel = layout.panel('Advanced', default_closed=False)
|
||||||
bones_advanced_header.label(text='Advanced')
|
bones_advanced_header.label(text='Advanced')
|
||||||
@@ -559,7 +579,7 @@ class PSA_OT_export_actions_select_all(Operator):
|
|||||||
case 'ACTIVE_ACTION':
|
case 'ACTIVE_ACTION':
|
||||||
return pg.active_action_list
|
return pg.active_action_list
|
||||||
case _:
|
case _:
|
||||||
return None
|
assert False, f'Invalid sequence source: {pg.sequence_source}'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from bpy.props import (
|
||||||
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
|
BoolProperty,
|
||||||
StringProperty
|
PointerProperty,
|
||||||
|
EnumProperty,
|
||||||
|
FloatProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
IntProperty,
|
||||||
|
StringProperty,
|
||||||
|
)
|
||||||
from bpy.types import PropertyGroup, Object, Action, AnimData, Context
|
from bpy.types import PropertyGroup, Object, Action, AnimData, Context
|
||||||
|
from ...shared.types import ForwardUpAxisMixin, ExportSpaceMixin, PsxBoneExportMixin
|
||||||
from ...shared.data import bone_filter_mode_items, ForwardUpAxisMixin, ExportSpaceMixin
|
|
||||||
from ...shared.types import PSX_PG_bone_collection_list_item
|
|
||||||
|
|
||||||
|
|
||||||
def psa_export_property_group_animation_data_override_poll(_context, obj):
|
def psa_export_property_group_animation_data_override_poll(_context, obj):
|
||||||
return obj.animation_data is not None
|
return obj.animation_data is not None
|
||||||
|
|
||||||
|
|
||||||
empty_set = set()
|
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_export_action_list_item(PropertyGroup):
|
class PSA_PG_export_action_list_item(PropertyGroup):
|
||||||
action: PointerProperty(type=Action)
|
action: PointerProperty(type=Action)
|
||||||
name: StringProperty()
|
name: StringProperty()
|
||||||
@@ -102,10 +103,10 @@ def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context):
|
|||||||
self.nla_track = ''
|
self.nla_track = ''
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin, PsxBoneExportMixin):
|
||||||
should_override_animation_data: BoolProperty(
|
should_override_animation_data: BoolProperty(
|
||||||
name='Override Animation Data',
|
name='Override Animation Data',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
default=False,
|
default=False,
|
||||||
description='Use the animation data from a different object instead of the selected object',
|
description='Use the animation data from a different object instead of the selected object',
|
||||||
update=animation_data_override_update_cb,
|
update=animation_data_override_update_cb,
|
||||||
@@ -117,7 +118,7 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
)
|
)
|
||||||
sequence_source: EnumProperty(
|
sequence_source: EnumProperty(
|
||||||
name='Source',
|
name='Source',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='',
|
description='',
|
||||||
items=(
|
items=(
|
||||||
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
|
('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0),
|
||||||
@@ -128,7 +129,7 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
)
|
)
|
||||||
nla_track: StringProperty(
|
nla_track: StringProperty(
|
||||||
name='NLA Track',
|
name='NLA Track',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='',
|
description='',
|
||||||
search=nla_track_search_cb,
|
search=nla_track_search_cb,
|
||||||
update=nla_track_update_cb
|
update=nla_track_update_cb
|
||||||
@@ -136,7 +137,7 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
nla_track_index: IntProperty(name='NLA Track Index', default=-1)
|
nla_track_index: IntProperty(name='NLA Track Index', default=-1)
|
||||||
fps_source: EnumProperty(
|
fps_source: EnumProperty(
|
||||||
name='FPS Source',
|
name='FPS Source',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='',
|
description='',
|
||||||
items=(
|
items=(
|
||||||
('SCENE', 'Scene', '', 'SCENE_DATA', 0),
|
('SCENE', 'Scene', '', 'SCENE_DATA', 0),
|
||||||
@@ -144,11 +145,11 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
('CUSTOM', 'Custom', '', 2)
|
('CUSTOM', 'Custom', '', 2)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=empty_set, step=100,
|
fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=set(), step=100,
|
||||||
soft_max=60.0)
|
soft_max=60.0)
|
||||||
compression_ratio_source: EnumProperty(
|
compression_ratio_source: EnumProperty(
|
||||||
name='Compression Ratio Source',
|
name='Compression Ratio Source',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='',
|
description='',
|
||||||
items=(
|
items=(
|
||||||
('ACTION_METADATA', 'Action Metadata', 'The compression ratio will be determined by action\'s Compression Ratio property found in the PSA Export panel.\n\nIf the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used', 'ACTION', 1),
|
('ACTION_METADATA', 'Action Metadata', 'The compression ratio will be determined by action\'s Compression Ratio property found in the PSA Export panel.\n\nIf the Sequence Source is Timeline Markers, the lowest value of all contributing actions will be used', 'ACTION', 1),
|
||||||
@@ -164,16 +165,8 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
nla_strip_list_index: IntProperty(default=0)
|
nla_strip_list_index: IntProperty(default=0)
|
||||||
active_action_list: CollectionProperty(type=PSA_PG_export_active_action_list_item)
|
active_action_list: CollectionProperty(type=PSA_PG_export_active_action_list_item)
|
||||||
active_action_list_index: IntProperty(default=0)
|
active_action_list_index: IntProperty(default=0)
|
||||||
bone_filter_mode: EnumProperty(
|
sequence_name_prefix: StringProperty(name='Prefix', options=set())
|
||||||
name='Bone Filter',
|
sequence_name_suffix: StringProperty(name='Suffix', options=set())
|
||||||
options=empty_set,
|
|
||||||
description='',
|
|
||||||
items=bone_filter_mode_items,
|
|
||||||
)
|
|
||||||
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
|
||||||
bone_collection_list_index: IntProperty(default=0, name='', description='')
|
|
||||||
sequence_name_prefix: StringProperty(name='Prefix', options=empty_set)
|
|
||||||
sequence_name_suffix: StringProperty(name='Suffix', options=empty_set)
|
|
||||||
sequence_filter_name: StringProperty(
|
sequence_filter_name: StringProperty(
|
||||||
default='',
|
default='',
|
||||||
name='Filter by Name',
|
name='Filter by Name',
|
||||||
@@ -182,21 +175,21 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
sequence_use_filter_invert: BoolProperty(
|
sequence_use_filter_invert: BoolProperty(
|
||||||
default=False,
|
default=False,
|
||||||
name='Invert',
|
name='Invert',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Invert filtering (show hidden items, and vice versa)')
|
description='Invert filtering (show hidden items, and vice versa)')
|
||||||
sequence_filter_asset: BoolProperty(
|
sequence_filter_asset: BoolProperty(
|
||||||
default=False,
|
default=False,
|
||||||
name='Show assets',
|
name='Show assets',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Show actions that belong to an asset library')
|
description='Show actions that belong to an asset library')
|
||||||
sequence_filter_pose_marker: BoolProperty(
|
sequence_filter_pose_marker: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
name='Show pose markers',
|
name='Show pose markers',
|
||||||
options=empty_set)
|
options=set())
|
||||||
sequence_use_filter_sort_reverse: BoolProperty(default=True, options=empty_set)
|
sequence_use_filter_sort_reverse: BoolProperty(default=True, options=set())
|
||||||
sequence_filter_reversed: BoolProperty(
|
sequence_filter_reversed: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
options=empty_set,
|
options=set(),
|
||||||
name='Show Reversed',
|
name='Show Reversed',
|
||||||
description='Show reversed sequences'
|
description='Show reversed sequences'
|
||||||
)
|
)
|
||||||
@@ -209,7 +202,7 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
)
|
)
|
||||||
sampling_mode: EnumProperty(
|
sampling_mode: EnumProperty(
|
||||||
name='Sampling Mode',
|
name='Sampling Mode',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='The method by which frames are sampled',
|
description='The method by which frames are sampled',
|
||||||
items=(
|
items=(
|
||||||
('INTERPOLATED', 'Interpolated', 'Sampling is performed by interpolating the evaluated bone poses from the adjacent whole frames.', 'INTERPOLATED', 0),
|
('INTERPOLATED', 'Interpolated', 'Sampling is performed by interpolating the evaluated bone poses from the adjacent whole frames.', 'INTERPOLATED', 0),
|
||||||
@@ -217,11 +210,6 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
),
|
),
|
||||||
default='INTERPOLATED'
|
default='INTERPOLATED'
|
||||||
)
|
)
|
||||||
root_bone_name: StringProperty(
|
|
||||||
name='Root Bone Name',
|
|
||||||
description='The name of the generated root bone when exporting multiple armatures',
|
|
||||||
default='ROOT',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import typing
|
from typing import cast as typing_cast
|
||||||
|
|
||||||
from bpy.types import UIList
|
from bpy.types import UIList
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class PSA_UL_export_sequences(UIList):
|
|||||||
self.use_filter_show = True
|
self.use_filter_show = True
|
||||||
|
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
item = typing.cast(PSA_PG_export_action_list_item, item)
|
item = typing_cast(PSA_PG_export_action_list_item, item)
|
||||||
|
|
||||||
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
|
is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker
|
||||||
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
|
layout.prop(item, 'is_selected', icon_only=True, text=item.name)
|
||||||
|
|||||||
@@ -2,20 +2,25 @@ import re
|
|||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from bpy.props import StringProperty, BoolProperty, CollectionProperty, IntProperty, PointerProperty, EnumProperty, \
|
from bpy.props import (
|
||||||
FloatProperty
|
BoolProperty,
|
||||||
|
CollectionProperty,
|
||||||
|
EnumProperty,
|
||||||
|
FloatProperty,
|
||||||
|
IntProperty,
|
||||||
|
PointerProperty,
|
||||||
|
StringProperty,
|
||||||
|
)
|
||||||
from bpy.types import PropertyGroup, Text
|
from bpy.types import PropertyGroup, Text
|
||||||
|
|
||||||
empty_set = set()
|
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_import_action_list_item(PropertyGroup):
|
class PSA_PG_import_action_list_item(PropertyGroup):
|
||||||
action_name: StringProperty(options=empty_set)
|
action_name: StringProperty(options=set())
|
||||||
is_selected: BoolProperty(default=True, options=empty_set)
|
is_selected: BoolProperty(default=True, options=set())
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_bone(PropertyGroup):
|
class PSA_PG_bone(PropertyGroup):
|
||||||
bone_name: StringProperty(options=empty_set)
|
bone_name: StringProperty(options=set())
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_data(PropertyGroup):
|
class PSA_PG_data(PropertyGroup):
|
||||||
@@ -43,31 +48,31 @@ class PsaImportMixin:
|
|||||||
should_use_fake_user: BoolProperty(default=True, name='Fake User',
|
should_use_fake_user: BoolProperty(default=True, name='Fake User',
|
||||||
description='Assign each imported action a fake user so that the data block is '
|
description='Assign each imported action a fake user so that the data block is '
|
||||||
'saved even it has no users',
|
'saved even it has no users',
|
||||||
options=empty_set)
|
options=set())
|
||||||
should_use_config_file: BoolProperty(default=True, name='Use Config File',
|
should_use_config_file: BoolProperty(default=True, name='Use Config File',
|
||||||
description='Use the .config file that is sometimes generated when the PSA '
|
description='Use the .config file that is sometimes generated when the PSA '
|
||||||
'file is exported from UEViewer. This file contains '
|
'file is exported from UEViewer. This file contains '
|
||||||
'options that can be used to filter out certain bones tracks '
|
'options that can be used to filter out certain bones tracks '
|
||||||
'from the imported actions',
|
'from the imported actions',
|
||||||
options=empty_set)
|
options=set())
|
||||||
should_stash: BoolProperty(default=False, name='Stash',
|
should_stash: BoolProperty(default=False, name='Stash',
|
||||||
description='Stash each imported action as a strip on a new non-contributing NLA track',
|
description='Stash each imported action as a strip on a new non-contributing NLA track',
|
||||||
options=empty_set)
|
options=set())
|
||||||
should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name', options=empty_set)
|
should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name', options=set())
|
||||||
action_name_prefix: StringProperty(default='', name='Prefix', options=empty_set)
|
action_name_prefix: StringProperty(default='', name='Prefix', options=set())
|
||||||
should_overwrite: BoolProperty(default=False, name='Overwrite', options=empty_set,
|
should_overwrite: BoolProperty(default=False, name='Overwrite', options=set(),
|
||||||
description='If an action with a matching name already exists, the existing action '
|
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')
|
'will have it\'s data overwritten instead of a new action being created')
|
||||||
should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=empty_set)
|
should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=set())
|
||||||
should_write_metadata: BoolProperty(default=True, name='Metadata', options=empty_set,
|
should_write_metadata: BoolProperty(default=True, name='Metadata', options=set(),
|
||||||
description='Additional data will be written to the custom properties of the '
|
description='Additional data will be written to the custom properties of the '
|
||||||
'Action (e.g., frame rate)')
|
'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=empty_set, name='Only Show Selected',
|
sequence_filter_is_selected: BoolProperty(default=False, options=set(), name='Only Show Selected',
|
||||||
description='Only show selected sequences')
|
description='Only show selected sequences')
|
||||||
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
|
sequence_use_filter_invert: BoolProperty(default=False, options=set())
|
||||||
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
||||||
description='Filter using regular expressions', options=empty_set)
|
description='Filter using regular expressions', options=set())
|
||||||
|
|
||||||
should_convert_to_samples: BoolProperty(
|
should_convert_to_samples: BoolProperty(
|
||||||
default=False,
|
default=False,
|
||||||
@@ -77,7 +82,7 @@ class PsaImportMixin:
|
|||||||
)
|
)
|
||||||
bone_mapping_mode: EnumProperty(
|
bone_mapping_mode: EnumProperty(
|
||||||
name='Bone Mapping',
|
name='Bone Mapping',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
||||||
items=bone_mapping_items,
|
items=bone_mapping_items,
|
||||||
default='CASE_INSENSITIVE'
|
default='CASE_INSENSITIVE'
|
||||||
@@ -87,7 +92,7 @@ class PsaImportMixin:
|
|||||||
default=30.0,
|
default=30.0,
|
||||||
name='Custom FPS',
|
name='Custom FPS',
|
||||||
description='The frame rate to which the imported sequences will be resampled to',
|
description='The frame rate to which the imported sequences will be resampled to',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
min=1.0,
|
min=1.0,
|
||||||
soft_min=1.0,
|
soft_min=1.0,
|
||||||
soft_max=60.0,
|
soft_max=60.0,
|
||||||
@@ -98,7 +103,7 @@ class PsaImportMixin:
|
|||||||
default=1.0,
|
default=1.0,
|
||||||
name='Custom Compression Ratio',
|
name='Custom Compression Ratio',
|
||||||
description='The compression ratio to apply to the imported sequences',
|
description='The compression ratio to apply to the imported sequences',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
min=0.0,
|
min=0.0,
|
||||||
soft_min=0.0,
|
soft_min=0.0,
|
||||||
soft_max=1.0,
|
soft_max=1.0,
|
||||||
@@ -119,11 +124,11 @@ class PSA_PG_import(PropertyGroup):
|
|||||||
sequence_list: CollectionProperty(type=PSA_PG_import_action_list_item)
|
sequence_list: CollectionProperty(type=PSA_PG_import_action_list_item)
|
||||||
sequence_list_index: IntProperty(name='', default=0)
|
sequence_list_index: IntProperty(name='', default=0)
|
||||||
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
|
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
|
||||||
sequence_filter_is_selected: BoolProperty(default=False, options=empty_set, name='Only Show Selected',
|
sequence_filter_is_selected: BoolProperty(default=False, options=set(), name='Only Show Selected',
|
||||||
description='Only show selected sequences')
|
description='Only show selected sequences')
|
||||||
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
|
sequence_use_filter_invert: BoolProperty(default=False, options=set())
|
||||||
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
||||||
description='Filter using regular expressions', options=empty_set)
|
description='Filter using regular expressions', options=set())
|
||||||
select_text: PointerProperty(type=Text)
|
select_text: PointerProperty(type=Text)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import typing
|
import typing
|
||||||
from typing import List, Optional
|
from typing import Iterable, List, Optional, cast as typing_cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Context, FCurve, Object
|
from bpy.types import Armature, Context, FCurve, Object
|
||||||
from mathutils import Vector, Quaternion
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
||||||
@@ -56,7 +56,7 @@ class ImportBone(object):
|
|||||||
self.fcurves: List[FCurve] = []
|
self.fcurves: List[FCurve] = []
|
||||||
|
|
||||||
|
|
||||||
def _calculate_fcurve_data(import_bone: ImportBone, key_data: typing.Iterable[float]):
|
def _calculate_fcurve_data(import_bone: ImportBone, key_data: Iterable[float]):
|
||||||
# Convert world-space transforms to local-space transforms.
|
# Convert world-space transforms to local-space transforms.
|
||||||
key_rotation = Quaternion(key_data[0:4])
|
key_rotation = Quaternion(key_data[0:4])
|
||||||
key_location = Vector(key_data[4:])
|
key_location = Vector(key_data[4:])
|
||||||
@@ -83,7 +83,7 @@ def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_name
|
|||||||
"""
|
"""
|
||||||
@param psa_bone_name: The name of the PSA bone.
|
@param psa_bone_name: The name of the PSA bone.
|
||||||
@param armature_bone_names: The names of the bones in the armature.
|
@param armature_bone_names: The names of the bones in the armature.
|
||||||
@param bone_mapping_mode: One of 'EXACT' or 'CASE_INSENSITIVE'.
|
@param bone_mapping_mode: One of `['EXACT', 'CASE_INSENSITIVE']`.
|
||||||
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
||||||
"""
|
"""
|
||||||
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
||||||
@@ -95,24 +95,27 @@ def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_name
|
|||||||
return armature_bone_index
|
return armature_bone_index
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_sample_frame_times(source_frame_count: int, frame_step: float) -> typing.Iterable[float]:
|
|
||||||
# TODO: for correctness, we should also emit the target frame time as well (because the last frame can be a
|
|
||||||
# fractional frame).
|
|
||||||
time = 0.0
|
|
||||||
while time < source_frame_count - 1:
|
|
||||||
yield time
|
|
||||||
time += frame_step
|
|
||||||
yield source_frame_count - 1
|
|
||||||
|
|
||||||
|
|
||||||
def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, frame_step: float = 1.0) -> np.ndarray:
|
def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, frame_step: float = 1.0) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Resamples the sequence data matrix to the target frame count.
|
Resamples the sequence data matrix to the target frame count.
|
||||||
|
|
||||||
@param sequence_data_matrix: FxBx7 matrix where F is the number of frames, B is the number of bones, and X is the
|
@param sequence_data_matrix: FxBx7 matrix where F is the number of frames, B is the number of bones, and X is the
|
||||||
number of data elements per bone.
|
number of data elements per bone.
|
||||||
@param frame_step: The step between frames in the resampled sequence.
|
@param frame_step: The step between frames in the resampled sequence.
|
||||||
@return: The resampled sequence data matrix, or sequence_data_matrix if no resampling is necessary.
|
@return: The resampled sequence data matrix, or sequence_data_matrix if no resampling is necessary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _get_sample_frame_times(source_frame_count: int, frame_step: float) -> Iterable[float]:
|
||||||
|
# TODO: for correctness, we should also emit the target frame time as well (because the last frame can be a
|
||||||
|
# fractional frame).
|
||||||
|
assert frame_step > 0.0, 'Frame step must be greater than 0'
|
||||||
|
time = 0.0
|
||||||
|
while time < source_frame_count - 1:
|
||||||
|
yield time
|
||||||
|
time += frame_step
|
||||||
|
yield source_frame_count - 1
|
||||||
|
|
||||||
if frame_step == 1.0:
|
if frame_step == 1.0:
|
||||||
# No resampling is necessary.
|
# No resampling is necessary.
|
||||||
return sequence_data_matrix
|
return sequence_data_matrix
|
||||||
@@ -145,7 +148,7 @@ def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, frame_step:
|
|||||||
def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, options: PsaImportOptions) -> PsaImportResult:
|
def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object, options: PsaImportOptions) -> PsaImportResult:
|
||||||
result = PsaImportResult()
|
result = PsaImportResult()
|
||||||
sequences = [psa_reader.sequences[x] for x in options.sequence_names]
|
sequences = [psa_reader.sequences[x] for x in options.sequence_names]
|
||||||
armature_data = typing.cast(bpy.types.Armature, armature_object.data)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
|
|
||||||
# Create an index mapping from bones in the PSA to bones in the target armature.
|
# Create an index mapping from bones in the PSA to bones in the target armature.
|
||||||
psa_to_armature_bone_indices = {}
|
psa_to_armature_bone_indices = {}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class PsaReader(object):
|
|||||||
def read_sequence_data_matrix(self, sequence_name: str) -> np.ndarray:
|
def read_sequence_data_matrix(self, sequence_name: str) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Reads and returns the data matrix for the given sequence.
|
Reads and returns the data matrix for the given sequence.
|
||||||
|
|
||||||
@param sequence_name: The name of the 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.
|
@return: An FxBx7 matrix where F is the number of frames, B is the number of bones.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import typing
|
|
||||||
from typing import Dict, Generator, Set, Iterable, Optional, cast, Tuple, List
|
|
||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Collection, Context, Object, Armature, Depsgraph
|
from bpy.types import Armature, Collection, Context, Depsgraph, Object
|
||||||
from mathutils import Matrix
|
from mathutils import Matrix
|
||||||
|
from typing import Dict, Generator, Iterable, List, Optional, Set, Tuple, cast as typing_cast
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
||||||
from ..shared.data import Vector3
|
from ..shared.data import Vector3
|
||||||
from ..shared.dfs import dfs_collection_objects, dfs_view_layer_objects, DfsObject
|
from ..shared.dfs import DfsObject, dfs_collection_objects, dfs_view_layer_objects
|
||||||
from ..shared.helpers import convert_string_to_cp1252_bytes, \
|
from ..shared.helpers import (
|
||||||
create_psx_bones, get_coordinate_system_transform
|
convert_string_to_cp1252_bytes,
|
||||||
|
create_psx_bones,
|
||||||
|
get_coordinate_system_transform,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PskInputObjects(object):
|
class PskInputObjects(object):
|
||||||
@@ -108,11 +108,11 @@ class PskBuildResult(object):
|
|||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
def _get_mesh_export_space_matrix(armature_object: Object, export_space: str) -> Matrix:
|
def _get_mesh_export_space_matrix(armature_object: Optional[Object], export_space: str) -> Matrix:
|
||||||
if armature_object is None:
|
if armature_object is None:
|
||||||
return Matrix.Identity(4)
|
return Matrix.Identity(4)
|
||||||
|
|
||||||
def get_object_space_space_matrix(obj: Object) -> Matrix:
|
def get_object_space_matrix(obj: Object) -> Matrix:
|
||||||
translation, rotation, _ = obj.matrix_world.decompose()
|
translation, rotation, _ = obj.matrix_world.decompose()
|
||||||
# We neutralize the scale here because the scale is already applied to the mesh objects implicitly.
|
# We neutralize the scale here because the scale is already applied to the mesh objects implicitly.
|
||||||
return Matrix.Translation(translation) @ rotation.to_matrix().to_4x4()
|
return Matrix.Translation(translation) @ rotation.to_matrix().to_4x4()
|
||||||
@@ -121,10 +121,10 @@ def _get_mesh_export_space_matrix(armature_object: Object, export_space: str) ->
|
|||||||
case 'WORLD':
|
case 'WORLD':
|
||||||
return Matrix.Identity(4)
|
return Matrix.Identity(4)
|
||||||
case 'ARMATURE':
|
case 'ARMATURE':
|
||||||
return get_object_space_space_matrix(armature_object).inverted()
|
return get_object_space_matrix(armature_object).inverted()
|
||||||
case 'ROOT':
|
case 'ROOT':
|
||||||
armature_data = cast(armature_object.data, Armature)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
armature_space_matrix = get_object_space_space_matrix(armature_object) @ armature_data.bones[0].matrix_local
|
armature_space_matrix = get_object_space_matrix(armature_object) @ armature_data.bones[0].matrix_local
|
||||||
return armature_space_matrix.inverted()
|
return armature_space_matrix.inverted()
|
||||||
case _:
|
case _:
|
||||||
assert False, f'Invalid export space: {export_space}'
|
assert False, f'Invalid export space: {export_space}'
|
||||||
@@ -200,15 +200,15 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
|
|
||||||
coordinate_system_matrix = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
coordinate_system_matrix = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
||||||
|
|
||||||
# TODO: we need to store this per armature
|
# Calculate the export spaces for the armature objects.
|
||||||
armature_mesh_export_space_matrices: Dict[Optional[Object], Matrix] = dict()
|
# This is used later to transform the mesh object geometry into the export space.
|
||||||
|
armature_mesh_export_space_matrices: Dict[Optional[Object], Matrix] = {None: Matrix.Identity(4)}
|
||||||
for armature_object in armature_objects:
|
for armature_object in armature_objects:
|
||||||
armature_mesh_export_space_matrices[armature_object] = _get_mesh_export_space_matrix(armature_object, options.export_space)
|
armature_mesh_export_space_matrices[armature_object] = _get_mesh_export_space_matrix(armature_object, options.export_space)
|
||||||
|
|
||||||
scale_matrix = Matrix.Scale(options.scale, 4)
|
scale_matrix = Matrix.Scale(options.scale, 4)
|
||||||
|
|
||||||
original_armature_object_pose_positions = {armature_object: armature_object.data.pose_position for armature_object in armature_objects}
|
original_armature_object_pose_positions = {a: a.data.pose_position for a in armature_objects}
|
||||||
|
|
||||||
# Temporarily force the armature into the rest position.
|
# Temporarily force the armature into the rest position.
|
||||||
# We will undo this later.
|
# We will undo this later.
|
||||||
@@ -218,7 +218,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
material_names = [m.name for m in materials]
|
material_names = [m.name for m in materials]
|
||||||
|
|
||||||
for object_index, input_mesh_object in enumerate(input_objects.mesh_dfs_objects):
|
for object_index, input_mesh_object in enumerate(input_objects.mesh_dfs_objects):
|
||||||
obj, instance_objects, matrix_world = input_mesh_object.obj, input_mesh_object.instance_objects, input_mesh_object.matrix_world
|
obj, matrix_world = input_mesh_object.obj, input_mesh_object.matrix_world
|
||||||
|
|
||||||
armature_object = get_armature_for_mesh_object(obj)
|
armature_object = get_armature_for_mesh_object(obj)
|
||||||
|
|
||||||
@@ -272,7 +272,8 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
# the normals will be correct. We can detect this by checking if the number of negative scaling axes is
|
# the normals will be correct. We can detect this by checking if the number of negative scaling axes is
|
||||||
# odd. If it is, we need to invert the normals of the mesh by swapping the order of the vertices in each
|
# odd. If it is, we need to invert the normals of the mesh by swapping the order of the vertices in each
|
||||||
# face.
|
# face.
|
||||||
should_flip_normals = sum(1 for x in scale if x < 0) % 2 == 1
|
if not should_flip_normals:
|
||||||
|
should_flip_normals = sum(1 for x in scale if x < 0) % 2 == 1
|
||||||
|
|
||||||
# Copy the vertex groups
|
# Copy the vertex groups
|
||||||
for vertex_group in obj.vertex_groups:
|
for vertex_group in obj.vertex_groups:
|
||||||
@@ -285,6 +286,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
point_transform_matrix = vertex_transform_matrix @ mesh_object.matrix_world
|
point_transform_matrix = vertex_transform_matrix @ mesh_object.matrix_world
|
||||||
|
|
||||||
# Vertices
|
# Vertices
|
||||||
|
vertex_offset = len(psk.points)
|
||||||
for vertex in mesh_data.vertices:
|
for vertex in mesh_data.vertices:
|
||||||
point = Vector3()
|
point = Vector3()
|
||||||
v = point_transform_matrix @ vertex.co
|
v = point_transform_matrix @ vertex.co
|
||||||
@@ -298,8 +300,6 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
# Wedges
|
# Wedges
|
||||||
mesh_data.calc_loop_triangles()
|
mesh_data.calc_loop_triangles()
|
||||||
|
|
||||||
vertex_offset = len(psk.points)
|
|
||||||
|
|
||||||
# Build a list of non-unique wedges.
|
# Build a list of non-unique wedges.
|
||||||
wedges = []
|
wedges = []
|
||||||
for loop_index, loop in enumerate(mesh_data.loops):
|
for loop_index, loop in enumerate(mesh_data.loops):
|
||||||
@@ -346,7 +346,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
|
|
||||||
# Weights
|
# Weights
|
||||||
if armature_object is not None:
|
if armature_object is not None:
|
||||||
armature_data = typing.cast(Armature, armature_object.data)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
bone_index_offset = psx_bone_create_result.armature_object_root_bone_indices[armature_object]
|
bone_index_offset = psx_bone_create_result.armature_object_root_bone_indices[armature_object]
|
||||||
# Because the vertex groups may contain entries for which there is no matching bone in the armature,
|
# Because the vertex groups may contain entries for which there is no matching bone in the armature,
|
||||||
# we must filter them out and not export any weights for these vertex groups.
|
# we must filter them out and not export any weights for these vertex groups.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, List, Optional, cast
|
from typing import Iterable, List, Optional, cast as typing_cast
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import BoolProperty, StringProperty
|
from bpy.props import BoolProperty, StringProperty
|
||||||
@@ -7,8 +7,13 @@ from bpy.types import Collection, Context, Depsgraph, Material, Object, Operator
|
|||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
from .properties import PskExportMixin
|
from .properties import PskExportMixin
|
||||||
from ..builder import PskBuildOptions, build_psk, get_psk_input_objects_for_context, \
|
from ..builder import (
|
||||||
get_psk_input_objects_for_collection, get_materials_for_mesh_objects
|
PskBuildOptions,
|
||||||
|
build_psk,
|
||||||
|
get_materials_for_mesh_objects,
|
||||||
|
get_psk_input_objects_for_collection,
|
||||||
|
get_psk_input_objects_for_context,
|
||||||
|
)
|
||||||
from ..writer import write_psk
|
from ..writer import write_psk
|
||||||
from ...shared.helpers import populate_bone_collection_list
|
from ...shared.helpers import populate_bone_collection_list
|
||||||
from ...shared.ui import draw_bone_filter_mode
|
from ...shared.ui import draw_bone_filter_mode
|
||||||
@@ -34,10 +39,10 @@ def get_collection_from_context(context: Context) -> Optional[Collection]:
|
|||||||
if context.space_data.type != 'PROPERTIES':
|
if context.space_data.type != 'PROPERTIES':
|
||||||
return None
|
return None
|
||||||
|
|
||||||
space_data = cast(SpaceProperties, context.space_data)
|
space_data = typing_cast(SpaceProperties, context.space_data)
|
||||||
|
|
||||||
if space_data.use_pin_id:
|
if space_data.use_pin_id:
|
||||||
return cast(Collection, space_data.pin_id)
|
return typing_cast(Collection, space_data.pin_id)
|
||||||
else:
|
else:
|
||||||
return context.collection
|
return context.collection
|
||||||
|
|
||||||
@@ -226,13 +231,11 @@ class PSK_OT_material_list_name_move_down(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
empty_set = set()
|
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_materials_by_names(materials: Iterable[Material], material_names: List[str]) -> List[Material]:
|
def get_sorted_materials_by_names(materials: Iterable[Material], material_names: List[str]) -> List[Material]:
|
||||||
"""
|
"""
|
||||||
Sorts the materials by the order of the material names list. Any materials not in the list will be appended to the
|
Sorts the materials by the order of the material names list. Any materials not in the list will be appended to the
|
||||||
end of the list in the order they are found.
|
end of the list in the order they are found.
|
||||||
|
|
||||||
@param materials: A list of materials to sort
|
@param materials: A list of materials to sort
|
||||||
@param material_names: A list of material names to sort by
|
@param material_names: A list of material names to sort by
|
||||||
@return: A sorted list of materials
|
@return: A sorted list of materials
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProperty, FloatProperty, StringProperty, \
|
from bpy.props import (
|
||||||
BoolProperty
|
BoolProperty,
|
||||||
from bpy.types import PropertyGroup, Material
|
CollectionProperty,
|
||||||
|
EnumProperty,
|
||||||
from ...shared.data import bone_filter_mode_items, ExportSpaceMixin, ForwardUpAxisMixin
|
FloatProperty,
|
||||||
from ...shared.types import PSX_PG_bone_collection_list_item
|
IntProperty,
|
||||||
|
PointerProperty,
|
||||||
empty_set = set()
|
StringProperty,
|
||||||
|
)
|
||||||
|
from bpy.types import Material, PropertyGroup
|
||||||
|
from ...shared.types import ExportSpaceMixin, ForwardUpAxisMixin, PsxBoneExportMixin
|
||||||
|
|
||||||
object_eval_state_items = (
|
object_eval_state_items = (
|
||||||
('EVALUATED', 'Evaluated', 'Use data from fully evaluated object'),
|
('EVALUATED', 'Evaluated', 'Use data from fully evaluated object'),
|
||||||
@@ -26,7 +29,7 @@ class PSK_PG_material_name_list_item(PropertyGroup):
|
|||||||
index: IntProperty()
|
index: IntProperty()
|
||||||
|
|
||||||
|
|
||||||
class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin):
|
class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin, PsxBoneExportMixin):
|
||||||
object_eval_state: EnumProperty(
|
object_eval_state: EnumProperty(
|
||||||
items=object_eval_state_items,
|
items=object_eval_state_items,
|
||||||
name='Object Evaluation State',
|
name='Object Evaluation State',
|
||||||
@@ -44,14 +47,6 @@ class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin):
|
|||||||
min=0.0001,
|
min=0.0001,
|
||||||
soft_max=100.0
|
soft_max=100.0
|
||||||
)
|
)
|
||||||
bone_filter_mode: EnumProperty(
|
|
||||||
name='Bone Filter',
|
|
||||||
options=empty_set,
|
|
||||||
description='',
|
|
||||||
items=bone_filter_mode_items,
|
|
||||||
)
|
|
||||||
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
|
||||||
bone_collection_list_index: IntProperty(default=0)
|
|
||||||
material_order_mode: EnumProperty(
|
material_order_mode: EnumProperty(
|
||||||
name='Material Order',
|
name='Material Order',
|
||||||
description='The order in which to export the materials',
|
description='The order in which to export the materials',
|
||||||
@@ -60,11 +55,6 @@ class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin):
|
|||||||
)
|
)
|
||||||
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
|
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
|
||||||
material_name_list_index: IntProperty(default=0)
|
material_name_list_index: IntProperty(default=0)
|
||||||
root_bone_name: StringProperty(
|
|
||||||
name='Root Bone Name',
|
|
||||||
description='The name of the generated root bone when exporting multiple armatures',
|
|
||||||
default='ROOT',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PSK_PG_export(PropertyGroup, PskExportMixin):
|
class PSK_PG_export(PropertyGroup, PskExportMixin):
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bpy.props import StringProperty, CollectionProperty
|
from bpy.props import CollectionProperty, StringProperty
|
||||||
from bpy.types import Operator, FileHandler, Context, OperatorFileListElement, UILayout
|
from bpy.types import Context, FileHandler, Operator, OperatorFileListElement, UILayout
|
||||||
from bpy_extras.io_utils import ImportHelper
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
from ..importer import PskImportOptions, import_psk
|
from ..importer import PskImportOptions, import_psk
|
||||||
from ..properties import PskImportMixin
|
from ..properties import PskImportMixin
|
||||||
from ..reader import read_psk
|
from ..reader import read_psk
|
||||||
|
|
||||||
empty_set = set()
|
|
||||||
|
|
||||||
def get_psk_import_options_from_properties(property_group: PskImportMixin):
|
def get_psk_import_options_from_properties(property_group: PskImportMixin):
|
||||||
options = PskImportOptions()
|
options = PskImportOptions()
|
||||||
options.should_import_mesh = property_group.should_import_mesh
|
options.should_import_mesh = property_group.should_import_mesh
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import VertexGroup, Context, Object
|
|
||||||
from mathutils import Quaternion, Vector, Matrix
|
from bpy.types import Context, Object, VertexGroup
|
||||||
|
from mathutils import Matrix, Quaternion, Vector
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
from .properties import poly_flags_to_triangle_type_and_bit_flags
|
from .properties import poly_flags_to_triangle_type_and_bit_flags
|
||||||
from ..shared.data import PsxBone
|
from ..shared.data import PsxBone
|
||||||
from ..shared.helpers import rgb_to_srgb, is_bdk_addon_loaded
|
from ..shared.helpers import is_bdk_addon_loaded, rgb_to_srgb
|
||||||
|
|
||||||
|
|
||||||
class PskImportOptions:
|
class PskImportOptions:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from bpy.props import EnumProperty, BoolProperty, FloatProperty, StringProperty
|
from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
|
|
||||||
mesh_triangle_types_items = (
|
mesh_triangle_types_items = (
|
||||||
@@ -44,8 +44,6 @@ def poly_flags_to_triangle_type_and_bit_flags(poly_flags: int) -> (str, set[str]
|
|||||||
triangle_bit_flags = {item[0] for item in mesh_triangle_bit_flags_items if item[3] & poly_flags}
|
triangle_bit_flags = {item[0] for item in mesh_triangle_bit_flags_items if item[3] & poly_flags}
|
||||||
return triangle_type, triangle_bit_flags
|
return triangle_type, triangle_bit_flags
|
||||||
|
|
||||||
empty_set = set()
|
|
||||||
|
|
||||||
|
|
||||||
def should_import_mesh_get(self):
|
def should_import_mesh_get(self):
|
||||||
return self.import_components in {'ALL', 'MESH'}
|
return self.import_components in {'ALL', 'MESH'}
|
||||||
@@ -57,13 +55,13 @@ def should_import_skleton_get(self):
|
|||||||
class PskImportMixin:
|
class PskImportMixin:
|
||||||
should_import_vertex_colors: BoolProperty(
|
should_import_vertex_colors: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
options=empty_set,
|
options=set(),
|
||||||
name='Import Vertex Colors',
|
name='Import Vertex Colors',
|
||||||
description='Import vertex colors, if available'
|
description='Import vertex colors, if available'
|
||||||
)
|
)
|
||||||
vertex_color_space: EnumProperty(
|
vertex_color_space: EnumProperty(
|
||||||
name='Vertex Color Space',
|
name='Vertex Color Space',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='The source vertex color space',
|
description='The source vertex color space',
|
||||||
default='SRGBA',
|
default='SRGBA',
|
||||||
items=(
|
items=(
|
||||||
@@ -74,18 +72,18 @@ class PskImportMixin:
|
|||||||
should_import_vertex_normals: BoolProperty(
|
should_import_vertex_normals: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
name='Import Vertex Normals',
|
name='Import Vertex Normals',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Import vertex normals, if available'
|
description='Import vertex normals, if available'
|
||||||
)
|
)
|
||||||
should_import_extra_uvs: BoolProperty(
|
should_import_extra_uvs: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
name='Import Extra UVs',
|
name='Import Extra UVs',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Import extra UV maps, if available'
|
description='Import extra UV maps, if available'
|
||||||
)
|
)
|
||||||
import_components: EnumProperty(
|
import_components: EnumProperty(
|
||||||
name='Import Components',
|
name='Import Components',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Determine which components to import',
|
description='Determine which components to import',
|
||||||
items=(
|
items=(
|
||||||
('ALL', 'Mesh & Skeleton', 'Import mesh and skeleton'),
|
('ALL', 'Mesh & Skeleton', 'Import mesh and skeleton'),
|
||||||
@@ -101,7 +99,7 @@ class PskImportMixin:
|
|||||||
should_import_materials: BoolProperty(
|
should_import_materials: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
name='Import Materials',
|
name='Import Materials',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
)
|
)
|
||||||
should_import_skeleton: BoolProperty(
|
should_import_skeleton: BoolProperty(
|
||||||
name='Import Skeleton',
|
name='Import Skeleton',
|
||||||
@@ -113,14 +111,14 @@ class PskImportMixin:
|
|||||||
step=100,
|
step=100,
|
||||||
soft_min=1.0,
|
soft_min=1.0,
|
||||||
name='Bone Length',
|
name='Bone Length',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
subtype='DISTANCE',
|
subtype='DISTANCE',
|
||||||
description='Length of the bones'
|
description='Length of the bones'
|
||||||
)
|
)
|
||||||
should_import_shape_keys: BoolProperty(
|
should_import_shape_keys: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
name='Import Shape Keys',
|
name='Import Shape Keys',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='Import shape keys, if available'
|
description='Import shape keys, if available'
|
||||||
)
|
)
|
||||||
scale: FloatProperty(
|
scale: FloatProperty(
|
||||||
@@ -131,7 +129,7 @@ class PskImportMixin:
|
|||||||
bdk_repository_id: StringProperty(
|
bdk_repository_id: StringProperty(
|
||||||
name='BDK Repository ID',
|
name='BDK Repository ID',
|
||||||
default='',
|
default='',
|
||||||
options=empty_set,
|
options=set(),
|
||||||
description='The ID of the BDK repository to use for loading materials'
|
description='The ID of the BDK repository to use for loading materials'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import re
|
|||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..shared.data import Section
|
from ..shared.data import Section
|
||||||
from .data import Color, Psk, PsxBone, Vector2, Vector3
|
from .data import Color, Psk, PsxBone, Vector2, Vector3
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from ctypes import Structure, sizeof
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
from ..shared.data import Section, Vector3, PsxBone
|
from ..shared.data import PsxBone, Section, Vector3
|
||||||
|
|
||||||
MAX_WEDGE_COUNT = 65536
|
MAX_WEDGE_COUNT = 65536
|
||||||
MAX_POINT_COUNT = 4294967296
|
MAX_POINT_COUNT = 4294967296
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from ctypes import Structure, c_char, c_int32, c_float, c_ubyte
|
from ctypes import Structure, c_char, c_int32, c_float, c_ubyte
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from bpy.props import EnumProperty
|
|
||||||
from mathutils import Quaternion as BpyQuaternion
|
from mathutils import Quaternion as BpyQuaternion
|
||||||
|
|
||||||
|
|
||||||
@@ -118,69 +116,3 @@ class Section(Structure):
|
|||||||
def __init__(self, *args, **kw):
|
def __init__(self, *args, **kw):
|
||||||
super().__init__(*args, **kw)
|
super().__init__(*args, **kw)
|
||||||
self.type_flags = 1999801
|
self.type_flags = 1999801
|
||||||
|
|
||||||
|
|
||||||
bone_filter_mode_items = (
|
|
||||||
('ALL', 'All', 'All bones will be exported'),
|
|
||||||
('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ancestors will be exported')
|
|
||||||
)
|
|
||||||
|
|
||||||
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
|
|
||||||
forward_items = (
|
|
||||||
('X', 'X Forward', ''),
|
|
||||||
('Y', 'Y Forward', ''),
|
|
||||||
('Z', 'Z Forward', ''),
|
|
||||||
('-X', '-X Forward', ''),
|
|
||||||
('-Y', '-Y Forward', ''),
|
|
||||||
('-Z', '-Z Forward', ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
up_items = (
|
|
||||||
('X', 'X Up', ''),
|
|
||||||
('Y', 'Y Up', ''),
|
|
||||||
('Z', 'Z Up', ''),
|
|
||||||
('-X', '-X Up', ''),
|
|
||||||
('-Y', '-Y Up', ''),
|
|
||||||
('-Z', '-Z Up', ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def forward_axis_update(self, _context):
|
|
||||||
if self.forward_axis == self.up_axis:
|
|
||||||
# Automatically set the up axis to the next available axis
|
|
||||||
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
|
|
||||||
|
|
||||||
|
|
||||||
def up_axis_update(self, _context):
|
|
||||||
if self.up_axis == self.forward_axis:
|
|
||||||
# Automatically set the forward axis to the next available axis
|
|
||||||
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
|
|
||||||
|
|
||||||
|
|
||||||
class ForwardUpAxisMixin:
|
|
||||||
forward_axis: EnumProperty(
|
|
||||||
name='Forward',
|
|
||||||
items=forward_items,
|
|
||||||
default='X',
|
|
||||||
update=forward_axis_update
|
|
||||||
)
|
|
||||||
up_axis: EnumProperty(
|
|
||||||
name='Up',
|
|
||||||
items=up_items,
|
|
||||||
default='Z',
|
|
||||||
update=up_axis_update
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export_space_items = [
|
|
||||||
('WORLD', 'World', 'Export in world space'),
|
|
||||||
('ARMATURE', 'Armature', 'Export the local space of the armature object'),
|
|
||||||
('ROOT', 'Root', 'Export in the space of the root bone')
|
|
||||||
]
|
|
||||||
|
|
||||||
class ExportSpaceMixin:
|
|
||||||
export_space: EnumProperty(
|
|
||||||
name='Export Space',
|
|
||||||
items=export_space_items,
|
|
||||||
default='WORLD'
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ from mathutils import Matrix
|
|||||||
|
|
||||||
|
|
||||||
class DfsObject:
|
class DfsObject:
|
||||||
'''
|
"""
|
||||||
Represents an object in a depth-first search.
|
Represents an object in a depth-first search.
|
||||||
'''
|
"""
|
||||||
def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix):
|
def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix):
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.instance_objects = instance_objects
|
self.instance_objects = instance_objects
|
||||||
@@ -22,10 +22,11 @@ class DfsObject:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_visible(self) -> bool:
|
def is_visible(self) -> bool:
|
||||||
'''
|
"""
|
||||||
Check if the object is visible.
|
Check if the object is visible.
|
||||||
|
|
||||||
@return: True if the object is visible, False otherwise.
|
@return: True if the object is visible, False otherwise.
|
||||||
'''
|
"""
|
||||||
if self.instance_objects:
|
if self.instance_objects:
|
||||||
return self.instance_objects[-1].visible_get()
|
return self.instance_objects[-1].visible_get()
|
||||||
return self.obj.visible_get()
|
return self.obj.visible_get()
|
||||||
@@ -41,11 +42,11 @@ class DfsObject:
|
|||||||
return self.obj.select_get()
|
return self.obj.select_get()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _dfs_object_children(obj: Object, collection: Collection) -> Iterable[Object]:
|
def _dfs_object_children(obj: Object, collection: Collection) -> Iterable[Object]:
|
||||||
'''
|
'''
|
||||||
Construct a list of objects in hierarchy order from `collection.objects`, only keeping those that are in the
|
Construct a list of objects in hierarchy order from `collection.objects`, only keeping those that are in the
|
||||||
collection.
|
collection.
|
||||||
|
|
||||||
@param obj: The object to start the search from.
|
@param obj: The object to start the search from.
|
||||||
@param collection: The collection to search in.
|
@param collection: The collection to search in.
|
||||||
@return: An iterable of objects in hierarchy order.
|
@return: An iterable of objects in hierarchy order.
|
||||||
@@ -60,6 +61,7 @@ def dfs_objects_in_collection(collection: Collection) -> Iterable[Object]:
|
|||||||
'''
|
'''
|
||||||
Returns a depth-first iterator over all objects in a collection, only keeping those that are directly in the
|
Returns a depth-first iterator over all objects in a collection, only keeping those that are directly in the
|
||||||
collection.
|
collection.
|
||||||
|
|
||||||
@param collection: The collection to search in.
|
@param collection: The collection to search in.
|
||||||
@return: An iterable of objects in hierarchy order.
|
@return: An iterable of objects in hierarchy order.
|
||||||
'''
|
'''
|
||||||
@@ -74,6 +76,7 @@ def dfs_objects_in_collection(collection: Collection) -> Iterable[Object]:
|
|||||||
def dfs_collection_objects(collection: Collection, visible_only: bool = False) -> Iterable[DfsObject]:
|
def dfs_collection_objects(collection: Collection, visible_only: bool = False) -> Iterable[DfsObject]:
|
||||||
'''
|
'''
|
||||||
Depth-first search of objects in a collection, including recursing into instances.
|
Depth-first search of objects in a collection, including recursing into instances.
|
||||||
|
|
||||||
@param collection: The collection to search in.
|
@param collection: The collection to search in.
|
||||||
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
||||||
'''
|
'''
|
||||||
@@ -89,6 +92,7 @@ def _dfs_collection_objects_recursive(
|
|||||||
'''
|
'''
|
||||||
Depth-first search of objects in a collection, including recursing into instances.
|
Depth-first search of objects in a collection, including recursing into instances.
|
||||||
This is a recursive function.
|
This is a recursive function.
|
||||||
|
|
||||||
@param collection: The collection to search in.
|
@param collection: The collection to search in.
|
||||||
@param instance_objects: The running hierarchy of instance objects.
|
@param instance_objects: The running hierarchy of instance objects.
|
||||||
@param matrix_world: The world matrix of the current object.
|
@param matrix_world: The world matrix of the current object.
|
||||||
@@ -130,6 +134,7 @@ def _dfs_collection_objects_recursive(
|
|||||||
def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
|
def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
|
||||||
'''
|
'''
|
||||||
Depth-first iterator over all objects in a view layer, including recursing into instances.
|
Depth-first iterator over all objects in a view layer, including recursing into instances.
|
||||||
|
|
||||||
@param view_layer: The view layer to inspect.
|
@param view_layer: The view layer to inspect.
|
||||||
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
@return: An iterable of tuples containing the object, the instance objects, and the world matrix.
|
||||||
'''
|
'''
|
||||||
@@ -146,6 +151,7 @@ def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]:
|
|||||||
def _is_dfs_object_visible(obj: Object, instance_objects: List[Object]) -> bool:
|
def _is_dfs_object_visible(obj: Object, instance_objects: List[Object]) -> bool:
|
||||||
'''
|
'''
|
||||||
Check if a DFS object is visible.
|
Check if a DFS object is visible.
|
||||||
|
|
||||||
@param obj: The object.
|
@param obj: The object.
|
||||||
@param instance_objects: The instance objects.
|
@param instance_objects: The instance objects.
|
||||||
@return: True if the object is visible, False otherwise.
|
@return: True if the object is visible, False otherwise.
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
from collections import Counter
|
|
||||||
from typing import List, Iterable, cast, Optional, Dict, Tuple
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
from collections import Counter
|
||||||
|
from typing import List, Iterable, Optional, Dict, Tuple, cast as typing_cast
|
||||||
from bpy.props import CollectionProperty
|
from bpy.props import CollectionProperty
|
||||||
from bpy.types import AnimData, Object
|
from bpy.types import Armature, AnimData, Object
|
||||||
from bpy.types import Armature
|
|
||||||
from mathutils import Matrix, Vector
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
from .data import Vector3, Quaternion
|
from .data import Vector3, Quaternion
|
||||||
from ..shared.data import PsxBone
|
from ..shared.data import PsxBone
|
||||||
|
|
||||||
@@ -57,7 +54,7 @@ def populate_bone_collection_list(armature_objects: Iterable[Object], bone_colle
|
|||||||
bone_collection_list.clear()
|
bone_collection_list.clear()
|
||||||
|
|
||||||
for armature_object in armature_objects:
|
for armature_object in armature_objects:
|
||||||
armature = cast(Armature, armature_object.data)
|
armature = typing_cast(Armature, armature_object.data)
|
||||||
|
|
||||||
if armature is None:
|
if armature is None:
|
||||||
return
|
return
|
||||||
@@ -85,15 +82,15 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_c
|
|||||||
|
|
||||||
Note that the ancestors of bones within the bone collections will also be present in the returned list.
|
Note that the ancestors of bones within the bone collections will also be present in the returned list.
|
||||||
|
|
||||||
:param armature_object: Blender object with type 'ARMATURE'
|
:param armature_object: Blender object with type `'ARMATURE'`
|
||||||
:param bone_filter_mode: One of ['ALL', 'BONE_COLLECTIONS']
|
:param bone_filter_mode: One of `['ALL', 'BONE_COLLECTIONS']`
|
||||||
:param bone_collection_indices: A list of bone collection indices to export.
|
:param bone_collection_indices: A list of bone collection indices to export.
|
||||||
:return: A sorted list of bone indices that should be exported.
|
:return: A sorted list of bone indices that should be exported.
|
||||||
"""
|
"""
|
||||||
if armature_object is None or armature_object.type != 'ARMATURE':
|
if armature_object is None or armature_object.type != 'ARMATURE':
|
||||||
raise ValueError('An armature object must be supplied')
|
raise ValueError('An armature object must be supplied')
|
||||||
|
|
||||||
armature_data = cast(Armature, armature_object.data)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
bones = armature_data.bones
|
bones = armature_data.bones
|
||||||
bone_names = [x.name for x in bones]
|
bone_names = [x.name for x in bones]
|
||||||
|
|
||||||
@@ -265,26 +262,6 @@ def create_psx_bones_from_blender_bones(
|
|||||||
return psx_bones
|
return psx_bones
|
||||||
|
|
||||||
|
|
||||||
# TODO: we need two different ones for the PSK and PSA.
|
|
||||||
# TODO: Figure out in what "space" the root bone is in for PSA animations.
|
|
||||||
# Maybe make a set of space-switching functions to make this easier to follow and figure out.
|
|
||||||
def get_export_space_matrix(export_space: str, armature_object: Optional[Object] = None) -> Matrix:
|
|
||||||
match export_space:
|
|
||||||
case 'WORLD':
|
|
||||||
return Matrix.Identity(4)
|
|
||||||
case 'ARMATURE':
|
|
||||||
# We do not care about the scale when dealing with export spaces, only the translation and rotation.
|
|
||||||
if armature_object is not None:
|
|
||||||
translation, rotation, _ = armature_object.matrix_world.decompose()
|
|
||||||
return (rotation.to_matrix().to_4x4() @ Matrix.Translation(translation)).inverted()
|
|
||||||
else:
|
|
||||||
return Matrix.Identity(4)
|
|
||||||
case 'ROOT':
|
|
||||||
pass
|
|
||||||
case _:
|
|
||||||
assert False, f'Invalid export space: {export_space}'
|
|
||||||
|
|
||||||
|
|
||||||
class PsxBoneCreateResult:
|
class PsxBoneCreateResult:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
bones: List[Tuple[PsxBone, Optional[Object]]], # List of tuples of (psx_bone, armature_object)
|
bones: List[Tuple[PsxBone, Optional[Object]]], # List of tuples of (psx_bone, armature_object)
|
||||||
@@ -324,9 +301,6 @@ def create_psx_bones(
|
|||||||
|
|
||||||
total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects)
|
total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects)
|
||||||
|
|
||||||
# Store the index of the root bone for each armature object.
|
|
||||||
# We will need this later to correctly assign vertex weights.
|
|
||||||
armature_object_root_bone_indices = dict()
|
|
||||||
|
|
||||||
# Store the bone names to be exported for each armature object.
|
# Store the bone names to be exported for each armature object.
|
||||||
armature_object_bone_names: Dict[Object, List[str]] = dict()
|
armature_object_bone_names: Dict[Object, List[str]] = dict()
|
||||||
@@ -335,6 +309,10 @@ def create_psx_bones(
|
|||||||
bone_names = get_export_bone_names(armature_object, bone_filter_mode, armature_bone_collection_indices)
|
bone_names = get_export_bone_names(armature_object, bone_filter_mode, armature_bone_collection_indices)
|
||||||
armature_object_bone_names[armature_object] = bone_names
|
armature_object_bone_names[armature_object] = bone_names
|
||||||
|
|
||||||
|
# Store the index of the root bone for each armature object.
|
||||||
|
# We will need this later to correctly assign vertex weights.
|
||||||
|
armature_object_root_bone_indices: Dict[Optional[Object], int] = dict()
|
||||||
|
|
||||||
if len(armature_objects) == 0 or total_bone_count == 0:
|
if len(armature_objects) == 0 or total_bone_count == 0:
|
||||||
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
|
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
|
||||||
# requirement that a PSK file must have at least one bone.
|
# requirement that a PSK file must have at least one bone.
|
||||||
@@ -366,7 +344,7 @@ def create_psx_bones(
|
|||||||
|
|
||||||
for armature_object in armature_objects:
|
for armature_object in armature_objects:
|
||||||
bone_names = armature_object_bone_names[armature_object]
|
bone_names = armature_object_bone_names[armature_object]
|
||||||
armature_data = cast(Armature, armature_object.data)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
armature_bones = [armature_data.bones[bone_name] for bone_name in bone_names]
|
armature_bones = [armature_data.bones[bone_name] for bone_name in bone_names]
|
||||||
|
|
||||||
armature_psx_bones = create_psx_bones_from_blender_bones(
|
armature_psx_bones = create_psx_bones_from_blender_bones(
|
||||||
@@ -379,7 +357,8 @@ def create_psx_bones(
|
|||||||
root_bone=root_bone,
|
root_bone=root_bone,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If we are appending these bones to an existing list of bones, we need to adjust the parent indices.
|
# If we are appending these bones to an existing list of bones, we need to adjust the parent indices for
|
||||||
|
# all the non-root bones.
|
||||||
if len(bones) > 0:
|
if len(bones) > 0:
|
||||||
parent_index_offset = len(bones)
|
parent_index_offset = len(bones)
|
||||||
for bone in armature_psx_bones[1:]:
|
for bone in armature_psx_bones[1:]:
|
||||||
@@ -393,7 +372,13 @@ def create_psx_bones(
|
|||||||
bone_name_counts = Counter(bone[0].name.decode('windows-1252').upper() for bone in bones)
|
bone_name_counts = Counter(bone[0].name.decode('windows-1252').upper() for bone in bones)
|
||||||
for bone_name, count in bone_name_counts.items():
|
for bone_name, count in bone_name_counts.items():
|
||||||
if count > 1:
|
if count > 1:
|
||||||
raise RuntimeError(f'Found {count} bones with the name "{bone_name}". Bone names must be unique when compared case-insensitively.')
|
error_message = f'Found {count} bones with the name "{bone_name}". '
|
||||||
|
f'Bone names must be unique when compared case-insensitively.'
|
||||||
|
|
||||||
|
if len(armature_objects) > 1 and bone_name == root_bone_name.upper():
|
||||||
|
error_message += f' This is the name of the automatically generated root bone. Consider changing this '
|
||||||
|
f''
|
||||||
|
raise RuntimeError(error_message)
|
||||||
|
|
||||||
return PsxBoneCreateResult(
|
return PsxBoneCreateResult(
|
||||||
bones=bones,
|
bones=bones,
|
||||||
@@ -416,6 +401,8 @@ def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
|
|||||||
return Vector((0.0, -1.0, 0.0))
|
return Vector((0.0, -1.0, 0.0))
|
||||||
case '-Z':
|
case '-Z':
|
||||||
return Vector((0.0, 0.0, -1.0))
|
return Vector((0.0, 0.0, -1.0))
|
||||||
|
case _:
|
||||||
|
assert False, f'Invalid axis identifier: {axis_identifier}'
|
||||||
|
|
||||||
|
|
||||||
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
|
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.props import StringProperty, IntProperty, BoolProperty, FloatProperty
|
from bpy.props import CollectionProperty, EnumProperty, StringProperty, IntProperty, BoolProperty, FloatProperty
|
||||||
from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Panel
|
from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType, Panel
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +56,90 @@ class PSX_PT_action(Panel):
|
|||||||
flow.prop(action.psa_export, 'fps')
|
flow.prop(action.psa_export, 'fps')
|
||||||
|
|
||||||
|
|
||||||
|
bone_filter_mode_items = (
|
||||||
|
('ALL', 'All', 'All bones will be exported'),
|
||||||
|
('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ancestors will be exported')
|
||||||
|
)
|
||||||
|
|
||||||
|
axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z')
|
||||||
|
forward_items = (
|
||||||
|
('X', 'X Forward', ''),
|
||||||
|
('Y', 'Y Forward', ''),
|
||||||
|
('Z', 'Z Forward', ''),
|
||||||
|
('-X', '-X Forward', ''),
|
||||||
|
('-Y', '-Y Forward', ''),
|
||||||
|
('-Z', '-Z Forward', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
up_items = (
|
||||||
|
('X', 'X Up', ''),
|
||||||
|
('Y', 'Y Up', ''),
|
||||||
|
('Z', 'Z Up', ''),
|
||||||
|
('-X', '-X Up', ''),
|
||||||
|
('-Y', '-Y Up', ''),
|
||||||
|
('-Z', '-Z Up', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def forward_axis_update(self, _context):
|
||||||
|
if self.forward_axis == self.up_axis:
|
||||||
|
# Automatically set the up axis to the next available axis
|
||||||
|
self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z')
|
||||||
|
|
||||||
|
|
||||||
|
def up_axis_update(self, _context):
|
||||||
|
if self.up_axis == self.forward_axis:
|
||||||
|
# Automatically set the forward axis to the next available axis
|
||||||
|
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardUpAxisMixin:
|
||||||
|
forward_axis: EnumProperty(
|
||||||
|
name='Forward',
|
||||||
|
items=forward_items,
|
||||||
|
default='X',
|
||||||
|
update=forward_axis_update
|
||||||
|
)
|
||||||
|
up_axis: EnumProperty(
|
||||||
|
name='Up',
|
||||||
|
items=up_items,
|
||||||
|
default='Z',
|
||||||
|
update=up_axis_update
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export_space_items = [
|
||||||
|
('WORLD', 'World', 'Export in world space'),
|
||||||
|
('ARMATURE', 'Armature', 'Export the local space of the armature object'),
|
||||||
|
('ROOT', 'Root', 'Export in the space of the root bone')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExportSpaceMixin:
|
||||||
|
export_space: EnumProperty(
|
||||||
|
name='Export Space',
|
||||||
|
items=export_space_items,
|
||||||
|
default='WORLD'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PsxBoneExportMixin:
|
||||||
|
bone_filter_mode: EnumProperty(
|
||||||
|
name='Bone Filter',
|
||||||
|
options=set(),
|
||||||
|
description='',
|
||||||
|
items=bone_filter_mode_items,
|
||||||
|
)
|
||||||
|
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
||||||
|
bone_collection_list_index: IntProperty(default=0, name='', description='')
|
||||||
|
root_bone_name: StringProperty(
|
||||||
|
name='Root Bone Name',
|
||||||
|
description='The name of the generated root bone when exporting multiple armatures',
|
||||||
|
default='ROOT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PSX_PG_action_export,
|
PSX_PG_action_export,
|
||||||
PSX_PG_bone_collection_list_item,
|
PSX_PG_bone_collection_list_item,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from bpy.types import UILayout
|
from bpy.types import UILayout
|
||||||
|
|
||||||
from .data import bone_filter_mode_items
|
from .types import bone_filter_mode_items
|
||||||
|
|
||||||
|
|
||||||
def is_bone_filter_mode_item_available(pg, identifier):
|
def is_bone_filter_mode_item_available(pg, identifier):
|
||||||
|
|||||||
Reference in New Issue
Block a user