Compare commits

...

16 Commits

Author SHA1 Message Date
Colin Basnett
a42ebbd240 Fix for accidental check-in of bugged animation code in previous commit 2021-09-07 21:51:41 -07:00
Colin Basnett
65833b57e8 Fix for bug https://github.com/DarklightGames/io_export_psk_psa/issues/7 2021-09-07 21:40:42 -07:00
Colin Basnett
0622cf43e5 Updated version to 1.1.1 2021-09-07 00:01:57 -07:00
Colin Basnett
6a94d1b862 Fixed a bug where animations would be exported incorrectly if the first key frame of an action wasn't at frame zero 2021-09-06 23:35:49 -07:00
Colin Basnett
313dfcc97c PSA files now export actions with the frame-rate defined in the scene (previously defaulted to 30fps) 2021-09-06 16:18:42 -07:00
Colin Basnett
58fb7a5dad Revert "PSA files now export actions with the frame-rate defined in the scene (previously defaulted to 30fps)"
This reverts commit e29f9e16dd.
2021-09-06 16:18:13 -07:00
Colin Basnett
e29f9e16dd PSA files now export actions with the frame-rate defined in the scene (previously defaulted to 30fps) 2021-09-06 16:14:55 -07:00
Colin Basnett
9db05766e0 Update README.md 2021-08-24 00:09:21 -07:00
Colin Basnett
4c4900e388 Merge remote-tracking branch 'origin/master' 2021-08-14 23:18:59 -07:00
Colin Basnett
08f4960e1f Update README.md 2021-08-14 23:15:38 -07:00
Colin Basnett
f08ecc457f Update README.md 2021-08-14 23:15:22 -07:00
Colin Basnett
0d03e49da3 Removed some unhelpful comments and TODOs. 2021-08-14 22:59:05 -07:00
Colin Basnett
32cadbfe31 Merge pull request #6 from DarklightGames/feature-static-meshes
Added the ability for users to export "static meshes" for meshes don't have armature modifiers
2021-08-14 22:02:19 -07:00
Colin Basnett
a76215569d Added the ability for users to export "static meshes" for meshes that don't have armature modifiers 2021-08-14 18:00:16 -07:00
Colin Basnett
e51013eec7 Incremented version. 2021-08-14 16:39:23 -07:00
Colin Basnett
94da7b4acf Merge branch 'feature-multiple-meshes' 2021-08-14 14:29:55 -07:00
6 changed files with 165 additions and 108 deletions

View File

@@ -1,3 +1,29 @@
# io_export_psk_psa This Blender add-on allows you to export meshes and animations to the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats).
If you are looking for a PSK/PSA importer, use [this one](https://github.com/Befzz/blender3d_import_psk_psa)! # Installation
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.
3. Navigate to the Blender Preferences (Edit > Preferences).
4. Select the "Add-ons" tab.
5. Click the "Install..." button.
6. Select the .zip file that you downloaded earlier and click "Install Add-on".
7. Enable the newly added "Import-Export: PSK/PSA Exporter" addon.
# Usage
## Exporting a PSK
1. Select the mesh objects you wish to export.
3. Navigate to File > Export > Unreal PSK (.psk)
4. Enter the file name and click "Export".
## Exporting a PSA
1. Select the armature objects you wish to export.
2. Navigate to File > Export > Unreal PSA (.psa)
3. Enter the file name and click "Export".
# FAQ
## Can I use this addon to import PSK and PSA files?
Currently, no.
Presumably you are using this in concert with the [UE Viewer](https://www.gildor.org/en/projects/umodel) program to import extracted meshes. It is *not recommended* to export PSK/PSA from UE Viewer since it [does not preserve smoothing groups](https://github.com/gildor2/UEViewer/issues/235). As a result, the shading of imported models will be incorrect and will need to be manually fixed. Instead, it is recommended to export meshes to the glTF format for import into Blender since it preserves the correct mesh shading.
Regardless, if you are dead set on using a PSK/PSA importer, use [this one](https://github.com/Befzz/blender3d_import_psk_psa).

View File

@@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "PSK/PSA Exporter", "name": "PSK/PSA Exporter",
"author": "Colin Basnett", "author": "Colin Basnett",
"version": ( 1, 0, 0 ), "version": (1, 1, 1),
"blender": (2, 80, 0), "blender": (2, 80, 0),
"location": "File > Export > PSK Export (.psk)", "location": "File > Export > PSK Export (.psk)",
"description": "PSK/PSA Export (.psk)", "description": "PSK/PSA Export (.psk)",

View File

@@ -85,15 +85,14 @@ class PsaBuilder(object):
sequence.name = bytes(action.name, encoding='utf-8') sequence.name = bytes(action.name, encoding='utf-8')
sequence.frame_count = frame_max - frame_min + 1 sequence.frame_count = frame_max - frame_min + 1
sequence.frame_start_index = frame_start_index sequence.frame_start_index = frame_start_index
sequence.fps = 30 # TODO: fill in later with r sequence.fps = context.scene.render.fps
frame_count = frame_max - frame_min + 1 frame_count = frame_max - frame_min + 1
for frame in range(frame_count): for frame in range(frame_count):
context.scene.frame_set(frame) context.scene.frame_set(frame_min + frame)
for bone in pose_bones: for bone in pose_bones:
# TODO: is the cast-to-matrix necessary? (guessing no)
key = Psa.Key() key = Psa.Key()
pose_bone_matrix = bone.matrix pose_bone_matrix = bone.matrix

View File

@@ -10,6 +10,7 @@ class PskInputObjects(object):
self.mesh_objects = [] self.mesh_objects = []
self.armature_object = None self.armature_object = None
class PskBuilder(object): class PskBuilder(object):
def __init__(self): def __init__(self):
pass pass
@@ -30,37 +31,43 @@ class PskBuilder(object):
if len(obj.data.materials) == 0: if len(obj.data.materials) == 0:
raise RuntimeError(f'Mesh "{obj.name}" must have at least one material') raise RuntimeError(f'Mesh "{obj.name}" must have at least one material')
# ensure that there is exactly one armature modifier object shared between all selected meshes # Ensure that there are either no armature modifiers (static mesh)
# or that there is exactly one armature modifier object shared between
# all selected meshes
armature_modifier_objects = set() armature_modifier_objects = set()
for obj in input_objects.mesh_objects: for obj in input_objects.mesh_objects:
modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE'] modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE']
if len(modifiers) != 1: if len(modifiers) == 0:
raise RuntimeError(f'Mesh "{obj.name}" must have one armature modifier') continue
elif len(modifiers) == 2:
raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier')
armature_modifier_objects.add(modifiers[0].object) armature_modifier_objects.add(modifiers[0].object)
if len(armature_modifier_objects) > 1: if len(armature_modifier_objects) > 1:
raise RuntimeError('All selected meshes must have the same armature modifier') raise RuntimeError('All selected meshes must have the same armature modifier')
elif len(armature_modifier_objects) == 1:
input_objects.armature_object = list(armature_modifier_objects)[0] input_objects.armature_object = list(armature_modifier_objects)[0]
if input_objects.armature_object is None:
raise RuntimeError('Armature modifier has no linked object')
return input_objects return input_objects
def build(self, context) -> Psk: def build(self, context) -> Psk:
input_objects = PskBuilder.get_input_objects(context) input_objects = PskBuilder.get_input_objects(context)
wedge_count = sum([len(m.data.loops) for m in input_objects.mesh_objects])
if wedge_count <= 65536:
wedge_type = Psk.Wedge16
else:
wedge_type = Psk.Wedge32
psk = Psk() psk = Psk()
materials = OrderedDict() materials = OrderedDict()
if input_objects.armature_object is None:
# Static mesh (no armature)
psk_bone = Psk.Bone()
psk_bone.name = bytes('static', encoding='utf-8')
psk_bone.flags = 0
psk_bone.children_count = 0
psk_bone.parent_index = 0
psk_bone.location = Vector3(0, 0, 0)
psk_bone.rotation = Quaternion(0, 0, 0, 1)
psk.bones.append(psk_bone)
else:
bones = list(input_objects.armature_object.data.bones) bones = list(input_objects.armature_object.data.bones)
for bone in bones: for bone in bones:
psk_bone = Psk.Bone() psk_bone = Psk.Bone()
@@ -99,10 +106,7 @@ class PskBuilder(object):
psk.bones.append(psk_bone) psk.bones.append(psk_bone)
vertex_offset = 0 vertex_offset = 0
wedge_offset = 0
weight_offset = 0
# TODO: if there is an edge-split modifier, we need to apply it (maybe?)
for object in input_objects.mesh_objects: for object in input_objects.mesh_objects:
# VERTICES # VERTICES
for vertex in object.data.vertices: for vertex in object.data.vertices:
@@ -113,18 +117,7 @@ class PskBuilder(object):
point.z = v.z point.z = v.z
psk.points.append(point) psk.points.append(point)
# WEDGES
uv_layer = object.data.uv_layers.active.data uv_layer = object.data.uv_layers.active.data
# needs to be additive!!!
psk.wedges.extend([wedge_type() for _ in range(len(object.data.loops))])
for loop_index, loop in enumerate(object.data.loops):
wedge = psk.wedges[wedge_offset + loop_index]
wedge.material_index = 0 # NOTE: this material index is set properly while building the faces
wedge.point_index = loop.vertex_index + vertex_offset
wedge.u, wedge.v = uv_layer[loop_index].uv
wedge.v = 1.0 - wedge.v
psk.wedges.append(wedge)
# MATERIALS # MATERIALS
material_indices = [] material_indices = []
@@ -142,24 +135,50 @@ class PskBuilder(object):
material_index = material.texture_index material_index = material.texture_index
material_indices.append(material_index) material_indices.append(material_index)
# FACES # WEDGES
# TODO: this is making the assumption that the mesh is triangulated
object.data.calc_loop_triangles() object.data.calc_loop_triangles()
# Build a list of non-unique wedges.
wedges = []
for loop_index, loop in enumerate(object.data.loops):
wedge = Psk.Wedge()
wedge.point_index = loop.vertex_index + vertex_offset
wedge.u, wedge.v = uv_layer[loop_index].uv
wedge.v = 1.0 - wedge.v
wedges.append(wedge)
# Assign material indices to the wedges.
for triangle in object.data.loop_triangles:
for loop_index in triangle.loops:
wedges[loop_index].material_index = material_indices[triangle.material_index]
# Populate the list of wedges with unique wedges & build a look-up table of loop indices to wedge indices
wedge_indices = {}
loop_wedge_indices = [-1] * len(object.data.loops)
for loop_index, wedge in enumerate(wedges):
wedge_hash = hash(wedge)
if wedge_hash in wedge_indices:
loop_wedge_indices[loop_index] = wedge_indices[wedge_hash]
else:
wedge_index = len(psk.wedges)
wedge_indices[wedge_hash] = wedge_index
psk.wedges.append(wedge)
loop_wedge_indices[loop_index] = wedge_index
# FACES
poly_groups, groups = object.data.calc_smooth_groups(use_bitflags=True) poly_groups, groups = object.data.calc_smooth_groups(use_bitflags=True)
for f in object.data.loop_triangles: for f in object.data.loop_triangles:
face = Psk.Face() face = Psk.Face()
face.material_index = material_indices[f.material_index] face.material_index = material_indices[f.material_index]
face.wedge_index_1 = f.loops[2] + wedge_offset face.wedge_indices[0] = loop_wedge_indices[f.loops[2]]
face.wedge_index_2 = f.loops[1] + wedge_offset face.wedge_indices[1] = loop_wedge_indices[f.loops[1]]
face.wedge_index_3 = f.loops[0] + wedge_offset face.wedge_indices[2] = loop_wedge_indices[f.loops[0]]
face.smoothing_groups = poly_groups[f.polygon_index] face.smoothing_groups = poly_groups[f.polygon_index]
psk.faces.append(face) psk.faces.append(face)
# update the material index of the wedges
for i in range(3):
psk.wedges[wedge_offset + f.loops[i]].material_index = face.material_index
# WEIGHTS # WEIGHTS
# TODO: bone ~> vg might not be 1:1, provide a nice error message if this is the case # TODO: bone ~> vg might not be 1:1, provide a nice error message if this is the case
if input_objects.armature_object is not None:
armature = input_objects.armature_object.data armature = input_objects.armature_object.data
bone_names = [x.name for x in armature.bones] bone_names = [x.name for x in armature.bones]
vertex_group_names = [x.name for x in object.vertex_groups] vertex_group_names = [x.name for x in object.vertex_groups]
@@ -179,8 +198,6 @@ class PskBuilder(object):
w.weight = weight w.weight = weight
psk.weights.append(w) psk.weights.append(w)
vertex_offset += len(psk.points) vertex_offset = len(psk.points)
wedge_offset += len(psk.wedges)
weight_offset += len(psk.weights)
return psk return psk

View File

@@ -4,30 +4,38 @@ from ..data import *
class Psk(object): class Psk(object):
class Wedge(object):
def __init__(self):
self.point_index: int = 0
self.u: float = 0.0
self.v: float = 0.0
self.material_index: int = 0
def __hash__(self):
return hash(f'{self.point_index}-{self.u}-{self.v}-{self.material_index}')
class Wedge16(Structure): class Wedge16(Structure):
_fields_ = [ _fields_ = [
('point_index', c_int16), ('point_index', c_uint16),
('padding1', c_int16), ('padding1', c_int16),
('u', c_float), ('u', c_float),
('v', c_float), ('v', c_float),
('material_index', c_int8), ('material_index', c_uint8),
('reserved', c_int8), ('reserved', c_int8),
('padding2', c_int16) ('padding2', c_int16)
] ]
class Wedge32(Structure): class Wedge32(Structure):
_fields_ = [ _fields_ = [
('point_index', c_int32), ('point_index', c_uint32),
('u', c_float), ('u', c_float),
('v', c_float), ('v', c_float),
('material_index', c_int32) ('material_index', c_uint32)
] ]
class Face(Structure): class Face(Structure):
_fields_ = [ _fields_ = [
('wedge_index_1', c_int16), ('wedge_indices', c_uint16 * 3),
('wedge_index_2', c_int16),
('wedge_index_3', c_int16),
('material_index', c_int8), ('material_index', c_int8),
('aux_material_index', c_int8), ('aux_material_index', c_int8),
('smoothing_groups', c_int32) ('smoothing_groups', c_int32)
@@ -65,7 +73,7 @@ class Psk(object):
def __init__(self): def __init__(self):
self.points: List[Vector3] = [] self.points: List[Vector3] = []
self.wedges: List[Psk.Wedge16] = [] self.wedges: List[Psk.Wedge] = []
self.faces: List[Psk.Face] = [] self.faces: List[Psk.Face] = []
self.materials: List[Psk.Material] = [] self.materials: List[Psk.Material] = []
self.weights: List[Psk.Weight] = [] self.weights: List[Psk.Weight] = []

View File

@@ -19,19 +19,26 @@ class PskExporter(object):
fp.write(datum) fp.write(datum)
def export(self, path: str): def export(self, path: str):
# TODO: add logic somewhere to assert lengths of ctype structs
with open(path, 'wb') as fp: with open(path, 'wb') as fp:
self.write_section(fp, b'ACTRHEAD') self.write_section(fp, b'ACTRHEAD')
self.write_section(fp, b'PNTS0000', Vector3, self.psk.points) self.write_section(fp, b'PNTS0000', Vector3, self.psk.points)
# WEDGES # WEDGES
# TODO: this really should be on the level of the builder, not the exporter
if len(self.psk.wedges) <= 65536: if len(self.psk.wedges) <= 65536:
wedge_type = Psk.Wedge16 wedge_type = Psk.Wedge16
else: else:
wedge_type = Psk.Wedge32 wedge_type = Psk.Wedge32
self.write_section(fp, b'VTXW0000', wedge_type, self.psk.wedges) wedges = []
for index, w in enumerate(self.psk.wedges):
wedge = wedge_type()
wedge.material_index = w.material_index
wedge.u = w.u
wedge.v = w.v
wedge.point_index = w.point_index
wedges.append(wedge)
self.write_section(fp, b'VTXW0000', wedge_type, wedges)
self.write_section(fp, b'FACE0000', Psk.Face, self.psk.faces) self.write_section(fp, b'FACE0000', Psk.Face, self.psk.faces)
self.write_section(fp, b'MATT0000', Psk.Material, self.psk.materials) self.write_section(fp, b'MATT0000', Psk.Material, self.psk.materials)
self.write_section(fp, b'REFSKELT', Psk.Bone, self.psk.bones) self.write_section(fp, b'REFSKELT', Psk.Bone, self.psk.bones)