Compare commits

..

2 Commits

Author SHA1 Message Date
Colin Basnett
24e606a3fd Revert "Added the ability to prefix imported action names."
This reverts commit 5a13faeb5e.
2022-01-26 01:01:52 -08:00
Colin Basnett
8c0b7f84fc Incremented version to v1.2.1 2022-01-25 22:54:30 -08:00
8 changed files with 30 additions and 206 deletions

View File

@@ -1,4 +1,4 @@
This Blender add-on allows you to import and export meshes and animations to the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats). In addition, the non-standard PSKX format is also supported for import only.
This Blender add-on allows you to import and export meshes and animations to the [PSK and PSA file formats](https://wiki.beyondunreal.com/PSK_%26_PSA_file_formats).
# Installation
1. Download the zip file for the latest version from the [releases](https://github.com/DarklightGames/io_export_psk_psa/releases) page.
@@ -10,9 +10,9 @@ This Blender add-on allows you to import and export meshes and animations to the
7. Enable the newly added "Import-Export: PSK/PSA Importer/Exporter" addon.
# Usage
## Exporting a PSK/PSKX
## Exporting a PSK
1. Select the mesh objects you wish to export.
3. Navigate to File > Export > Unreal PSK (.psk/.pskx)
3. Navigate to File > Export > Unreal PSK (.psk)
4. Enter the file name and click "Export".
## Importing a PSK

View File

@@ -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)",
@@ -58,7 +58,7 @@ def psk_export_menu_func(self, context):
def psk_import_menu_func(self, context):
self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk/.pskx)')
self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk)')
def psa_export_menu_func(self, context):
@@ -72,7 +72,6 @@ def register():
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psk_import = PointerProperty(type=psk_importer.PskImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup)

View File

@@ -1,43 +1,4 @@
from ctypes import *
from typing import Tuple
class Color(Structure):
_fields_ = [
('r', c_ubyte),
('g', c_ubyte),
('b', c_ubyte),
('a', c_ubyte),
]
def __iter__(self):
yield self.r
yield self.g
yield self.b
yield self.a
def __eq__(self, other):
return all(map(lambda x: x[0] == x[1], zip(self, other)))
def __repr__(self):
return repr(tuple(self))
def normalized(self) -> Tuple:
return tuple(map(lambda x: x / 255.0, iter(self)))
class Vector2(Structure):
_fields_ = [
('x', c_float),
('y', c_float),
]
def __iter__(self):
yield self.x
yield self.y
def __repr__(self):
return repr(tuple(self))
class Vector3(Structure):

View File

@@ -1,13 +1,6 @@
from typing import List
def rgb_to_srgb(c):
if c > 0.0031308:
return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055
else:
return 12.92 * c
def populate_bone_group_list(armature_object, bone_group_list):
bone_group_list.clear()

View File

@@ -16,7 +16,6 @@ class PsaImportOptions(object):
self.should_use_fake_user = False
self.should_stash = False
self.sequence_names = []
self.action_name_prefix = ''
class PsaImporter(object):
@@ -116,8 +115,7 @@ class PsaImporter(object):
actions = []
for sequence in sequences:
# Add the action.
action_name = options.action_name_prefix + sequence.name.decode('windows-1252')
action = bpy.data.actions.new(name=action_name)
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.
@@ -235,7 +233,7 @@ def on_psa_file_path_updated(property, context):
pass
class PsaImportPropertyGroup(PropertyGroup):
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)
action_list: CollectionProperty(type=PsaImportActionListItem)
@@ -244,7 +242,6 @@ class PsaImportPropertyGroup(PropertyGroup):
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')
action_name_prefix: StringProperty(default='', name='Action Name Prefix')
class PSA_UL_ImportActionList(UIList):
@@ -352,13 +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')
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(property_group, 'should_clean_keys')
col.prop(property_group, 'should_use_fake_user')
col.prop(property_group, 'should_stash')
col.prop(property_group, 'action_name_prefix')
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')
@@ -402,7 +399,6 @@ class PsaImportOperator(Operator):
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
options.action_name_prefix = property_group.action_name_prefix
PsaImporter().import_psa(psa_reader, context.view_layer.objects.active, options)
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
return {'FINISHED'}

View File

@@ -41,15 +41,6 @@ class Psk(object):
('smoothing_groups', c_int32)
]
class Face32(Structure):
_pack_ = 1
_fields_ = [
('wedge_indices', c_uint32 * 3),
('material_index', c_uint8),
('aux_material_index', c_uint8),
('smoothing_groups', c_int32)
]
class Material(Structure):
_fields_ = [
('name', c_char * 64),
@@ -80,18 +71,6 @@ class Psk(object):
('bone_index', c_int32),
]
@property
def has_extra_uvs(self):
return len(self.extra_uvs) > 0
@property
def has_vertex_colors(self):
return len(self.vertex_colors) > 0
@property
def has_vertex_normals(self):
return len(self.vertex_normals) > 0
def __init__(self):
self.points: List[Vector3] = []
self.wedges: List[Psk.Wedge] = []
@@ -99,6 +78,3 @@ class Psk(object):
self.materials: List[Psk.Material] = []
self.weights: List[Psk.Weight] = []
self.bones: List[Psk.Bone] = []
self.extra_uvs: List[Vector2] = []
self.vertex_colors: List[Color] = []
self.vertex_normals: List[Vector3] = []

View File

@@ -1,35 +1,23 @@
import os
import bpy
import bmesh
import numpy as np
from math import inf
from typing import Optional
from .data import Psk
from ..helpers import rgb_to_srgb
from mathutils import Quaternion, Vector, Matrix
from .reader import PskReader
from bpy.props import StringProperty, EnumProperty, BoolProperty
from bpy.types import Operator, PropertyGroup
from bpy.props import StringProperty
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper
class PskImportOptions(object):
def __init__(self):
self.name = ''
self.should_import_vertex_colors = True
self.vertex_color_space = 'sRGB'
self.should_import_vertex_normals = True
self.should_import_extra_uvs = True
class PskImporter(object):
def __init__(self):
pass
def import_psk(self, psk: Psk, context, options: PskImportOptions):
def import_psk(self, psk: Psk, name: str, context):
# ARMATURE
armature_data = bpy.data.armatures.new(options.name)
armature_object = bpy.data.objects.new(options.name, armature_data)
armature_data = bpy.data.armatures.new(name)
armature_object = bpy.data.objects.new(name, armature_data)
armature_object.show_in_front = True
context.scene.collection.objects.link(armature_object)
@@ -107,8 +95,8 @@ class PskImporter(object):
edit_bone['post_quat'] = import_bone.local_rotation.conjugated()
# MESH
mesh_data = bpy.data.meshes.new(options.name)
mesh_object = bpy.data.objects.new(options.name, mesh_data)
mesh_data = bpy.data.meshes.new(name)
mesh_object = bpy.data.objects.new(name, mesh_data)
# MATERIALS
for material in psk.materials:
@@ -132,6 +120,7 @@ class PskImporter(object):
bm_face.material_index = face.material_index
except ValueError:
degenerate_face_indices.add(face_index)
pass
if len(degenerate_face_indices) > 0:
print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).')
@@ -140,7 +129,7 @@ class PskImporter(object):
# TEXTURE COORDINATES
data_index = 0
uv_layer = mesh_data.uv_layers.new(name='VTXW0000')
uv_layer = mesh_data.uv_layers.new()
for face_index, face in enumerate(psk.faces):
if face_index in degenerate_face_indices:
continue
@@ -149,63 +138,11 @@ class PskImporter(object):
uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v
data_index += 1
# EXTRA UVS
if psk.has_extra_uvs and options.should_import_extra_uvs:
extra_uv_channel_count = int(len(psk.extra_uvs) / len(psk.wedges))
wedge_index_offset = 0
for extra_uv_index in range(extra_uv_channel_count):
data_index = 0
uv_layer = mesh_data.uv_layers.new(name=f'EXTRAUV{extra_uv_index}')
for face_index, face in enumerate(psk.faces):
if face_index in degenerate_face_indices:
continue
for wedge_index in reversed(face.wedge_indices):
u, v = psk.extra_uvs[wedge_index_offset + wedge_index]
uv_layer.data[data_index].uv = u, 1.0 - v
data_index += 1
wedge_index_offset += len(psk.wedges)
# VERTEX COLORS
if psk.has_vertex_colors and options.should_import_vertex_colors:
size = (len(psk.points), 4)
vertex_colors = np.full(size, inf)
vertex_color_data = mesh_data.vertex_colors.new(name='VERTEXCOLOR')
ambiguous_vertex_color_point_indices = []
for wedge_index, wedge in enumerate(psk.wedges):
point_index = wedge.point_index
psk_vertex_color = psk.vertex_colors[wedge_index].normalized()
if vertex_colors[point_index, 0] != inf and tuple(vertex_colors[point_index]) != psk_vertex_color:
ambiguous_vertex_color_point_indices.append(point_index)
else:
vertex_colors[point_index] = psk_vertex_color
if options.vertex_color_space == 'SRGBA':
for i in range(vertex_colors.shape[0]):
vertex_colors[i, :3] = tuple(map(lambda x: rgb_to_srgb(x), vertex_colors[i, :3]))
for loop_index, loop in enumerate(mesh_data.loops):
vertex_color = vertex_colors[loop.vertex_index]
if vertex_color is not None:
vertex_color_data.data[loop_index].color = vertex_color
else:
vertex_color_data.data[loop_index].color = 1.0, 1.0, 1.0, 1.0
if len(ambiguous_vertex_color_point_indices) > 0:
print(f'WARNING: {len(ambiguous_vertex_color_point_indices)} vertex(es) with ambiguous vertex colors.')
# VERTEX NORMALS
if psk.has_vertex_normals and options.should_import_vertex_normals:
mesh_data.polygons.foreach_set("use_smooth", [True] * len(mesh_data.polygons))
normals = []
for vertex_normal in psk.vertex_normals:
normals.append(tuple(vertex_normal))
mesh_data.normals_split_custom_set_from_vertices(normals)
mesh_data.use_auto_smooth = True
bm.normal_update()
bm.free()
# VERTEX WEIGHTS
# 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))
for import_bone in map(lambda x: import_bones[x], sorted(list(vertex_group_bone_indices))):
@@ -227,27 +164,12 @@ class PskImporter(object):
pass
class PskImportPropertyGroup(PropertyGroup):
should_import_vertex_colors: BoolProperty(default=True, name='Vertex Colors', description='Import vertex colors from PSKX files, if available')
vertex_color_space: EnumProperty(
name='Vertex Color Space',
description='The source vertex color space',
default='SRGBA',
items=(
('LINEAR', 'Linear', ''),
('SRGBA', 'sRGBA', ''),
)
)
should_import_vertex_normals: BoolProperty(default=True, name='Vertex Normals', description='Import vertex normals from PSKX files, if available')
should_import_extra_uvs: BoolProperty(default=True, name='Extra UVs', description='Import extra UV maps from PSKX files, if available')
class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk'
bl_label = 'Export'
__doc__ = 'Load a PSK file'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSK file',
@@ -255,28 +177,13 @@ class PskImportOperator(Operator, ImportHelper):
default='')
def execute(self, context):
pg = context.scene.psk_import
reader = PskReader()
psk = reader.read(self.filepath)
options = PskImportOptions()
options.name = os.path.splitext(os.path.basename(self.filepath))[0]
options.vertex_color_space = pg.vertex_color_space
PskImporter().import_psk(psk, context, options)
name = os.path.splitext(os.path.basename(self.filepath))[0]
PskImporter().import_psk(psk, name, context)
return {'FINISHED'}
def draw(self, context):
pg = context.scene.psk_import
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(pg, 'should_import_vertex_normals')
layout.prop(pg, 'should_import_extra_uvs')
layout.prop(pg, 'should_import_vertex_colors')
if pg.should_import_vertex_colors:
layout.prop(pg, 'vertex_color_space')
__classes__ = [
PskImportOperator,
PskImportPropertyGroup,
PskImportOperator
]

View File

@@ -41,14 +41,6 @@ class PskReader(object):
PskReader.read_types(fp, Psk.Bone, section, psk.bones)
elif section.name == b'RAWWEIGHTS':
PskReader.read_types(fp, Psk.Weight, section, psk.weights)
elif section.name == b'FACE3200':
PskReader.read_types(fp, Psk.Face32, section, psk.faces)
elif section.name == b'VERTEXCOLOR':
PskReader.read_types(fp, Color, section, psk.vertex_colors)
elif section.name.startswith(b'EXTRAUVS'):
PskReader.read_types(fp, Vector2, section, psk.extra_uvs)
elif section.name == b'VTXNORMS':
PskReader.read_types(fp, Vector3, section, psk.vertex_normals)
else:
raise RuntimeError(f'Unrecognized section "{section.name} at position {15:fp.tell()}"')
raise RuntimeError(f'Unrecognized section "{section.name}"')
return psk