Compare commits

...

10 Commits
5.0.2 ... 5.0.6

Author SHA1 Message Date
Colin Basnett
122e765bca PSA sequences are now selected by default
There were multiple bug reports from users who were getting mixed
signals from the addon, believing that the bone name warnings were
the cause of the sequences not being imported. The actual issue was that
users didn't know they needed to manually select the sequences within
the PSA.

This should fix the poor UX around this, as just having it selected by
default is more sensible for a wider audience.
2023-10-19 13:14:57 -07:00
Colin Basnett
4db8764677 Incremented version to v5.0.5 2023-09-09 18:34:04 -07:00
Colin Basnett
f185ffbe16 Fixed a bug where the importer would crash when importing PSA files from programs using CUE4Parse (#46)
This is not actually a bug in this addon, but a bug with the CUE4Parse library itself. A fix for the library has been submitted (https://github.com/FabianFG/CUE4Parse/issues/103), but there will still be bugged versions hanging around for a while, so it's prudent to have this addon accomodate the broken files, especially since the fix is relatively easy and won't interfere with properly formatted files.

At some point, this fix will be deprecated to reduce cruft.
2023-09-09 18:24:38 -07:00
Colin Basnett
3d460a15e3 Added a specific error for when the user doesn't select any animations to export, as this was confusing some folks 2023-09-06 02:38:11 -07:00
Colin Basnett
da39c14464 Fix for #47
This code had been refactored but not tested with the no-armature workflow
2023-09-05 23:37:42 -07:00
Colin Basnett
83e65687ac Incremented version to 5.0.4 2023-08-25 17:40:30 -07:00
Colin Basnett
63fb6f7d09 Fixed a bug where it was not possible to export from markers 2023-08-25 16:58:15 -07:00
Colin Basnett
741357d0af Incremented version to 5.0.3 2023-08-14 22:43:26 -07:00
Colin Basnett
fb2ab89766 Fixed a compatibility issue caused by the fix to #43 2023-08-14 22:42:59 -07:00
Colin Basnett
d0d6deb63c Changed name of faces that couldn't be added to "invalid" faces instead of "degenerate" faces
This is because a face can still be rejected by being a duplicate, but it doesn't necessarily have to be degenerate (i.e., a line or point).
2023-08-10 23:38:56 -07:00
9 changed files with 67 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
bl_info = {
"name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett, Yurii Ti",
"version": (5, 0, 2),
"version": (5, 0, 6),
"blender": (3, 4, 0),
"description": "PSK/PSA Import/Export (.psk/.psa)",
"warning": "",

View File

@@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke
from bpy_extras.io_utils import ExportHelper
from bpy_types import Operator
from io_scene_psk_psa.helpers import populate_bone_group_list, get_nla_strips_in_timeframe
from io_scene_psk_psa.psa.builder import build_psa, PsaBuildSequence, PsaBuildOptions
from io_scene_psk_psa.psa.export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
from io_scene_psk_psa.psa.writer import write_psa
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
from ..export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
from ..writer import write_psa
from ...helpers import populate_bone_group_list, get_nla_strips_in_timeframe
def is_action_for_armature(armature: Armature, action: Action):
@@ -358,7 +358,7 @@ class PSA_OT_export(Operator, ExportHelper):
# Ensure that we actually have items that we are going to be exporting.
if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0:
raise RuntimeError('No actions were selected for export')
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0:
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
raise RuntimeError('No timeline markers were selected for export')
# Populate the export sequence list.

View File

@@ -19,7 +19,7 @@ empty_set = set()
class PSA_PG_export_action_list_item(PropertyGroup):
action: PointerProperty(type=Action)
name: StringProperty()
is_selected: BoolProperty(default=False)
is_selected: BoolProperty(default=True)
frame_start: IntProperty(options={'HIDDEN'})
frame_end: IntProperty(options={'HIDDEN'})
is_pose_marker: BoolProperty(options={'HIDDEN'})

View File

@@ -167,12 +167,17 @@ class PSA_OT_import(Operator, ImportHelper):
options.should_convert_to_samples = pg.should_convert_to_samples
options.bone_mapping_mode = pg.bone_mapping_mode
if len(sequence_names) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
return {'CANCELLED'}
result = import_psa(context, psa_reader, context.view_layer.objects.active, options)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
message += '\n'.join(result.warnings)
self.report({'WARNING'}, message)
for warning in result.warnings:
self.report({'WARNING'}, warning)
else:
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')

View File

@@ -10,7 +10,7 @@ empty_set = set()
class PSA_PG_import_action_list_item(PropertyGroup):
action_name: StringProperty(options=empty_set)
is_selected: BoolProperty(default=False, options=empty_set)
is_selected: BoolProperty(default=True, options=empty_set)
class PSA_PG_bone(PropertyGroup):

View File

@@ -5,6 +5,24 @@ import numpy as np
from .data import *
def _try_fix_cue4parse_issue_103(sequences) -> bool:
# Detect if the file was exported from CUE4Parse prior to the fix for issue #103.
# https://github.com/FabianFG/CUE4Parse/issues/103
# The issue was that the frame_start_index was not being set correctly, and was always being set to the same value
# as the frame_count.
# This fix will eventually be deprecated as it is only necessary for files exported prior to the fix.
if len(sequences) > 0:
if sequences[0].frame_start_index == sequences[0].frame_count:
# Manually set the frame_start_index for each sequence. This assumes that the sequences are in order with
# no shared frames between sequences (all exporters that I know of do this, so it's a safe assumption).
frame_start_index = 0
for i, sequence in enumerate(sequences):
sequence.frame_start_index = frame_start_index
frame_start_index += sequence.frame_count
return True
return False
class PsaReader(object):
"""
This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
@@ -86,14 +104,15 @@ class PsaReader(object):
elif section.name == b'ANIMINFO':
sequences = []
PsaReader._read_types(fp, Psa.Sequence, section, sequences)
# Try to fix CUE4Parse bug, if necessary.
_try_fix_cue4parse_issue_103(sequences)
for sequence in sequences:
psa.sequences[sequence.name.decode()] = sequence
elif section.name == b'ANIMKEYS':
# Skip keys on this pass. We will keep this file open and read from it as needed.
self.keys_data_offset = fp.tell()
fp.seek(section.data_size * section.data_count, 1)
elif section.name == b'SCALEKEYS':
fp.seek(section.data_size * section.data_count, 1)
else:
raise RuntimeError(f'Unrecognized section "{section.name}"')
fp.seek(section.data_size * section.data_count, 1)
print(f'Unrecognized section in PSA: "{section.name}"')
return psa

View File

@@ -147,6 +147,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
# Temporarily force the armature into the rest position.
# We will undo this later.
old_pose_position = None
if armature_object is not None:
old_pose_position = armature_object.data.pose_position
armature_object.data.pose_position = 'REST'
@@ -164,6 +166,7 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
mesh_object.vertex_groups.new(name=vertex_group.name)
# Restore the previous pose position on the armature.
if old_pose_position is not None:
armature_object.data.pose_position = old_pose_position
vertex_offset = len(psk.points)

View File

@@ -150,17 +150,22 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
bm.verts.ensure_lookup_table()
degenerate_face_indices = set()
invalid_face_indices = set()
for face_index, face in enumerate(psk.faces):
point_indices = [bm.verts[psk.wedges[i].point_index] for i in reversed(face.wedge_indices)]
point_indices = map(lambda i: psk.wedges[i].point_index, reversed(face.wedge_indices))
points = [bm.verts[i] for i in point_indices]
try:
bm_face = bm.faces.new(point_indices)
bm_face = bm.faces.new(points)
bm_face.material_index = face.material_index
except ValueError:
degenerate_face_indices.add(face_index)
# This happens for two reasons:
# 1. Two or more of the face's points are the same. (i.e, point indices of [0, 0, 1])
# 2. The face is a duplicate of another face. (i.e., point indices of [0, 1, 2] and [0, 1, 2])
invalid_face_indices.add(face_index)
if len(degenerate_face_indices) > 0:
result.warnings.append(f'Discarded {len(degenerate_face_indices)} degenerate face(s).')
# TODO: Handle invalid faces better.
if len(invalid_face_indices) > 0:
result.warnings.append(f'Discarded {len(invalid_face_indices)} invalid face(s).')
bm.to_mesh(mesh_data)
@@ -168,7 +173,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
data_index = 0
uv_layer = mesh_data.uv_layers.new(name='VTXW0000')
for face_index, face in enumerate(psk.faces):
if face_index in degenerate_face_indices:
if face_index in invalid_face_indices:
continue
face_wedges = [psk.wedges[i] for i in reversed(face.wedge_indices)]
for wedge in face_wedges:
@@ -183,7 +188,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
data_index = 0
uv_layer = mesh_data.uv_layers.new(name=f'EXTRAUV{extra_uv_index}')
for face_index, face in enumerate(psk.faces):
if face_index in degenerate_face_indices:
if face_index in invalid_face_indices:
continue
for wedge_index in reversed(face.wedge_indices):
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]

View File

@@ -78,4 +78,16 @@ def read_psk(path: str) -> Psk:
'''
psk.material_references = _read_material_references(path)
'''
Tools like UEViewer and CUE4Parse write the point index as a 32-bit integer, exploiting the fact that due to struct
alignment, there were 16-bits of padding following the original 16-bit point index in the wedge struct.
However, this breaks compatibility with PSK files that were created with older tools that treated the
point index as a 16-bit integer and might have junk data written to the padding bits.
To work around this, we check if each point is still addressable using a 16-bit index, and if it is, assume the
point index is a 16-bit integer and truncate the high bits.
'''
if len(psk.points) <= 65536:
for wedge in psk.wedges:
wedge.point_index &= 0xFFFF
return psk