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:
commit
7d749ea30f
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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] = []
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user