Compare commits
16 Commits
feature-mu
...
1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42ebbd240 | ||
|
|
65833b57e8 | ||
|
|
0622cf43e5 | ||
|
|
6a94d1b862 | ||
|
|
313dfcc97c | ||
|
|
58fb7a5dad | ||
|
|
e29f9e16dd | ||
|
|
9db05766e0 | ||
|
|
4c4900e388 | ||
|
|
08f4960e1f | ||
|
|
f08ecc457f | ||
|
|
0d03e49da3 | ||
|
|
32cadbfe31 | ||
|
|
a76215569d | ||
|
|
e51013eec7 | ||
|
|
94da7b4acf |
30
README.md
30
README.md
@@ -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).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
bl_info = {
|
||||
"name": "PSK/PSA Exporter",
|
||||
"author": "Colin Basnett",
|
||||
"version": ( 1, 0, 0 ),
|
||||
"blender": ( 2, 80, 0 ),
|
||||
"version": (1, 1, 1),
|
||||
"blender": (2, 80, 0),
|
||||
"location": "File > Export > PSK Export (.psk)",
|
||||
"description": "PSK/PSA Export (.psk)",
|
||||
"warning": "",
|
||||
|
||||
@@ -85,15 +85,14 @@ class PsaBuilder(object):
|
||||
sequence.name = bytes(action.name, encoding='utf-8')
|
||||
sequence.frame_count = frame_max - frame_min + 1
|
||||
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
|
||||
|
||||
for frame in range(frame_count):
|
||||
context.scene.frame_set(frame)
|
||||
context.scene.frame_set(frame_min + frame)
|
||||
|
||||
for bone in pose_bones:
|
||||
# TODO: is the cast-to-matrix necessary? (guessing no)
|
||||
key = Psa.Key()
|
||||
pose_bone_matrix = bone.matrix
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class PskInputObjects(object):
|
||||
self.mesh_objects = []
|
||||
self.armature_object = None
|
||||
|
||||
|
||||
class PskBuilder(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -30,37 +31,43 @@ class PskBuilder(object):
|
||||
if len(obj.data.materials) == 0:
|
||||
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()
|
||||
|
||||
for obj in input_objects.mesh_objects:
|
||||
modifiers = [x for x in obj.modifiers if x.type == 'ARMATURE']
|
||||
if len(modifiers) != 1:
|
||||
raise RuntimeError(f'Mesh "{obj.name}" must have one armature modifier')
|
||||
if len(modifiers) == 0:
|
||||
continue
|
||||
elif len(modifiers) == 2:
|
||||
raise RuntimeError(f'Mesh "{obj.name}" must have only one armature modifier')
|
||||
armature_modifier_objects.add(modifiers[0].object)
|
||||
|
||||
if len(armature_modifier_objects) > 1:
|
||||
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]
|
||||
|
||||
if input_objects.armature_object is None:
|
||||
raise RuntimeError('Armature modifier has no linked object')
|
||||
|
||||
return input_objects
|
||||
|
||||
def build(self, context) -> Psk:
|
||||
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()
|
||||
|
||||
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)
|
||||
for bone in bones:
|
||||
psk_bone = Psk.Bone()
|
||||
@@ -99,10 +106,7 @@ class PskBuilder(object):
|
||||
psk.bones.append(psk_bone)
|
||||
|
||||
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:
|
||||
# VERTICES
|
||||
for vertex in object.data.vertices:
|
||||
@@ -113,18 +117,7 @@ class PskBuilder(object):
|
||||
point.z = v.z
|
||||
psk.points.append(point)
|
||||
|
||||
# WEDGES
|
||||
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
|
||||
material_indices = []
|
||||
@@ -142,24 +135,50 @@ class PskBuilder(object):
|
||||
material_index = material.texture_index
|
||||
material_indices.append(material_index)
|
||||
|
||||
# FACES
|
||||
# TODO: this is making the assumption that the mesh is triangulated
|
||||
# WEDGES
|
||||
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)
|
||||
for f in object.data.loop_triangles:
|
||||
face = Psk.Face()
|
||||
face.material_index = material_indices[f.material_index]
|
||||
face.wedge_index_1 = f.loops[2] + wedge_offset
|
||||
face.wedge_index_2 = f.loops[1] + wedge_offset
|
||||
face.wedge_index_3 = f.loops[0] + wedge_offset
|
||||
face.wedge_indices[0] = loop_wedge_indices[f.loops[2]]
|
||||
face.wedge_indices[1] = loop_wedge_indices[f.loops[1]]
|
||||
face.wedge_indices[2] = loop_wedge_indices[f.loops[0]]
|
||||
face.smoothing_groups = poly_groups[f.polygon_index]
|
||||
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
|
||||
# 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
|
||||
bone_names = [x.name for x in armature.bones]
|
||||
vertex_group_names = [x.name for x in object.vertex_groups]
|
||||
@@ -179,8 +198,6 @@ class PskBuilder(object):
|
||||
w.weight = weight
|
||||
psk.weights.append(w)
|
||||
|
||||
vertex_offset += len(psk.points)
|
||||
wedge_offset += len(psk.wedges)
|
||||
weight_offset += len(psk.weights)
|
||||
vertex_offset = len(psk.points)
|
||||
|
||||
return psk
|
||||
|
||||
@@ -4,30 +4,38 @@ from ..data import *
|
||||
|
||||
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):
|
||||
_fields_ = [
|
||||
('point_index', c_int16),
|
||||
('point_index', c_uint16),
|
||||
('padding1', c_int16),
|
||||
('u', c_float),
|
||||
('v', c_float),
|
||||
('material_index', c_int8),
|
||||
('material_index', c_uint8),
|
||||
('reserved', c_int8),
|
||||
('padding2', c_int16)
|
||||
]
|
||||
|
||||
class Wedge32(Structure):
|
||||
_fields_ = [
|
||||
('point_index', c_int32),
|
||||
('point_index', c_uint32),
|
||||
('u', c_float),
|
||||
('v', c_float),
|
||||
('material_index', c_int32)
|
||||
('material_index', c_uint32)
|
||||
]
|
||||
|
||||
class Face(Structure):
|
||||
_fields_ = [
|
||||
('wedge_index_1', c_int16),
|
||||
('wedge_index_2', c_int16),
|
||||
('wedge_index_3', c_int16),
|
||||
('wedge_indices', c_uint16 * 3),
|
||||
('material_index', c_int8),
|
||||
('aux_material_index', c_int8),
|
||||
('smoothing_groups', c_int32)
|
||||
@@ -65,7 +73,7 @@ class Psk(object):
|
||||
|
||||
def __init__(self):
|
||||
self.points: List[Vector3] = []
|
||||
self.wedges: List[Psk.Wedge16] = []
|
||||
self.wedges: List[Psk.Wedge] = []
|
||||
self.faces: List[Psk.Face] = []
|
||||
self.materials: List[Psk.Material] = []
|
||||
self.weights: List[Psk.Weight] = []
|
||||
|
||||
@@ -19,19 +19,26 @@ class PskExporter(object):
|
||||
fp.write(datum)
|
||||
|
||||
def export(self, path: str):
|
||||
# TODO: add logic somewhere to assert lengths of ctype structs
|
||||
with open(path, 'wb') as fp:
|
||||
self.write_section(fp, b'ACTRHEAD')
|
||||
self.write_section(fp, b'PNTS0000', Vector3, self.psk.points)
|
||||
|
||||
# WEDGES
|
||||
# TODO: this really should be on the level of the builder, not the exporter
|
||||
if len(self.psk.wedges) <= 65536:
|
||||
wedge_type = Psk.Wedge16
|
||||
else:
|
||||
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'MATT0000', Psk.Material, self.psk.materials)
|
||||
self.write_section(fp, b'REFSKELT', Psk.Bone, self.psk.bones)
|
||||
|
||||
Reference in New Issue
Block a user