Compare commits
10 Commits
feature-im
...
2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24e606a3fd | ||
|
|
8c0b7f84fc | ||
|
|
5a13faeb5e | ||
|
|
3932176a57 | ||
|
|
57a2179412 | ||
|
|
605b618856 | ||
|
|
ea6905ec74 | ||
|
|
13c194e629 | ||
|
|
dba6983ae0 | ||
|
|
ff58da8788 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: cmbasnett
|
||||
@@ -37,4 +37,4 @@ This Blender add-on allows you to import and export meshes and animations to the
|
||||
## Why are the mesh normals not accurate when importing a PSK extracted from [UE Viewer](https://www.gildor.org/en/projects/umodel)?
|
||||
If preserving the mesh normals of models is important for your workflow, it is *not recommended* to export PSK files from UE Viewer. This is because UE Viewer makes no attempt to reconstruct the original [smoothing groups](https://en.wikipedia.org/wiki/Smoothing_group). As a result, the normals of imported PSK files will be incorrect when imported into Blender and will need to be manually fixed.
|
||||
|
||||
As a workaround, it is recommended to export [glTF](https://en.wikipedia.org/wiki/GlTF) meshes out of UE Viewer instead, since the glTF format has support for explicit normals and UE Viewer can correctly preserve the mesh normals on export. Note, however, that the imported glTF armature may have it's bones oriented incorrectly when imported into blender. To mitigate this, you can combine the armature of PSK and the mesh of the glTF for best results.
|
||||
As a workaround, it is recommended to export [glTF](https://en.wikipedia.org/wiki/GlTF) meshes out of UE Viewer instead, since the glTF format has support for explicit normals and UE Viewer can correctly preserve the mesh normals on export. Note, however, that the imported glTF armature may have it's bones oriented incorrectly when imported into Blender. To mitigate this, you can combine the armature of PSK and the mesh of the glTF for best results.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "PSK/PSA Importer/Exporter",
|
||||
"author": "Colin Basnett",
|
||||
"version": (1, 2, 0),
|
||||
"version": (1, 2, 1),
|
||||
"blender": (2, 80, 0),
|
||||
# "location": "File > Export > PSK Export (.psk)",
|
||||
"description": "PSK/PSA Import/Export (.psk/.psa)",
|
||||
|
||||
@@ -102,6 +102,7 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
box.label(text='Bones', icon='BONE_DATA')
|
||||
bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static
|
||||
row = box.row(align=True)
|
||||
|
||||
for item in bone_filter_mode_items:
|
||||
identifier = item.identifier
|
||||
item_layout = row.row(align=True)
|
||||
@@ -114,6 +115,7 @@ class PsaExportOperator(Operator, ExportHelper):
|
||||
rows = max(3, min(len(property_group.bone_group_list), 10))
|
||||
row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows)
|
||||
|
||||
|
||||
def is_action_for_armature(self, action):
|
||||
if len(action.fcurves) == 0:
|
||||
return False
|
||||
|
||||
@@ -10,12 +10,20 @@ from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerP
|
||||
from .reader import PsaReader
|
||||
|
||||
|
||||
class PsaImportOptions(object):
|
||||
def __init__(self):
|
||||
self.should_clean_keys = True
|
||||
self.should_use_fake_user = False
|
||||
self.should_stash = False
|
||||
self.sequence_names = []
|
||||
|
||||
|
||||
class PsaImporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def import_psa(self, psa_reader: PsaReader, sequence_names: List[AnyStr], armature_object):
|
||||
sequences = map(lambda x: psa_reader.sequences[x], sequence_names)
|
||||
def import_psa(self, psa_reader: PsaReader, armature_object, options: PsaImportOptions):
|
||||
sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names)
|
||||
armature_data = armature_object.data
|
||||
|
||||
class ImportBone(object):
|
||||
@@ -104,9 +112,11 @@ class PsaImporter(object):
|
||||
import_bone.post_quat = import_bone.orig_quat.conjugated()
|
||||
|
||||
# Create and populate the data for new sequences.
|
||||
actions = []
|
||||
for sequence in sequences:
|
||||
# Add the action.
|
||||
action = bpy.data.actions.new(name=sequence.name.decode())
|
||||
action.use_fake_user = options.should_use_fake_user
|
||||
|
||||
# Create f-curves for the rotation and location of each bone.
|
||||
for psa_bone_index, armature_bone_index in psa_to_armature_bone_indices.items():
|
||||
@@ -124,19 +134,30 @@ class PsaImporter(object):
|
||||
action.fcurves.new(location_data_path, index=2), # Lz
|
||||
]
|
||||
|
||||
# Read the sequence keys from the PSA file.
|
||||
sequence_name = sequence.name.decode('windows-1252')
|
||||
|
||||
# Read the sequence data matrix from the PSA.
|
||||
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
|
||||
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
|
||||
|
||||
# The first step is to determine the frames at which each bone will write out a keyframe.
|
||||
# Convert the sequence's data from world-space to local-space.
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for frame_index in range(sequence.frame_count):
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
# Calculate the local-space key data for the bone.
|
||||
sequence_data_matrix[frame_index, bone_index] = calculate_fcurve_data(import_bone, key_data)
|
||||
|
||||
# Clean the keyframe data. This is accomplished by writing zeroes to the write matrix when there is an
|
||||
# insufficiently large change in the data from frame-to-frame.
|
||||
if options.should_clean_keys:
|
||||
threshold = 0.001
|
||||
for bone_index, import_bone in enumerate(import_bones):
|
||||
if import_bone is None:
|
||||
continue
|
||||
for fcurve_index, fcurve in enumerate(import_bone.fcurves):
|
||||
for fcurve_index in range(len(import_bone.fcurves)):
|
||||
# Get all the keyframe data for the bone's f-curve data from the sequence data matrix.
|
||||
fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index]
|
||||
last_written_datum = 0
|
||||
@@ -145,7 +166,7 @@ class PsaImporter(object):
|
||||
if frame_index > 0 and abs(datum - last_written_datum) < threshold:
|
||||
keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0
|
||||
else:
|
||||
last_written_datum = fcurve_frame_data[frame_index]
|
||||
last_written_datum = datum
|
||||
|
||||
# Write the keyframes out!
|
||||
for frame_index in range(sequence.frame_count):
|
||||
@@ -156,12 +177,22 @@ class PsaImporter(object):
|
||||
if bone_has_writeable_keyframes:
|
||||
# This bone has writeable keyframes for this frame.
|
||||
key_data = sequence_data_matrix[frame_index, bone_index]
|
||||
# Calculate the local-space key data for the bone.
|
||||
fcurve_data = calculate_fcurve_data(import_bone, key_data)
|
||||
for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data):
|
||||
for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], key_data):
|
||||
if should_write:
|
||||
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
|
||||
|
||||
actions.append(action)
|
||||
|
||||
# If the user specifies, store the new animations as strips on a non-contributing NLA stack.
|
||||
if options.should_stash:
|
||||
if armature_object.animation_data is None:
|
||||
armature_object.animation_data_create()
|
||||
for action in actions:
|
||||
nla_track = armature_object.animation_data.nla_tracks.new()
|
||||
nla_track.name = action.name
|
||||
nla_track.mute = True
|
||||
nla_track.strips.new(name=action.name, start=0, action=action)
|
||||
|
||||
|
||||
class PsaImportPsaBoneItem(PropertyGroup):
|
||||
bone_name: StringProperty()
|
||||
@@ -182,7 +213,6 @@ class PsaImportActionListItem(PropertyGroup):
|
||||
|
||||
|
||||
def on_psa_file_path_updated(property, context):
|
||||
print('PATH UPDATED')
|
||||
property_group = context.scene.psa_import
|
||||
property_group.action_list.clear()
|
||||
property_group.psa_bones.clear()
|
||||
@@ -195,33 +225,23 @@ def on_psa_file_path_updated(property, context):
|
||||
item.action_name = sequence.name.decode('windows-1252')
|
||||
item.frame_count = sequence.frame_count
|
||||
item.is_selected = True
|
||||
|
||||
for psa_bone in psa_reader.bones:
|
||||
item = property_group.psa_bones.add()
|
||||
item.bone_name = psa_bone.name
|
||||
except IOError as e:
|
||||
print('ERROR READING FILE')
|
||||
print(e)
|
||||
# TODO: set an error somewhere so the user knows the PSA could not be read.
|
||||
pass
|
||||
|
||||
|
||||
def on_armature_object_updated(property, context):
|
||||
# TODO: ensure that there are matching bones between the two rigs.
|
||||
property_group = context.scene.psa_import
|
||||
armature_object = property_group.armature_object
|
||||
if armature_object is not None:
|
||||
armature_bone_names = set(map(lambda bone: bone.name, armature_object.data.bones))
|
||||
psa_bone_names = set(map(lambda psa_bone: psa_bone.name, property_group.psa_bones))
|
||||
|
||||
|
||||
class PsaImportPropertyGroup(bpy.types.PropertyGroup):
|
||||
psa_file_path: StringProperty(default='', update=on_psa_file_path_updated, name='PSA File Path')
|
||||
psa_bones: CollectionProperty(type=PsaImportPsaBoneItem)
|
||||
# armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated)
|
||||
action_list: CollectionProperty(type=PsaImportActionListItem)
|
||||
action_list_index: IntProperty(name='', default=0)
|
||||
action_filter_name: StringProperty(default='')
|
||||
should_clean_keys: BoolProperty(default=True, name='Clean Keyframes', description='Exclude unnecessary keyframes from being written to the actions.')
|
||||
should_use_fake_user: BoolProperty(default=True, name='Fake User', description='Assign each imported action a fake user so that the data block is saved even it has no users.')
|
||||
should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track')
|
||||
|
||||
|
||||
class PSA_UL_ImportActionList(UIList):
|
||||
@@ -314,12 +334,10 @@ class PSA_PT_ImportPanel(Panel):
|
||||
row = layout.row()
|
||||
row.prop(property_group, 'psa_file_path', text='')
|
||||
row.enabled = False
|
||||
# row.enabled = property_group.psa_file_path is not ''
|
||||
|
||||
row = layout.row()
|
||||
|
||||
layout.separator()
|
||||
|
||||
row.operator('psa_import.select_file', text='Select PSA File', icon='FILEBROWSER')
|
||||
|
||||
if len(property_group.action_list) > 0:
|
||||
box = layout.box()
|
||||
box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION')
|
||||
@@ -331,7 +349,13 @@ class PSA_PT_ImportPanel(Panel):
|
||||
row.operator('psa_import.actions_select_all', text='All')
|
||||
row.operator('psa_import.actions_deselect_all', text='None')
|
||||
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
row.prop(property_group, 'should_clean_keys')
|
||||
|
||||
# DATA
|
||||
row = layout.row()
|
||||
row.prop(property_group, 'should_use_fake_user')
|
||||
row.prop(property_group, 'should_stash')
|
||||
|
||||
layout.operator('psa_import.import', text=f'Import')
|
||||
|
||||
@@ -370,7 +394,12 @@ class PsaImportOperator(Operator):
|
||||
property_group = context.scene.psa_import
|
||||
psa_reader = PsaReader(property_group.psa_file_path)
|
||||
sequence_names = [x.action_name for x in property_group.action_list if x.is_selected]
|
||||
PsaImporter().import_psa(psa_reader, sequence_names, context.view_layer.objects.active)
|
||||
options = PsaImportOptions()
|
||||
options.sequence_names = sequence_names
|
||||
options.should_clean_keys = property_group.should_clean_keys
|
||||
options.should_use_fake_user = property_group.should_use_fake_user
|
||||
options.should_stash = property_group.should_stash
|
||||
PsaImporter().import_psa(psa_reader, context.view_layer.objects.active, options)
|
||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -127,11 +127,11 @@ class PskExportOperator(Operator, ExportHelper):
|
||||
options.bone_group_indices = [x.index for x in property_group.bone_group_list if x.is_selected]
|
||||
try:
|
||||
psk = builder.build(context, options)
|
||||
exporter = PskExporter(psk)
|
||||
exporter.export(self.filepath)
|
||||
except RuntimeError as e:
|
||||
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
exporter = PskExporter(psk)
|
||||
exporter.export(self.filepath)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user