Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
122e765bca | ||
|
|
4db8764677 | ||
|
|
f185ffbe16 | ||
|
|
3d460a15e3 | ||
|
|
da39c14464 | ||
|
|
83e65687ac | ||
|
|
63fb6f7d09 | ||
|
|
741357d0af | ||
|
|
fb2ab89766 | ||
|
|
d0d6deb63c |
@@ -1,7 +1,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "PSK/PSA Importer/Exporter",
|
"name": "PSK/PSA Importer/Exporter",
|
||||||
"author": "Colin Basnett, Yurii Ti",
|
"author": "Colin Basnett, Yurii Ti",
|
||||||
"version": (5, 0, 2),
|
"version": (5, 0, 6),
|
||||||
"blender": (3, 4, 0),
|
"blender": (3, 4, 0),
|
||||||
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke
|
|||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from bpy_types import Operator
|
from bpy_types import Operator
|
||||||
|
|
||||||
from io_scene_psk_psa.helpers import populate_bone_group_list, get_nla_strips_in_timeframe
|
from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
||||||
from io_scene_psk_psa.psa.builder import build_psa, PsaBuildSequence, PsaBuildOptions
|
from ..export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
|
||||||
from io_scene_psk_psa.psa.export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences
|
from ..writer import write_psa
|
||||||
from io_scene_psk_psa.psa.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):
|
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.
|
# Ensure that we actually have items that we are going to be exporting.
|
||||||
if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0:
|
if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0:
|
||||||
raise RuntimeError('No actions were selected for export')
|
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')
|
raise RuntimeError('No timeline markers were selected for export')
|
||||||
|
|
||||||
# Populate the export sequence list.
|
# Populate the export sequence list.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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()
|
||||||
is_selected: BoolProperty(default=False)
|
is_selected: BoolProperty(default=True)
|
||||||
frame_start: IntProperty(options={'HIDDEN'})
|
frame_start: IntProperty(options={'HIDDEN'})
|
||||||
frame_end: IntProperty(options={'HIDDEN'})
|
frame_end: IntProperty(options={'HIDDEN'})
|
||||||
is_pose_marker: BoolProperty(options={'HIDDEN'})
|
is_pose_marker: BoolProperty(options={'HIDDEN'})
|
||||||
|
|||||||
@@ -167,12 +167,17 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
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_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)
|
result = import_psa(context, psa_reader, context.view_layer.objects.active, options)
|
||||||
|
|
||||||
if len(result.warnings) > 0:
|
if len(result.warnings) > 0:
|
||||||
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
||||||
message += '\n'.join(result.warnings)
|
|
||||||
self.report({'WARNING'}, message)
|
self.report({'WARNING'}, message)
|
||||||
|
for warning in result.warnings:
|
||||||
|
self.report({'WARNING'}, warning)
|
||||||
else:
|
else:
|
||||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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=empty_set)
|
||||||
is_selected: BoolProperty(default=False, options=empty_set)
|
is_selected: BoolProperty(default=True, options=empty_set)
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_bone(PropertyGroup):
|
class PSA_PG_bone(PropertyGroup):
|
||||||
|
|||||||
@@ -5,6 +5,24 @@ import numpy as np
|
|||||||
from .data import *
|
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):
|
class PsaReader(object):
|
||||||
"""
|
"""
|
||||||
This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle.
|
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':
|
elif section.name == b'ANIMINFO':
|
||||||
sequences = []
|
sequences = []
|
||||||
PsaReader._read_types(fp, Psa.Sequence, section, 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:
|
for sequence in sequences:
|
||||||
psa.sequences[sequence.name.decode()] = sequence
|
psa.sequences[sequence.name.decode()] = sequence
|
||||||
elif section.name == b'ANIMKEYS':
|
elif section.name == b'ANIMKEYS':
|
||||||
# Skip keys on this pass. We will keep this file open and read from it as needed.
|
# Skip keys on this pass. We will keep this file open and read from it as needed.
|
||||||
self.keys_data_offset = fp.tell()
|
self.keys_data_offset = fp.tell()
|
||||||
fp.seek(section.data_size * section.data_count, 1)
|
fp.seek(section.data_size * section.data_count, 1)
|
||||||
elif section.name == b'SCALEKEYS':
|
|
||||||
fp.seek(section.data_size * section.data_count, 1)
|
|
||||||
else:
|
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
|
return psa
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk:
|
|||||||
|
|
||||||
# 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.
|
||||||
|
old_pose_position = None
|
||||||
|
if armature_object is not None:
|
||||||
old_pose_position = armature_object.data.pose_position
|
old_pose_position = armature_object.data.pose_position
|
||||||
armature_object.data.pose_position = 'REST'
|
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)
|
mesh_object.vertex_groups.new(name=vertex_group.name)
|
||||||
|
|
||||||
# Restore the previous pose position on the armature.
|
# Restore the previous pose position on the armature.
|
||||||
|
if old_pose_position is not None:
|
||||||
armature_object.data.pose_position = old_pose_position
|
armature_object.data.pose_position = old_pose_position
|
||||||
|
|
||||||
vertex_offset = len(psk.points)
|
vertex_offset = len(psk.points)
|
||||||
|
|||||||
@@ -150,17 +150,22 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
|
|||||||
|
|
||||||
bm.verts.ensure_lookup_table()
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
degenerate_face_indices = set()
|
invalid_face_indices = set()
|
||||||
for face_index, face in enumerate(psk.faces):
|
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:
|
try:
|
||||||
bm_face = bm.faces.new(point_indices)
|
bm_face = bm.faces.new(points)
|
||||||
bm_face.material_index = face.material_index
|
bm_face.material_index = face.material_index
|
||||||
except ValueError:
|
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:
|
# TODO: Handle invalid faces better.
|
||||||
result.warnings.append(f'Discarded {len(degenerate_face_indices)} degenerate face(s).')
|
if len(invalid_face_indices) > 0:
|
||||||
|
result.warnings.append(f'Discarded {len(invalid_face_indices)} invalid face(s).')
|
||||||
|
|
||||||
bm.to_mesh(mesh_data)
|
bm.to_mesh(mesh_data)
|
||||||
|
|
||||||
@@ -168,7 +173,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
|
|||||||
data_index = 0
|
data_index = 0
|
||||||
uv_layer = mesh_data.uv_layers.new(name='VTXW0000')
|
uv_layer = mesh_data.uv_layers.new(name='VTXW0000')
|
||||||
for face_index, face in enumerate(psk.faces):
|
for face_index, face in enumerate(psk.faces):
|
||||||
if face_index in degenerate_face_indices:
|
if face_index in invalid_face_indices:
|
||||||
continue
|
continue
|
||||||
face_wedges = [psk.wedges[i] for i in reversed(face.wedge_indices)]
|
face_wedges = [psk.wedges[i] for i in reversed(face.wedge_indices)]
|
||||||
for wedge in face_wedges:
|
for wedge in face_wedges:
|
||||||
@@ -183,7 +188,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
|
|||||||
data_index = 0
|
data_index = 0
|
||||||
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}')
|
||||||
for face_index, face in enumerate(psk.faces):
|
for face_index, face in enumerate(psk.faces):
|
||||||
if face_index in degenerate_face_indices:
|
if face_index in invalid_face_indices:
|
||||||
continue
|
continue
|
||||||
for wedge_index in reversed(face.wedge_indices):
|
for wedge_index in reversed(face.wedge_indices):
|
||||||
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]
|
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]
|
||||||
|
|||||||
@@ -78,4 +78,16 @@ def read_psk(path: str) -> Psk:
|
|||||||
'''
|
'''
|
||||||
psk.material_references = _read_material_references(path)
|
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
|
return psk
|
||||||
|
|||||||
Reference in New Issue
Block a user