Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9566131690 | ||
|
|
e4e2354834 | ||
|
|
db6204d592 | ||
|
|
89772ad90d | ||
|
|
f54d10bb80 | ||
|
|
c99725b686 | ||
|
|
947c86eb8f | ||
|
|
f40db53cb9 | ||
|
|
ab998885bb | ||
|
|
f821bec0ff | ||
|
|
43b0fe82dd |
20
README.md
20
README.md
@@ -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.
|
||||||
|
|||||||
BIN
io_scene_psk_psa-master-4.2.1.zip
Normal file
BIN
io_scene_psk_psa-master-4.2.1.zip
Normal file
Binary file not shown.
@@ -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": "",
|
||||||
|
|||||||
@@ -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] = []
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user