mirror of
https://github.com/DarklightGames/io_scene_psk_psa.git
synced 2025-02-07 22:41:26 +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
|
||||
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)
|
||||
4. Enter the file name and click "Export".
|
||||
|
||||
## Importing a PSK
|
||||
1. Navigate to File > Import > Unreal PSK (.psk)
|
||||
## Importing a PSK/PSKX
|
||||
1. Navigate to File > Import > Unreal PSK (.psk/.pskx)
|
||||
2. Select the PSK file you want to import and click "Import"
|
||||
|
||||
## Exporting a PSA
|
||||
|
@ -1,7 +1,7 @@
|
||||
bl_info = {
|
||||
"name": "PSK/PSA Importer/Exporter",
|
||||
"author": "Colin Basnett",
|
||||
"version": (1, 2, 1),
|
||||
"version": (2, 1, 0),
|
||||
"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)')
|
||||
self.layout.operator(psk_importer.PskImportOperator.bl_idname, text='Unreal PSK (.psk/.pskx)')
|
||||
|
||||
|
||||
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_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)
|
||||
|
||||
|
@ -1,4 +1,43 @@
|
||||
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):
|
||||
|
@ -1,6 +1,13 @@
|
||||
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()
|
||||
|
||||
|
@ -41,6 +41,15 @@ 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),
|
||||
@ -71,6 +80,18 @@ 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] = []
|
||||
@ -78,3 +99,6 @@ 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] = []
|
||||
|
@ -1,23 +1,35 @@
|
||||
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
|
||||
from bpy.types import Operator
|
||||
from bpy.props import StringProperty, EnumProperty, BoolProperty
|
||||
from bpy.types import Operator, PropertyGroup
|
||||
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, name: str, context):
|
||||
def import_psk(self, psk: Psk, context, options: PskImportOptions):
|
||||
# ARMATURE
|
||||
armature_data = bpy.data.armatures.new(name)
|
||||
armature_object = bpy.data.objects.new(name, armature_data)
|
||||
armature_data = bpy.data.armatures.new(options.name)
|
||||
armature_object = bpy.data.objects.new(options.name, armature_data)
|
||||
armature_object.show_in_front = True
|
||||
|
||||
context.scene.collection.objects.link(armature_object)
|
||||
@ -95,8 +107,8 @@ class PskImporter(object):
|
||||
edit_bone['post_quat'] = import_bone.local_rotation.conjugated()
|
||||
|
||||
# MESH
|
||||
mesh_data = bpy.data.meshes.new(name)
|
||||
mesh_object = bpy.data.objects.new(name, mesh_data)
|
||||
mesh_data = bpy.data.meshes.new(options.name)
|
||||
mesh_object = bpy.data.objects.new(options.name, mesh_data)
|
||||
|
||||
# MATERIALS
|
||||
for material in psk.materials:
|
||||
@ -120,7 +132,6 @@ 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).')
|
||||
@ -129,7 +140,7 @@ class PskImporter(object):
|
||||
|
||||
# TEXTURE COORDINATES
|
||||
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):
|
||||
if face_index in degenerate_face_indices:
|
||||
continue
|
||||
@ -138,11 +149,63 @@ 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))):
|
||||
@ -164,12 +227,27 @@ 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', options={'HIDDEN'})
|
||||
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
|
||||
filepath: StringProperty(
|
||||
name='File Path',
|
||||
description='File path used for exporting the PSK file',
|
||||
@ -177,13 +255,29 @@ class PskImportOperator(Operator, ImportHelper):
|
||||
default='')
|
||||
|
||||
def execute(self, context):
|
||||
pg = context.scene.psk_import
|
||||
reader = PskReader()
|
||||
psk = reader.read(self.filepath)
|
||||
name = os.path.splitext(os.path.basename(self.filepath))[0]
|
||||
PskImporter().import_psk(psk, name, context)
|
||||
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)
|
||||
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,
|
||||
)
|
||||
|
@ -41,6 +41,14 @@ 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}"')
|
||||
raise RuntimeError(f'Unrecognized section "{section.name} at position {15:fp.tell()}"')
|
||||
return psk
|
||||
|
Loading…
x
Reference in New Issue
Block a user