Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37f7cc4d9f | ||
|
|
93083f09f8 | ||
|
|
75660f9dc1 | ||
|
|
5421ac5151 | ||
|
|
9dcbb74058 | ||
|
|
8ed985263c | ||
|
|
d91408ecab | ||
|
|
dd1ea683bb | ||
|
|
240b79d374 | ||
|
|
33e7862288 |
@@ -1,6 +1,41 @@
|
|||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
|
|
||||||
if 'bpy' in locals():
|
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 .psk import (
|
||||||
|
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.export import (
|
||||||
|
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 .psa import (
|
||||||
|
config as psa_config,
|
||||||
|
data as psa_data,
|
||||||
|
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 ui as psa_import_ui, properties as psa_import_properties
|
||||||
|
|
||||||
|
_needs_reload = 'bpy' in locals()
|
||||||
|
|
||||||
|
if _needs_reload:
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
importlib.reload(shared_data)
|
importlib.reload(shared_data)
|
||||||
@@ -33,58 +68,10 @@ if 'bpy' in locals():
|
|||||||
importlib.reload(psa_import_properties)
|
importlib.reload(psa_import_properties)
|
||||||
importlib.reload(psa_import_operators)
|
importlib.reload(psa_import_operators)
|
||||||
importlib.reload(psa_import_ui)
|
importlib.reload(psa_import_ui)
|
||||||
else:
|
|
||||||
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 .psk import (
|
|
||||||
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.export import (
|
|
||||||
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 .psa import (
|
|
||||||
config as psa_config,
|
|
||||||
data as psa_data,
|
|
||||||
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 ui as psa_import_ui, properties as psa_import_properties
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import PointerProperty
|
from bpy.props import PointerProperty
|
||||||
|
|
||||||
classes = shared_types.classes + \
|
|
||||||
shared_ui.classes + \
|
|
||||||
psk_properties.classes + \
|
|
||||||
psk_ui.classes + \
|
|
||||||
psk_import_operators.classes + \
|
|
||||||
psk_export_properties.classes + \
|
|
||||||
psk_export_operators.classes + \
|
|
||||||
psk_export_ui.classes + \
|
|
||||||
psa_export_properties.classes + \
|
|
||||||
psa_export_operators.classes + \
|
|
||||||
psa_export_ui.classes + \
|
|
||||||
psa_import_properties.classes + \
|
|
||||||
psa_import_operators.classes + \
|
|
||||||
psa_import_ui.classes
|
|
||||||
|
|
||||||
|
|
||||||
def psk_export_menu_func(self, context):
|
def psk_export_menu_func(self, context):
|
||||||
self.layout.operator(psk_export_operators.PSK_OT_export.bl_idname, text='Unreal PSK (.psk)')
|
self.layout.operator(psk_export_operators.PSK_OT_export.bl_idname, text='Unreal PSK (.psk)')
|
||||||
@@ -102,9 +89,26 @@ def psa_import_menu_func(self, context):
|
|||||||
self.layout.operator(psa_import_operators.PSA_OT_import.bl_idname, text='Unreal PSA (.psa)')
|
self.layout.operator(psa_import_operators.PSA_OT_import.bl_idname, text='Unreal PSA (.psa)')
|
||||||
|
|
||||||
|
|
||||||
|
_modules = (
|
||||||
|
shared_types,
|
||||||
|
shared_ui,
|
||||||
|
psk_properties,
|
||||||
|
psk_ui,
|
||||||
|
psk_import_operators,
|
||||||
|
psk_export_properties,
|
||||||
|
psk_export_operators,
|
||||||
|
psk_export_ui,
|
||||||
|
psa_export_properties,
|
||||||
|
psa_export_operators,
|
||||||
|
psa_export_ui,
|
||||||
|
psa_import_properties,
|
||||||
|
psa_import_operators,
|
||||||
|
psa_import_ui
|
||||||
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes:
|
for module in _modules:
|
||||||
bpy.utils.register_class(cls)
|
module.register()
|
||||||
bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func)
|
bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func)
|
||||||
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
|
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
|
||||||
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
|
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
|
||||||
@@ -128,8 +132,8 @@ def unregister():
|
|||||||
bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func)
|
bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func)
|
||||||
bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func)
|
bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func)
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func)
|
bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func)
|
||||||
for cls in reversed(classes):
|
for module in reversed(_modules):
|
||||||
bpy.utils.unregister_class(cls)
|
module.unregister()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
id = "io_scene_psk_psa"
|
id = "io_scene_psk_psa"
|
||||||
version = "8.2.1"
|
version = "8.2.4"
|
||||||
name = "Unreal PSK/PSA (.psk/.psa)"
|
name = "Unreal PSK/PSA (.psk/.psa)"
|
||||||
tagline = "Import and export PSK and PSA files used in Unreal Engine"
|
tagline = "Import and export PSK and PSA files used in Unreal Engine"
|
||||||
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
maintainer = "Colin Basnett <cmbasnett@gmail.com>"
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ def _get_pose_bone_location_and_rotation(
|
|||||||
|
|
||||||
|
|
||||||
def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
||||||
|
|
||||||
|
assert context.scene
|
||||||
|
assert context.window_manager
|
||||||
|
|
||||||
psa = Psa()
|
psa = Psa()
|
||||||
|
|
||||||
armature_objects_for_bones = options.armature_objects
|
armature_objects_for_bones = options.armature_objects
|
||||||
@@ -224,6 +228,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
|||||||
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0))))
|
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0))))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
assert armature_object.pose
|
||||||
pose_bone = armature_object.pose.bones[psx_bone.name.decode('windows-1252')]
|
pose_bone = armature_object.pose.bones[psx_bone.name.decode('windows-1252')]
|
||||||
|
|
||||||
export_bones.append(PsaExportBone(pose_bone, armature_object, armature_scales[armature_object]))
|
export_bones.append(PsaExportBone(pose_bone, armature_object, armature_scales[armature_object]))
|
||||||
@@ -321,6 +326,7 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa:
|
|||||||
|
|
||||||
# Restore the previous actions & frame.
|
# Restore the previous actions & frame.
|
||||||
for armature_object, action in saved_armature_object_actions.items():
|
for armature_object, action in saved_armature_object_actions.items():
|
||||||
|
assert armature_object.animation_data
|
||||||
armature_object.animation_data.action = action
|
armature_object.animation_data.action = action
|
||||||
|
|
||||||
context.scene.frame_set(saved_frame_current)
|
context.scene.frame_set(saved_frame_current)
|
||||||
|
|||||||
@@ -461,6 +461,9 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
if animation_data is None:
|
if animation_data is None:
|
||||||
raise RuntimeError(f'No animation data for object \'{animation_data_object.name}\'')
|
raise RuntimeError(f'No animation data for object \'{animation_data_object.name}\'')
|
||||||
|
|
||||||
|
if context.active_object is None:
|
||||||
|
raise RuntimeError('No active object')
|
||||||
|
|
||||||
export_sequences: List[PsaBuildSequence] = []
|
export_sequences: List[PsaBuildSequence] = []
|
||||||
|
|
||||||
@@ -658,10 +661,14 @@ class PSA_OT_export_bone_collections_deselect_all(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_OT_export,
|
PSA_OT_export,
|
||||||
PSA_OT_export_actions_select_all,
|
PSA_OT_export_actions_select_all,
|
||||||
PSA_OT_export_actions_deselect_all,
|
PSA_OT_export_actions_deselect_all,
|
||||||
PSA_OT_export_bone_collections_select_all,
|
PSA_OT_export_bone_collections_select_all,
|
||||||
PSA_OT_export_bone_collections_deselect_all,
|
PSA_OT_export_bone_collections_deselect_all,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ sampling_mode_items = (
|
|||||||
|
|
||||||
def sequence_source_update_cb(self: 'PSA_PG_export', context: Context) -> None:
|
def sequence_source_update_cb(self: 'PSA_PG_export', context: Context) -> None:
|
||||||
armature_objects = []
|
armature_objects = []
|
||||||
|
assert context.view_layer
|
||||||
for dfs_object in dfs_view_layer_objects(context.view_layer):
|
for dfs_object in dfs_view_layer_objects(context.view_layer):
|
||||||
if dfs_object.obj.type == 'ARMATURE' and dfs_object.is_selected:
|
if dfs_object.obj.type == 'ARMATURE' and dfs_object.is_selected:
|
||||||
armature_objects.append(dfs_object.obj)
|
armature_objects.append(dfs_object.obj)
|
||||||
@@ -262,10 +263,14 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
|||||||
return flt_flags
|
return flt_flags
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_PG_export_action_list_item,
|
PSA_PG_export_action_list_item,
|
||||||
PSA_PG_export_timeline_markers,
|
PSA_PG_export_timeline_markers,
|
||||||
PSA_PG_export_nla_strip_list_item,
|
PSA_PG_export_nla_strip_list_item,
|
||||||
PSA_PG_export_active_action_list_item,
|
PSA_PG_export_active_action_list_item,
|
||||||
PSA_PG_export,
|
PSA_PG_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ class PSA_UL_export_sequences(UIList):
|
|||||||
return flt_flags, flt_neworder
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_UL_export_sequences,
|
PSA_UL_export_sequences,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ from bpy_extras.io_utils import ImportHelper
|
|||||||
|
|
||||||
from .properties import PsaImportMixin, get_visible_sequences
|
from .properties import PsaImportMixin, get_visible_sequences
|
||||||
from ..config import read_psa_config
|
from ..config import read_psa_config
|
||||||
from ..importer import PsaImportOptions, import_psa
|
from ..importer import BoneMapping, PsaImportOptions, import_psa
|
||||||
from ..reader import PsaReader
|
from ..reader import PsaReader
|
||||||
|
|
||||||
|
|
||||||
def psa_import_poll(cls, context: Context):
|
def psa_import_poll(cls, context: Context):
|
||||||
|
assert context.view_layer and context.view_layer.objects.active
|
||||||
active_object = context.view_layer.objects.active
|
active_object = context.view_layer.objects.active
|
||||||
if active_object is None or active_object.type != 'ARMATURE':
|
if active_object is None or active_object.type != 'ARMATURE':
|
||||||
cls.poll_message_set('The active object must be an armature')
|
cls.poll_message_set('The active object must be an armature')
|
||||||
@@ -32,10 +33,12 @@ class PSA_OT_import_sequences_select_from_text(Operator):
|
|||||||
return len(pg.sequence_list) > 0
|
return len(pg.sequence_list) > 0
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
assert context.window_manager
|
||||||
return context.window_manager.invoke_props_dialog(self, width=256)
|
return context.window_manager.invoke_props_dialog(self, width=256)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
assert layout
|
||||||
pg = getattr(context.scene, 'psa_import')
|
pg = getattr(context.scene, 'psa_import')
|
||||||
layout.label(icon='INFO', text='Each sequence name should be on a new line.')
|
layout.label(icon='INFO', text='Each sequence name should be on a new line.')
|
||||||
layout.prop(pg, 'select_text', text='')
|
layout.prop(pg, 'select_text', text='')
|
||||||
@@ -134,6 +137,8 @@ class PSA_OT_import_drag_and_drop(Operator, PsaImportMixin):
|
|||||||
warnings = []
|
warnings = []
|
||||||
sequences_count = 0
|
sequences_count = 0
|
||||||
|
|
||||||
|
assert context.view_layer and context.view_layer.objects.active
|
||||||
|
|
||||||
for file in self.files:
|
for file in self.files:
|
||||||
psa_path = str(os.path.join(self.directory, file.name))
|
psa_path = str(os.path.join(self.directory, file.name))
|
||||||
psa_reader = PsaReader(psa_path)
|
psa_reader = PsaReader(psa_path)
|
||||||
@@ -157,12 +162,14 @@ class PSA_OT_import_drag_and_drop(Operator, PsaImportMixin):
|
|||||||
|
|
||||||
def invoke(self, context: Context, event):
|
def invoke(self, context: Context, event):
|
||||||
# Make sure the selected object is an obj.
|
# Make sure the selected object is an obj.
|
||||||
|
assert context.view_layer and context.view_layer.objects.active
|
||||||
active_object = context.view_layer.objects.active
|
active_object = context.view_layer.objects.active
|
||||||
if active_object is None or active_object.type != 'ARMATURE':
|
if active_object is None or active_object.type != 'ARMATURE':
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'The active object must be an armature')
|
self.report({'ERROR_INVALID_CONTEXT'}, 'The active object must be an armature')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Show the import operator properties in a pop-up dialog (do not use the file selector).
|
# Show the import operator properties in a pop-up dialog (do not use the file selector).
|
||||||
|
assert context.window_manager
|
||||||
context.window_manager.invoke_props_dialog(self)
|
context.window_manager.invoke_props_dialog(self)
|
||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
@@ -181,7 +188,10 @@ def psa_import_options_from_property_group(pg: PsaImportMixin, sequence_names: I
|
|||||||
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
|
||||||
options.should_convert_to_samples = pg.should_convert_to_samples
|
options.should_convert_to_samples = pg.should_convert_to_samples
|
||||||
options.bone_mapping_mode = pg.bone_mapping_mode
|
options.bone_mapping = BoneMapping(
|
||||||
|
is_case_sensitive=pg.bone_mapping_is_case_sensitive,
|
||||||
|
should_ignore_trailing_whitespace=pg.bone_mapping_should_ignore_trailing_whitespace
|
||||||
|
)
|
||||||
options.fps_source = pg.fps_source
|
options.fps_source = pg.fps_source
|
||||||
options.fps_custom = pg.fps_custom
|
options.fps_custom = pg.fps_custom
|
||||||
options.translation_scale = pg.translation_scale
|
options.translation_scale = pg.translation_scale
|
||||||
@@ -236,7 +246,10 @@ class PSA_OT_import_all(Operator, PsaImportMixin):
|
|||||||
|
|
||||||
options = PsaImportOptions(
|
options = PsaImportOptions(
|
||||||
action_name_prefix=self.action_name_prefix,
|
action_name_prefix=self.action_name_prefix,
|
||||||
bone_mapping_mode=self.bone_mapping_mode,
|
bone_mapping=BoneMapping(
|
||||||
|
is_case_sensitive=self.bone_mapping_is_case_sensitive,
|
||||||
|
should_ignore_trailing_whitespace=self.bone_mapping_should_ignore_trailing_whitespace
|
||||||
|
),
|
||||||
fps_custom=self.fps_custom,
|
fps_custom=self.fps_custom,
|
||||||
fps_source=self.fps_source,
|
fps_source=self.fps_source,
|
||||||
sequence_names=sequence_names,
|
sequence_names=sequence_names,
|
||||||
@@ -250,6 +263,8 @@ class PSA_OT_import_all(Operator, PsaImportMixin):
|
|||||||
translation_scale=self.translation_scale
|
translation_scale=self.translation_scale
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert context.view_layer
|
||||||
|
assert context.view_layer.objects.active
|
||||||
result = _import_psa(context, options, self.filepath, context.view_layer.objects.active)
|
result = _import_psa(context, options, self.filepath, context.view_layer.objects.active)
|
||||||
|
|
||||||
if len(result.warnings) > 0:
|
if len(result.warnings) > 0:
|
||||||
@@ -308,12 +323,13 @@ class PSA_OT_import(Operator, ImportHelper, PsaImportMixin):
|
|||||||
def invoke(self, context: Context, event: Event):
|
def invoke(self, context: Context, event: Event):
|
||||||
# Attempt to load the PSA file for the pre-selected file.
|
# Attempt to load the PSA file for the pre-selected file.
|
||||||
load_psa_file(context, self.filepath)
|
load_psa_file(context, self.filepath)
|
||||||
|
assert context.window_manager
|
||||||
context.window_manager.fileselect_add(self)
|
context.window_manager.fileselect_add(self)
|
||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
def draw(self, context: Context):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
assert layout
|
||||||
pg = getattr(context.scene, 'psa_import')
|
pg = getattr(context.scene, 'psa_import')
|
||||||
|
|
||||||
sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False)
|
sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False)
|
||||||
@@ -370,10 +386,14 @@ class PSA_OT_import(Operator, ImportHelper, PsaImportMixin):
|
|||||||
advanced_header.label(text='Advanced')
|
advanced_header.label(text='Advanced')
|
||||||
|
|
||||||
if advanced_panel:
|
if advanced_panel:
|
||||||
col = advanced_panel.column()
|
bone_mapping_header, bone_mapping_panel = layout.panel('bone_mapping_id', default_closed=False)
|
||||||
col.use_property_split = True
|
bone_mapping_header.label(text='Bone Mapping')
|
||||||
col.use_property_decorate = False
|
if bone_mapping_panel:
|
||||||
col.prop(self, 'bone_mapping_mode')
|
col = bone_mapping_panel.column()
|
||||||
|
col.use_property_split = True
|
||||||
|
col.use_property_decorate = False
|
||||||
|
col.prop(self, 'bone_mapping_is_case_sensitive')
|
||||||
|
col.prop(self, 'bone_mapping_should_ignore_trailing_whitespace')
|
||||||
|
|
||||||
col = advanced_panel.column()
|
col = advanced_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
@@ -412,10 +432,15 @@ def draw_psa_import_options_no_panels(layout, pg: PsaImportMixin):
|
|||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'should_convert_to_samples')
|
col.prop(pg, 'should_convert_to_samples')
|
||||||
|
|
||||||
|
col = layout.column(heading='Bone Mapping')
|
||||||
|
col.use_property_split = True
|
||||||
|
col.use_property_decorate = False
|
||||||
|
col.prop(pg, 'bone_mapping_is_case_sensitive')
|
||||||
|
col.prop(pg, 'bone_mapping_should_ignore_trailing_whitespace')
|
||||||
|
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'bone_mapping_mode')
|
|
||||||
col.prop(pg, 'translation_scale')
|
col.prop(pg, 'translation_scale')
|
||||||
|
|
||||||
col = layout.column(heading='Options')
|
col = layout.column(heading='Options')
|
||||||
@@ -434,11 +459,11 @@ class PSA_FH_import(FileHandler): # TODO: rename and add handling for PSA expor
|
|||||||
bl_file_extensions = '.psa'
|
bl_file_extensions = '.psa'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll_drop(cls, context: Context):
|
def poll_drop(cls, context: Context) -> bool:
|
||||||
return context.area and context.area.type == 'VIEW_3D'
|
return context.area is not None and context.area.type == 'VIEW_3D'
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_OT_import_sequences_select_all,
|
PSA_OT_import_sequences_select_all,
|
||||||
PSA_OT_import_sequences_deselect_all,
|
PSA_OT_import_sequences_deselect_all,
|
||||||
PSA_OT_import_sequences_select_from_text,
|
PSA_OT_import_sequences_select_from_text,
|
||||||
@@ -447,3 +472,6 @@ classes = (
|
|||||||
PSA_OT_import_drag_and_drop,
|
PSA_OT_import_drag_and_drop,
|
||||||
PSA_FH_import,
|
PSA_FH_import,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ class PsaImportMixin:
|
|||||||
description='Convert keyframes to read-only samples. '
|
description='Convert keyframes to read-only samples. '
|
||||||
'Recommended if you do not plan on editing the actions directly'
|
'Recommended if you do not plan on editing the actions directly'
|
||||||
)
|
)
|
||||||
bone_mapping_mode: EnumProperty(
|
bone_mapping_is_case_sensitive: BoolProperty(
|
||||||
name='Bone Mapping',
|
default=False,
|
||||||
options=set(),
|
name='Case Sensitive'
|
||||||
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
)
|
||||||
items=bone_mapping_items,
|
bone_mapping_should_ignore_trailing_whitespace: BoolProperty(
|
||||||
default='CASE_INSENSITIVE'
|
default=True,
|
||||||
|
name='Ignore Trailing Whitespace'
|
||||||
)
|
)
|
||||||
fps_source: EnumProperty(name='FPS Source', items=fps_source_items)
|
fps_source: EnumProperty(name='FPS Source', items=fps_source_items)
|
||||||
fps_custom: FloatProperty(
|
fps_custom: FloatProperty(
|
||||||
@@ -175,9 +176,12 @@ def get_visible_sequences(pg: PSA_PG_import, sequences) -> List[PSA_PG_import_ac
|
|||||||
return visible_sequences
|
return visible_sequences
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_PG_import_action_list_item,
|
PSA_PG_import_action_list_item,
|
||||||
PSA_PG_bone,
|
PSA_PG_bone,
|
||||||
PSA_PG_data,
|
PSA_PG_data,
|
||||||
PSA_PG_import,
|
PSA_PG_import,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ class PSA_UL_import_actions(PSA_UL_sequences_mixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSA_UL_sequences,
|
PSA_UL_sequences,
|
||||||
PSA_UL_import_sequences,
|
PSA_UL_import_sequences,
|
||||||
PSA_UL_import_actions,
|
PSA_UL_import_actions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from typing import Sequence, Iterable, List, Optional, cast as typing_cast
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import re
|
||||||
from bpy.types import Armature, Context, FCurve, Object, Bone, PoseBone
|
from bpy.types import Armature, Context, FCurve, Object, Bone, PoseBone
|
||||||
from mathutils import Vector, Quaternion
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
@@ -9,11 +10,22 @@ from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
|||||||
from .reader import PsaReader
|
from .reader import PsaReader
|
||||||
from ..shared.data import PsxBone
|
from ..shared.data import PsxBone
|
||||||
|
|
||||||
|
class BoneMapping:
|
||||||
|
def __init__(self,
|
||||||
|
is_case_sensitive: bool = False,
|
||||||
|
should_ignore_trailing_whitespace: bool = True
|
||||||
|
):
|
||||||
|
self.is_case_sensitive = is_case_sensitive
|
||||||
|
# Ancient PSK and PSA exporters would, for some reason, pad the bone names with spaces
|
||||||
|
# instead of just writing null bytes, probably because the programmers were lazy.
|
||||||
|
# By default, we will ignore trailing whitespace when doing comparisons.
|
||||||
|
self.should_ignore_trailing_whitespace = should_ignore_trailing_whitespace
|
||||||
|
|
||||||
|
|
||||||
class PsaImportOptions(object):
|
class PsaImportOptions(object):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
action_name_prefix: str = '',
|
action_name_prefix: str = '',
|
||||||
bone_mapping_mode: str = 'CASE_INSENSITIVE',
|
bone_mapping: BoneMapping = BoneMapping(),
|
||||||
fps_custom: float = 30.0,
|
fps_custom: float = 30.0,
|
||||||
fps_source: str = 'SEQUENCE',
|
fps_source: str = 'SEQUENCE',
|
||||||
psa_config: PsaConfig = PsaConfig(),
|
psa_config: PsaConfig = PsaConfig(),
|
||||||
@@ -28,7 +40,7 @@ class PsaImportOptions(object):
|
|||||||
translation_scale: float = 1.0
|
translation_scale: float = 1.0
|
||||||
):
|
):
|
||||||
self.action_name_prefix = action_name_prefix
|
self.action_name_prefix = action_name_prefix
|
||||||
self.bone_mapping_mode = bone_mapping_mode
|
self.bone_mapping = bone_mapping
|
||||||
self.fps_custom = fps_custom
|
self.fps_custom = fps_custom
|
||||||
self.fps_source = fps_source
|
self.fps_source = fps_source
|
||||||
self.psa_config = psa_config
|
self.psa_config = psa_config
|
||||||
@@ -78,20 +90,30 @@ class PsaImportResult:
|
|||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]:
|
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping: BoneMapping) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
@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', 'CASE_INSENSITIVE']`.
|
@param bone_mapping: Bone mapping information.
|
||||||
@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.
|
||||||
"""
|
"""
|
||||||
|
# Use regular expressions for bone name matching.
|
||||||
|
pattern = psa_bone_name
|
||||||
|
flags = 0
|
||||||
|
|
||||||
|
if bone_mapping.should_ignore_trailing_whitespace:
|
||||||
|
psa_bone_name = psa_bone_name.rstrip()
|
||||||
|
pattern += r'\s*'
|
||||||
|
|
||||||
|
if not bone_mapping.is_case_sensitive:
|
||||||
|
flags = re.IGNORECASE
|
||||||
|
|
||||||
|
pattern = re.compile(pattern, flags)
|
||||||
|
|
||||||
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
||||||
if bone_mapping_mode == 'CASE_INSENSITIVE':
|
if re.fullmatch(pattern, armature_bone_name):
|
||||||
if armature_bone_name.lower() == psa_bone_name.lower():
|
return armature_bone_index
|
||||||
return armature_bone_index
|
|
||||||
else:
|
|
||||||
if armature_bone_name == psa_bone_name:
|
|
||||||
return armature_bone_index
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -145,6 +167,9 @@ 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:
|
||||||
|
|
||||||
|
assert context.window_manager
|
||||||
|
|
||||||
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(Armature, armature_object.data)
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
@@ -158,7 +183,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|||||||
|
|
||||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||||
psa_bone_name: str = psa_bone.name.decode('windows-1252')
|
psa_bone_name: str = psa_bone.name.decode('windows-1252')
|
||||||
armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping_mode)
|
armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping)
|
||||||
if armature_bone_index is not None:
|
if armature_bone_index is not None:
|
||||||
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
||||||
if armature_bone_index not in armature_to_psa_bone_indices:
|
if armature_bone_index not in armature_to_psa_bone_indices:
|
||||||
@@ -259,6 +284,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|||||||
case 'CUSTOM':
|
case 'CUSTOM':
|
||||||
target_fps = options.fps_custom
|
target_fps = options.fps_custom
|
||||||
case 'SCENE':
|
case 'SCENE':
|
||||||
|
assert context.scene
|
||||||
target_fps = context.scene.render.fps
|
target_fps = context.scene.render.fps
|
||||||
case 'SEQUENCE':
|
case 'SEQUENCE':
|
||||||
target_fps = sequence.fps
|
target_fps = sequence.fps
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Armature, Collection, Context, Depsgraph, Object, ArmatureModifier
|
from bpy.types import Armature, Collection, Context, Depsgraph, Object, ArmatureModifier, Mesh
|
||||||
from mathutils import Matrix
|
from mathutils import Matrix
|
||||||
from typing import Dict, Iterable, List, Optional, Set, cast as typing_cast
|
from typing import Dict, Iterable, List, Optional, Set, cast as typing_cast
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
@@ -94,9 +94,9 @@ def get_psk_input_objects_for_collection(collection: Collection) -> PskInputObje
|
|||||||
|
|
||||||
|
|
||||||
class PskBuildResult(object):
|
class PskBuildResult(object):
|
||||||
def __init__(self):
|
def __init__(self, psk: Psk, warnings: list[str]):
|
||||||
self.psk = None
|
self.psk: Psk = psk
|
||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = warnings
|
||||||
|
|
||||||
|
|
||||||
def _get_mesh_export_space_matrix(armature_object: Optional[Object], export_space: str) -> Matrix:
|
def _get_mesh_export_space_matrix(armature_object: Optional[Object], export_space: str) -> Matrix:
|
||||||
@@ -137,9 +137,12 @@ def _get_material_name_indices(obj: Object, material_names: List[str]) -> Iterab
|
|||||||
|
|
||||||
|
|
||||||
def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
||||||
|
|
||||||
|
assert context.window_manager
|
||||||
|
|
||||||
armature_objects = list(input_objects.armature_objects)
|
armature_objects = list(input_objects.armature_objects)
|
||||||
|
|
||||||
result = PskBuildResult()
|
warnings: list[str] = []
|
||||||
psk = Psk()
|
psk = Psk()
|
||||||
|
|
||||||
psx_bone_create_result = create_psx_bones(
|
psx_bone_create_result = create_psx_bones(
|
||||||
@@ -208,7 +211,8 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
# 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.
|
||||||
for armature_object in armature_objects:
|
for armature_object in armature_objects:
|
||||||
armature_object.data.pose_position = 'REST'
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
|
armature_data.pose_position = 'REST'
|
||||||
|
|
||||||
material_names = [m.name if m is not None else 'None' for m in materials]
|
material_names = [m.name if m is not None else 'None' for m in materials]
|
||||||
|
|
||||||
@@ -232,7 +236,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
match options.object_eval_state:
|
match options.object_eval_state:
|
||||||
case 'ORIGINAL':
|
case 'ORIGINAL':
|
||||||
mesh_object = obj
|
mesh_object = obj
|
||||||
mesh_data = obj.data
|
mesh_data = typing_cast(Mesh, obj.data)
|
||||||
case 'EVALUATED':
|
case 'EVALUATED':
|
||||||
# Create a copy of the mesh object after non-armature modifiers are applied.
|
# Create a copy of the mesh object after non-armature modifiers are applied.
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
@@ -299,7 +303,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
mesh_data.calc_loop_triangles()
|
mesh_data.calc_loop_triangles()
|
||||||
|
|
||||||
if mesh_data.uv_layers.active is None:
|
if mesh_data.uv_layers.active is None:
|
||||||
result.warnings.append(f'"{mesh_object.name}" has no active UV Map')
|
warnings.append(f'"{mesh_object.name}" has no active UV Map')
|
||||||
|
|
||||||
# Build a list of non-unique wedges.
|
# Build a list of non-unique wedges.
|
||||||
wedges = []
|
wedges = []
|
||||||
@@ -423,13 +427,12 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil
|
|||||||
|
|
||||||
# Restore the original pose position of the armature objects.
|
# Restore the original pose position of the armature objects.
|
||||||
for armature_object, pose_position in original_armature_object_pose_positions.items():
|
for armature_object, pose_position in original_armature_object_pose_positions.items():
|
||||||
armature_object.data.pose_position = pose_position
|
armature_data = typing_cast(Armature, armature_object.data)
|
||||||
|
armature_data.pose_position = pose_position
|
||||||
|
|
||||||
# https://github.com/DarklightGames/io_scene_psk_psa/issues/129.
|
# https://github.com/DarklightGames/io_scene_psk_psa/issues/129.
|
||||||
psk.sort_and_normalize_weights()
|
psk.sort_and_normalize_weights()
|
||||||
|
|
||||||
context.window_manager.progress_end()
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
result.psk = psk
|
return PskBuildResult(psk, warnings)
|
||||||
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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
|
||||||
from bpy.types import Collection, Context, Depsgraph, Material, Object, Operator, SpaceProperties, Scene
|
from bpy.types import Context, Depsgraph, Material, Object, Operator, Scene
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
from .properties import PskExportMixin
|
from .properties import PskExportMixin
|
||||||
@@ -91,6 +91,7 @@ class PSK_OT_populate_material_name_list(Operator):
|
|||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'No valid export operator found in context')
|
self.report({'ERROR_INVALID_CONTEXT'}, 'No valid export operator found in context')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
assert context.collection
|
||||||
input_objects = get_psk_input_objects_for_collection(context.collection)
|
input_objects = get_psk_input_objects_for_collection(context.collection)
|
||||||
try:
|
try:
|
||||||
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_dfs_objects], export_operator.material_name_list)
|
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_dfs_objects], export_operator.material_name_list)
|
||||||
@@ -115,6 +116,7 @@ class PSK_OT_material_list_name_add(Operator):
|
|||||||
name: StringProperty(search=material_list_names_search_cb, name='Material Name', default='None')
|
name: StringProperty(search=material_list_names_search_cb, name='Material Name', default='None')
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
assert context.window_manager
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
@@ -239,7 +241,7 @@ def get_psk_build_options_from_property_group(scene: Scene, pg: PskExportMixin)
|
|||||||
match pg.transform_source:
|
match pg.transform_source:
|
||||||
case 'SCENE':
|
case 'SCENE':
|
||||||
transform_source = getattr(scene, 'psx_export')
|
transform_source = getattr(scene, 'psx_export')
|
||||||
case 'SELF':
|
case 'CUSTOM':
|
||||||
transform_source = pg
|
transform_source = pg
|
||||||
case _:
|
case _:
|
||||||
assert False, f'Invalid transform source: {pg.transform_source}'
|
assert False, f'Invalid transform source: {pg.transform_source}'
|
||||||
@@ -266,7 +268,10 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
|||||||
collection: StringProperty(options={'HIDDEN'})
|
collection: StringProperty(options={'HIDDEN'})
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
collection = bpy.data.collections.get(self.collection)
|
collection = bpy.data.collections.get(self.collection, None)
|
||||||
|
|
||||||
|
if collection is not None:
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
input_objects = get_psk_input_objects_for_collection(collection)
|
input_objects = get_psk_input_objects_for_collection(collection)
|
||||||
@@ -295,6 +300,8 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
|||||||
def draw(self, context: Context):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
|
assert layout is not None
|
||||||
|
|
||||||
flow = layout.grid_flow(row_major=True)
|
flow = layout.grid_flow(row_major=True)
|
||||||
flow.use_property_split = True
|
flow.use_property_split = True
|
||||||
flow.use_property_decorate = False
|
flow.use_property_decorate = False
|
||||||
@@ -376,6 +383,8 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
|||||||
flow.enabled = False
|
flow.enabled = False
|
||||||
case 'CUSTOM':
|
case 'CUSTOM':
|
||||||
transform_source = self
|
transform_source = self
|
||||||
|
case _:
|
||||||
|
assert False, f'Invalid transform source: {self.transform_source}'
|
||||||
|
|
||||||
flow.prop(transform_source, 'scale')
|
flow.prop(transform_source, 'scale')
|
||||||
flow.prop(transform_source, 'forward_axis')
|
flow.prop(transform_source, 'forward_axis')
|
||||||
@@ -414,6 +423,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
assert context.window_manager
|
||||||
context.window_manager.fileselect_add(self)
|
context.window_manager.fileselect_add(self)
|
||||||
|
|
||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
@@ -421,6 +431,8 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
|
assert layout
|
||||||
|
|
||||||
pg = getattr(context.scene, 'psk_export')
|
pg = getattr(context.scene, 'psk_export')
|
||||||
|
|
||||||
# Mesh
|
# Mesh
|
||||||
@@ -474,9 +486,24 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
flow.use_property_split = True
|
flow.use_property_split = True
|
||||||
flow.use_property_decorate = False
|
flow.use_property_decorate = False
|
||||||
flow.prop(pg, 'export_space')
|
flow.prop(pg, 'export_space')
|
||||||
flow.prop(pg, 'scale')
|
flow.prop(pg, 'transform_source')
|
||||||
flow.prop(pg, 'forward_axis')
|
|
||||||
flow.prop(pg, 'up_axis')
|
flow = transform_panel.grid_flow(row_major=True)
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
|
||||||
|
match pg.transform_source:
|
||||||
|
case 'SCENE':
|
||||||
|
transform_source = getattr(context.scene, 'psx_export')
|
||||||
|
flow.enabled = False
|
||||||
|
case 'CUSTOM':
|
||||||
|
transform_source = pg
|
||||||
|
case _:
|
||||||
|
assert False, f'Invalid transform source: {pg.transform_source}'
|
||||||
|
|
||||||
|
flow.prop(transform_source, 'scale')
|
||||||
|
flow.prop(transform_source, 'forward_axis')
|
||||||
|
flow.prop(transform_source, 'up_axis')
|
||||||
|
|
||||||
# Extended Format
|
# Extended Format
|
||||||
extended_format_header, extended_format_panel = layout.panel('Extended Format', default_closed=False)
|
extended_format_header, extended_format_panel = layout.panel('Extended Format', default_closed=False)
|
||||||
@@ -490,6 +517,8 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = getattr(context.scene, 'psk_export')
|
pg = getattr(context.scene, 'psk_export')
|
||||||
|
|
||||||
|
assert context.scene
|
||||||
|
|
||||||
input_objects = get_psk_input_objects_for_context(context)
|
input_objects = get_psk_input_objects_for_context(context)
|
||||||
options = get_psk_build_options_from_property_group(context.scene, pg)
|
options = get_psk_build_options_from_property_group(context.scene, pg)
|
||||||
|
|
||||||
@@ -509,7 +538,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSK_OT_material_list_move_up,
|
PSK_OT_material_list_move_up,
|
||||||
PSK_OT_material_list_move_down,
|
PSK_OT_material_list_move_down,
|
||||||
PSK_OT_export,
|
PSK_OT_export,
|
||||||
@@ -521,3 +550,6 @@ classes = (
|
|||||||
PSK_OT_material_list_name_move_down,
|
PSK_OT_material_list_name_move_down,
|
||||||
PSK_OT_material_list_name_add,
|
PSK_OT_material_list_name_add,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class PskExportMixin(ExportSpaceMixin, TransformMixin, PsxBoneExportMixin):
|
|||||||
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)
|
||||||
should_export_vertex_normals: BoolProperty(
|
should_export_vertex_normals: BoolProperty(
|
||||||
'Export Vertex Normals',
|
name='Export Vertex Normals',
|
||||||
default=False,
|
default=False,
|
||||||
description='Export VTXNORMS section.'
|
description='Export VTXNORMS section.'
|
||||||
)
|
)
|
||||||
@@ -67,8 +67,12 @@ class PSK_PG_export(PropertyGroup, PskExportMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSK_PG_material_list_item,
|
PSK_PG_material_list_item,
|
||||||
PSK_PG_material_name_list_item,
|
PSK_PG_material_name_list_item,
|
||||||
PSK_PG_export,
|
PSK_PG_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ class PSK_UL_material_names(UIList):
|
|||||||
row.prop(item, 'material_name', text='', emboss=False, icon_value=icon_value, icon='BLANK1' if icon_value == 0 else 'NONE')
|
row.prop(item, 'material_name', text='', emboss=False, icon_value=icon_value, icon='BLANK1' if icon_value == 0 else 'NONE')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSK_UL_material_names,
|
PSK_UL_material_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..importer import PskImportOptions, import_psk
|
|||||||
from ..properties import PskImportMixin
|
from ..properties import PskImportMixin
|
||||||
from ..reader import read_psk
|
from ..reader import read_psk
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -109,6 +110,7 @@ class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
assert self.layout
|
||||||
psk_import_draw(self.layout, self)
|
psk_import_draw(self.layout, self)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,13 +124,15 @@ class PSK_OT_import_drag_and_drop(Operator, PskImportMixin):
|
|||||||
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
|
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context) -> bool:
|
||||||
return context.area and context.area.type == 'VIEW_3D'
|
return context.area is not None and context.area.type == 'VIEW_3D'
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
|
assert self.layout
|
||||||
psk_import_draw(self.layout, self)
|
psk_import_draw(self.layout, self)
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
assert context.window_manager
|
||||||
context.window_manager.invoke_props_dialog(self)
|
context.window_manager.invoke_props_dialog(self)
|
||||||
return {'RUNNING_MODAL'}
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
@@ -167,12 +171,15 @@ class PSK_FH_import(FileHandler):
|
|||||||
bl_file_extensions = '.psk;.pskx'
|
bl_file_extensions = '.psk;.pskx'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll_drop(cls, context: Context):
|
def poll_drop(cls, context: Context) -> bool:
|
||||||
return context.area and context.area.type == 'VIEW_3D'
|
return context.area is not None and context.area.type == 'VIEW_3D'
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSK_OT_import,
|
PSK_OT_import,
|
||||||
PSK_OT_import_drag_and_drop,
|
PSK_OT_import_drag_and_drop,
|
||||||
PSK_FH_import,
|
PSK_FH_import,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions)
|
|||||||
armature_object = None
|
armature_object = None
|
||||||
mesh_object = None
|
mesh_object = None
|
||||||
|
|
||||||
|
assert context.scene
|
||||||
|
assert bpy.context.view_layer
|
||||||
|
|
||||||
if options.should_import_armature:
|
if options.should_import_armature:
|
||||||
# Armature
|
# Armature
|
||||||
armature_data = bpy.data.armatures.new(name)
|
armature_data = bpy.data.armatures.new(name)
|
||||||
@@ -207,8 +210,9 @@ def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions)
|
|||||||
for face_index, face in enumerate(psk.faces):
|
for face_index, face in enumerate(psk.faces):
|
||||||
if face_index in invalid_face_indices:
|
if face_index in invalid_face_indices:
|
||||||
continue
|
continue
|
||||||
for wedge in map(lambda i: psk.wedges[i], reversed(face.wedge_indices)):
|
for wedge_index in reversed(face.wedge_indices):
|
||||||
uv_layer_data[uv_layer_data_index] = wedge.u, 1.0 - wedge.v
|
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]
|
||||||
|
uv_layer_data[uv_layer_data_index] = u, 1.0 - v
|
||||||
uv_layer_data_index += 1
|
uv_layer_data_index += 1
|
||||||
wedge_index_offset += len(psk.wedges)
|
wedge_index_offset += len(psk.wedges)
|
||||||
uv_layer = mesh_data.uv_layers.new(name=f'EXTRAUV{extra_uv_index}')
|
uv_layer = mesh_data.uv_layers.new(name=f'EXTRAUV{extra_uv_index}')
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ class PskImportMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSX_PG_material,
|
PSX_PG_material,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def read_psk(path: str) -> Psk:
|
|||||||
case b'MRPHDATA':
|
case b'MRPHDATA':
|
||||||
_read_types(fp, Psk.MorphData, section, psk.morph_data)
|
_read_types(fp, Psk.MorphData, section, psk.morph_data)
|
||||||
case _:
|
case _:
|
||||||
if section.name.startswith(b'EXTRAUVS'):
|
if section.name.startswith(b'EXTRAUV'):
|
||||||
_read_types(fp, Vector2, section, psk.extra_uvs)
|
_read_types(fp, Vector2, section, psk.extra_uvs)
|
||||||
else:
|
else:
|
||||||
# Section is not handled, skip it.
|
# Section is not handled, skip it.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class PSK_PT_material(Panel):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
assert layout is not None
|
||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
material = context.material
|
material = context.material
|
||||||
@@ -23,6 +24,10 @@ class PSK_PT_material(Panel):
|
|||||||
col.prop(material.psk, 'mesh_triangle_bit_flags', expand=True, text='Flags')
|
col.prop(material.psk, 'mesh_triangle_bit_flags', expand=True, text='Flags')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSK_PT_material,
|
PSK_PT_material,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -154,10 +154,13 @@ class PSX_PG_scene_export(PropertyGroup, TransformMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSX_PG_scene_export,
|
PSX_PG_scene_export,
|
||||||
PSX_PG_action_export,
|
PSX_PG_action_export,
|
||||||
PSX_PG_bone_collection_list_item,
|
PSX_PG_bone_collection_list_item,
|
||||||
PSX_UL_bone_collection_list,
|
PSX_UL_bone_collection_list,
|
||||||
PSX_PT_action,
|
PSX_PT_action,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class PSX_PT_scene(Panel):
|
|||||||
flow.prop(psx_export, 'up_axis')
|
flow.prop(psx_export, 'up_axis')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
_classes = (
|
||||||
PSX_PT_scene,
|
PSX_PT_scene,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from bpy.utils import register_classes_factory
|
||||||
|
register, unregister = register_classes_factory(_classes)
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,12 @@ def test_psk_import_extra_uvs():
|
|||||||
assert mesh_data.uv_layers[0].name == 'UVMap', "First UV layer should be named 'UVMap'"
|
assert mesh_data.uv_layers[0].name == 'UVMap', "First UV layer should be named 'UVMap'"
|
||||||
assert mesh_data.uv_layers[1].name == 'EXTRAUV0', "Second UV layer should be named 'EXTRAUV0'"
|
assert mesh_data.uv_layers[1].name == 'EXTRAUV0', "Second UV layer should be named 'EXTRAUV0'"
|
||||||
|
|
||||||
|
# Verify that the data is actually different
|
||||||
|
assert mesh_data.uv_layers[0].uv[0].vector.x == 0.92480468750
|
||||||
|
assert mesh_data.uv_layers[0].uv[0].vector.y == 0.90533447265625
|
||||||
|
assert mesh_data.uv_layers[1].uv[0].vector.x == 3.0517578125e-05
|
||||||
|
assert mesh_data.uv_layers[1].uv[0].vector.y == 0.999969482421875
|
||||||
|
|
||||||
|
|
||||||
def test_psk_import_materials():
|
def test_psk_import_materials():
|
||||||
assert bpy.ops.psk.import_file(
|
assert bpy.ops.psk.import_file(
|
||||||
|
|||||||
Reference in New Issue
Block a user