1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2025-02-08 06:48:19 +01:00

Merge branch 'master' into feature-original-sequence-names

# Conflicts:
#	io_scene_psk_psa/psa/importer.py
#	io_scene_psk_psa/psk/importer.py
This commit is contained in:
Colin Basnett 2022-01-29 21:40:14 -08:00
commit 7d749ea30f
7 changed files with 193 additions and 20 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). This Blender 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). In addition, the non-standard PSKX format is also supported for import only.
# 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.
@ -15,8 +15,8 @@ This Blender add-on allows you to import and export meshes and animations to the
3. Navigate to File > Export > Unreal PSK (.psk) 3. Navigate to File > Export > Unreal PSK (.psk)
4. Enter the file name and click "Export". 4. Enter the file name and click "Export".
## Importing a PSK ## Importing a PSK/PSKX
1. Navigate to File > Import > Unreal PSK (.psk) 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

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "PSK/PSA Importer/Exporter", "name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett", "author": "Colin Basnett",
"version": (1, 2, 1), "version": (2, 1, 0),
"blender": (2, 80, 0), "blender": (2, 80, 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)",
@ -58,7 +58,7 @@ def psk_export_menu_func(self, context):
def psk_import_menu_func(self, context): def psk_import_menu_func(self, context):
self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk)') self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk/.pskx)')
def psa_export_menu_func(self, context): def psa_export_menu_func(self, context):
@ -72,6 +72,7 @@ def register():
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func) bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.append(psa_export_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.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.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup)

View File

@ -1,4 +1,43 @@
from ctypes import * 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): class Vector3(Structure):

View File

@ -1,6 +1,13 @@
from typing import List 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): def populate_bone_group_list(armature_object, bone_group_list):
bone_group_list.clear() bone_group_list.clear()

View File

@ -41,6 +41,15 @@ class Psk(object):
('smoothing_groups', c_int32) ('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): class Material(Structure):
_fields_ = [ _fields_ = [
('name', c_char * 64), ('name', c_char * 64),
@ -71,6 +80,18 @@ class Psk(object):
('bone_index', c_int32), ('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): def __init__(self):
self.points: List[Vector3] = [] self.points: List[Vector3] = []
self.wedges: List[Psk.Wedge] = [] self.wedges: List[Psk.Wedge] = []
@ -78,3 +99,6 @@ class Psk(object):
self.materials: List[Psk.Material] = [] self.materials: List[Psk.Material] = []
self.weights: List[Psk.Weight] = [] self.weights: List[Psk.Weight] = []
self.bones: List[Psk.Bone] = [] self.bones: List[Psk.Bone] = []
self.extra_uvs: List[Vector2] = []
self.vertex_colors: List[Color] = []
self.vertex_normals: List[Vector3] = []

View File

@ -1,23 +1,35 @@
import os import os
import bpy import bpy
import bmesh import bmesh
import numpy as np
from math import inf
from typing import Optional from typing import Optional
from .data import Psk from .data import Psk
from ..helpers import rgb_to_srgb
from mathutils import Quaternion, Vector, Matrix from mathutils import Quaternion, Vector, Matrix
from .reader import PskReader from .reader import PskReader
from bpy.props import StringProperty from bpy.props import StringProperty, EnumProperty, BoolProperty
from bpy.types import Operator from bpy.types import Operator, PropertyGroup
from bpy_extras.io_utils import ImportHelper 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): class PskImporter(object):
def __init__(self): def __init__(self):
pass pass
def import_psk(self, psk: Psk, name: str, context): def import_psk(self, psk: Psk, context, options: PskImportOptions):
# ARMATURE # ARMATURE
armature_data = bpy.data.armatures.new(name) armature_data = bpy.data.armatures.new(options.name)
armature_object = bpy.data.objects.new(name, armature_data) armature_object = bpy.data.objects.new(options.name, armature_data)
armature_object.show_in_front = True armature_object.show_in_front = True
context.scene.collection.objects.link(armature_object) context.scene.collection.objects.link(armature_object)
@ -95,8 +107,8 @@ class PskImporter(object):
edit_bone['post_quat'] = import_bone.local_rotation.conjugated() edit_bone['post_quat'] = import_bone.local_rotation.conjugated()
# MESH # MESH
mesh_data = bpy.data.meshes.new(name) mesh_data = bpy.data.meshes.new(options.name)
mesh_object = bpy.data.objects.new(name, mesh_data) mesh_object = bpy.data.objects.new(options.name, mesh_data)
# MATERIALS # MATERIALS
for material in psk.materials: for material in psk.materials:
@ -120,7 +132,6 @@ class PskImporter(object):
bm_face.material_index = face.material_index bm_face.material_index = face.material_index
except ValueError: except ValueError:
degenerate_face_indices.add(face_index) degenerate_face_indices.add(face_index)
pass
if len(degenerate_face_indices) > 0: if len(degenerate_face_indices) > 0:
print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).') print(f'WARNING: Discarded {len(degenerate_face_indices)} degenerate face(s).')
@ -129,7 +140,7 @@ class PskImporter(object):
# TEXTURE COORDINATES # TEXTURE COORDINATES
data_index = 0 data_index = 0
uv_layer = mesh_data.uv_layers.new() uv_layer = mesh_data.uv_layers.new(name='VTXW0000')
for face_index, face in enumerate(psk.faces): for face_index, face in enumerate(psk.faces):
if face_index in degenerate_face_indices: if face_index in degenerate_face_indices:
continue continue
@ -138,11 +149,63 @@ class PskImporter(object):
uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v
data_index += 1 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.normal_update()
bm.free() bm.free()
# VERTEX 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))
for import_bone in map(lambda x: import_bones[x], sorted(list(vertex_group_bone_indices))): for import_bone in map(lambda x: import_bones[x], sorted(list(vertex_group_bone_indices))):
@ -164,12 +227,27 @@ class PskImporter(object):
pass 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): class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk' bl_idname = 'import.psk'
bl_label = 'Export' bl_label = 'Export'
__doc__ = 'Load a PSK file' __doc__ = 'Load a PSK file'
filename_ext = '.psk' filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
filepath: StringProperty( filepath: StringProperty(
name='File Path', name='File Path',
description='File path used for exporting the PSK file', description='File path used for exporting the PSK file',
@ -177,13 +255,29 @@ class PskImportOperator(Operator, ImportHelper):
default='') default='')
def execute(self, context): def execute(self, context):
pg = context.scene.psk_import
reader = PskReader() reader = PskReader()
psk = reader.read(self.filepath) psk = reader.read(self.filepath)
name = os.path.splitext(os.path.basename(self.filepath))[0] options = PskImportOptions()
PskImporter().import_psk(psk, name, context) 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)
return {'FINISHED'} 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 = ( classes = (
PskImportOperator, PskImportOperator,
PskImportPropertyGroup,
) )

View File

@ -41,6 +41,14 @@ class PskReader(object):
PskReader.read_types(fp, Psk.Bone, section, psk.bones) PskReader.read_types(fp, Psk.Bone, section, psk.bones)
elif section.name == b'RAWWEIGHTS': elif section.name == b'RAWWEIGHTS':
PskReader.read_types(fp, Psk.Weight, section, psk.weights) 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: else:
raise RuntimeError(f'Unrecognized section "{section.name}"') raise RuntimeError(f'Unrecognized section "{section.name} at position {15:fp.tell()}"')
return psk return psk