Compare commits

...

11 Commits
4.2.0 ... 4.3.0

Author SHA1 Message Date
Colin Basnett
9566131690 Updated the readme. 2023-04-04 02:42:31 -07:00
Colin Basnett
e4e2354834 Added option to include or exclude shape keys from PSK import. 2023-04-03 22:12:09 -07:00
Colin Basnett
db6204d592 Incremented version to 4.3.0 2023-04-03 22:03:14 -07:00
Colin Basnett
89772ad90d Added an empty MORPH_BASE shape key if any shape keys are present on import. 2023-04-03 22:01:25 -07:00
Colin Basnett
f54d10bb80 Added support for reading and importing MRPHINFO and MRPHKEYS sections. 2023-04-03 21:28:08 -07:00
Colin Basnett
c99725b686 Bumped the minimum Blender version to 2.90. 2023-04-03 02:26:53 -07:00
Colin Basnett
947c86eb8f Fix for issue #32.
Unrecognized sections are now simply ignored.
2023-04-03 01:52:52 -07:00
Colin Basnett
f40db53cb9 Fixed a bug where it was possible to export a PSK with no bones 2023-02-18 00:28:31 -08:00
Colin Basnett
ab998885bb Update README.md 2022-12-06 10:13:30 -08:00
Colin Basnett
f821bec0ff Update README.md 2022-12-06 10:13:13 -08:00
Colin Basnett
43b0fe82dd Update README.md 2022-12-06 10:11:55 -08:00
7 changed files with 70 additions and 17 deletions

View File

@@ -1,17 +1,17 @@
This Blender 2.80+ add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many version of the Unreal Engine. This Blender 2.90+ add-on allows you to import and export meshes and animations to and from the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats) used in many versions of the Unreal Engine.
# Features # Features
* Full PSK/PSA import and export capabilities * Full PSK/PSA import and export capabilities.
* Non-standard PSKX file format with vertex normals, extra UV channels and vertex colors is supported for import only * Non-standard file section data is supported for import only (vertex normals, extra UV channels, vertex colors, shape keys).
* Fine-grained PSA sequence importing for efficient workflow when working with large PSA files * Fine-grained PSA sequence importing for efficient workflow when working with large PSA files.
* Automatic keyframe reduction on PSA import * PSA sequence metadata (e.g., frame rate, sequence name) is preserved on import, allowing this data to be reused on export.
* PSA sequence metadata (e.g., frame rate, sequence name) is preserved on import, allowing this data to be reused on export * Specific [bone groups](https://docs.blender.org/manual/en/latest/animation/armatures/properties/bone_groups.html) can be excluded from PSK/PSA export (useful for excluding non-contributing bones such as IK controllers).
* Specific [bone groups](https://docs.blender.org/manual/en/latest/animation/armatures/properties/bone_groups.html) can be excluded from PSK/PSA export (useful for excluding non-contributing bones such as IK controllers) * PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html), allowing direct use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences.
* PSA sequences can be exported directly from actions or delineated using a scene's [timeline markers](https://docs.blender.org/manual/en/latest/animation/markers.html), allowing direct use of the [NLA](https://docs.blender.org/manual/en/latest/editors/nla/index.html) when creating sequences * Manual re-ordering of material slots when exporting multiple mesh objects.
# Installation # Installation
1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page. 1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page.
2. Open Blender 2.80 or later. 2. Open Blender 2.90 or later.
3. Navigate to the Blender Preferences (Edit > Preferences). 3. Navigate to the Blender Preferences (Edit > Preferences).
4. Select the "Add-ons" tab. 4. Select the "Add-ons" tab.
5. Click the "Install..." button. 5. Click the "Install..." button.
@@ -26,7 +26,7 @@ This Blender 2.80+ add-on allows you to import and export meshes and animations
## Importing a PSK/PSKX ## Importing a PSK/PSKX
1. Navigate to File > Import > Unreal PSK (.psk/.pskx) 1. Navigate to File > Import > Unreal PSK (.psk/.pskx)
2. Select the PSK file you want to import and click "Import" 2. Select the PSK file you want to import and click "Import".
## Exporting a PSA ## Exporting a PSA
1. Select the armature objects you wish to export. 1. Select the armature objects you wish to export.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
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": (4, 2, 0), "version": (4, 3, 0),
"blender": (2, 80, 0), "blender": (2, 90, 0),
# "location": "File > Export > PSK Export (.psk)", # "location": "File > Export > PSK Export (.psk)",
"description": "PSK/PSA Import/Export (.psk/.psa)", "description": "PSK/PSA Import/Export (.psk/.psa)",
"warning": "", "warning": "",

View File

@@ -80,6 +80,19 @@ class Psk(object):
('bone_index', c_int32), ('bone_index', c_int32),
] ]
class MorphInfo(Structure):
_fields_ = [
('name', c_char * 64),
('vertex_count', c_int32)
]
class MorphData(Structure):
_fields_ = [
('position_delta', Vector3),
('tangent_z_delta', Vector3),
('point_index', c_int32)
]
@property @property
def has_extra_uvs(self): def has_extra_uvs(self):
return len(self.extra_uvs) > 0 return len(self.extra_uvs) > 0
@@ -92,6 +105,10 @@ class Psk(object):
def has_vertex_normals(self): def has_vertex_normals(self):
return len(self.vertex_normals) > 0 return len(self.vertex_normals) > 0
@property
def has_morph_data(self):
return len(self.morph_infos) > 0
def __init__(self): def __init__(self):
self.points: List[Vector3] = [] self.points: List[Vector3] = []
self.wedges: List[Psk.Wedge] = [] self.wedges: List[Psk.Wedge] = []
@@ -102,3 +119,5 @@ class Psk(object):
self.extra_uvs: List[Vector2] = [] self.extra_uvs: List[Vector2] = []
self.vertex_colors: List[Color] = [] self.vertex_colors: List[Color] = []
self.vertex_normals: List[Vector3] = [] self.vertex_normals: List[Vector3] = []
self.morph_infos: List[Psk.MorphInfo] = []
self.morph_data: List[Psk.MorphData] = []

View File

@@ -30,12 +30,14 @@ def _write_section(fp, name: bytes, data_type: Type[Structure] = None, data: lis
def export_psk(psk: Psk, path: str): def export_psk(psk: Psk, path: str):
if len(psk.wedges) > MAX_WEDGE_COUNT: if len(psk.wedges) > MAX_WEDGE_COUNT:
raise RuntimeError(f'Number of wedges ({len(psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}') raise RuntimeError(f'Number of wedges ({len(psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}')
if len(psk.bones) > MAX_BONE_COUNT:
raise RuntimeError(f'Number of bones ({len(psk.bones)}) exceeds limit of {MAX_BONE_COUNT}')
if len(psk.points) > MAX_POINT_COUNT: if len(psk.points) > MAX_POINT_COUNT:
raise RuntimeError(f'Numbers of vertices ({len(psk.points)}) exceeds limit of {MAX_POINT_COUNT}') raise RuntimeError(f'Numbers of vertices ({len(psk.points)}) exceeds limit of {MAX_POINT_COUNT}')
if len(psk.materials) > MAX_MATERIAL_COUNT: if len(psk.materials) > MAX_MATERIAL_COUNT:
raise RuntimeError(f'Number of materials ({len(psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}') raise RuntimeError(f'Number of materials ({len(psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}')
if len(psk.bones) > MAX_BONE_COUNT:
raise RuntimeError(f'Number of bones ({len(psk.bones)}) exceeds limit of {MAX_BONE_COUNT}')
elif len(psk.bones) == 0:
raise RuntimeError(f'At least one bone must be marked for export')
with open(path, 'wb') as fp: with open(path, 'wb') as fp:
_write_section(fp, b'ACTRHEAD') _write_section(fp, b'ACTRHEAD')

View File

@@ -25,6 +25,7 @@ class PskImportOptions(object):
self.should_import_vertex_normals = True self.should_import_vertex_normals = True
self.should_import_extra_uvs = True self.should_import_extra_uvs = True
self.should_import_skeleton = True self.should_import_skeleton = True
self.should_import_shape_keys = True
self.bone_length = 1.0 self.bone_length = 1.0
@@ -221,6 +222,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
bm.normal_update() bm.normal_update()
bm.free() bm.free()
# WEIGHTS
# Get a list of all bones that have weights associated with them. # Get a list of all bones that have weights associated with them.
vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights)) vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights))
vertex_groups: List[Optional[VertexGroup]] = [None] * len(psk.bones) vertex_groups: List[Optional[VertexGroup]] = [None] * len(psk.bones)
@@ -230,6 +232,21 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
for weight in psk.weights: for weight in psk.weights:
vertex_groups[weight.bone_index].add((weight.point_index,), weight.weight, 'ADD') vertex_groups[weight.bone_index].add((weight.point_index,), weight.weight, 'ADD')
# MORPHS (SHAPE KEYS)
if options.should_import_shape_keys:
morph_data_iterator = iter(psk.morph_data)
if psk.has_morph_data:
mesh_object.shape_key_add(name='MORPH_BASE', from_mix=False)
for morph_info in psk.morph_infos:
shape_key = mesh_object.shape_key_add(name=morph_info.name.decode('windows-1252'), from_mix=False)
for _ in range(morph_info.vertex_count):
morph_data = next(morph_data_iterator)
x, y, z = morph_data.position_delta
shape_key.data[morph_data.point_index].co += Vector((x, -y, z))
context.scene.collection.objects.link(mesh_object) context.scene.collection.objects.link(mesh_object)
# Add armature modifier to our mesh object. # Add armature modifier to our mesh object.
@@ -270,13 +287,13 @@ class PskImportPropertyGroup(PropertyGroup):
default=True, default=True,
name='Vertex Normals', name='Vertex Normals',
options=empty_set, options=empty_set,
description='Import vertex normals from PSKX files, if available' description='Import vertex normals, if available'
) )
should_import_extra_uvs: BoolProperty( should_import_extra_uvs: BoolProperty(
default=True, default=True,
name='Extra UVs', name='Extra UVs',
options=empty_set, options=empty_set,
description='Import extra UV maps from PSKX files, if available' description='Import extra UV maps, if available'
) )
should_import_mesh: BoolProperty( should_import_mesh: BoolProperty(
default=True, default=True,
@@ -299,6 +316,12 @@ class PskImportPropertyGroup(PropertyGroup):
options=empty_set, options=empty_set,
description='Length of the bones' description='Length of the bones'
) )
should_import_shape_keys: BoolProperty(
default=True,
name='Import Shape Keys',
options=empty_set,
description='Import shape keys, if available'
)
class PskImportOperator(Operator, ImportHelper): class PskImportOperator(Operator, ImportHelper):
@@ -327,6 +350,7 @@ class PskImportOperator(Operator, ImportHelper):
options.should_import_vertex_normals = pg.should_import_vertex_normals options.should_import_vertex_normals = pg.should_import_vertex_normals
options.vertex_color_space = pg.vertex_color_space options.vertex_color_space = pg.vertex_color_space
options.should_import_skeleton = pg.should_import_skeleton options.should_import_skeleton = pg.should_import_skeleton
options.should_import_shape_keys = pg.should_import_shape_keys
options.bone_length = pg.bone_length options.bone_length = pg.bone_length
result = import_psk(psk, context, options) result = import_psk(psk, context, options)
@@ -359,6 +383,7 @@ class PskImportOperator(Operator, ImportHelper):
row.use_property_decorate = False row.use_property_decorate = False
if pg.should_import_skeleton: if pg.should_import_skeleton:
row.prop(pg, 'bone_length') row.prop(pg, 'bone_length')
layout.prop(pg, 'should_import_shape_keys')
classes = ( classes = (

View File

@@ -1,4 +1,5 @@
import ctypes import ctypes
import os
from .data import * from .data import *
@@ -45,6 +46,12 @@ def read_psk(path: str) -> Psk:
_read_types(fp, Vector2, section, psk.extra_uvs) _read_types(fp, Vector2, section, psk.extra_uvs)
elif section.name == b'VTXNORMS': elif section.name == b'VTXNORMS':
_read_types(fp, Vector3, section, psk.vertex_normals) _read_types(fp, Vector3, section, psk.vertex_normals)
elif section.name == b'MRPHINFO':
_read_types(fp, Psk.MorphInfo, section, psk.morph_infos)
elif section.name == b'MRPHDATA':
_read_types(fp, Psk.MorphData, section, psk.morph_data)
else: else:
raise RuntimeError(f'Unrecognized section "{section.name} at position {15:fp.tell()}"') # Section is not handled, skip it.
fp.seek(section.data_size * section.data_count, os.SEEK_CUR)
print(f'Unrecognized section "{section.name} at position {fp.tell():15}"')
return psk return psk