* PSK importer now working

* Fleshing out PSA importer (not done yet but getting there)
This commit is contained in:
Colin Basnett
2022-01-14 12:26:35 -08:00
parent d578350980
commit 9fa0780032
11 changed files with 504 additions and 296 deletions

View File

@@ -36,8 +36,8 @@ class Psk(object):
class Face(Structure):
_fields_ = [
('wedge_indices', c_uint16 * 3),
('material_index', c_int8),
('aux_material_index', c_int8),
('material_index', c_uint8),
('aux_material_index', c_uint8),
('smoothing_groups', c_int32)
]

View File

@@ -1,8 +1,18 @@
from typing import Type
from .data import *
from .builder import PskBuilder
from typing import Type
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty
MAX_WEDGE_COUNT = 65536
MAX_POINT_COUNT = 4294967296
MAX_BONE_COUNT = 256
MAX_MATERIAL_COUNT = 256
class PskExporter(object):
def __init__(self, psk: Psk):
self.psk: Psk = psk
@@ -19,27 +29,61 @@ class PskExporter(object):
fp.write(datum)
def export(self, path: str):
if len(self.psk.wedges) > MAX_WEDGE_COUNT:
raise RuntimeError(f'Number of wedges ({len(self.psk.wedges)}) exceeds limit of {MAX_WEDGE_COUNT}')
if len(self.psk.bones) > MAX_BONE_COUNT:
raise RuntimeError(f'Number of bones ({len(self.psk.bones)}) exceeds limit of {MAX_BONE_COUNT}')
if len(self.psk.points) > MAX_POINT_COUNT:
raise RuntimeError(f'Numbers of vertices ({len(self.psk.points)}) exceeds limit of {MAX_POINT_COUNT}')
if len(self.psk.materials) > MAX_MATERIAL_COUNT:
raise RuntimeError(f'Number of materials ({len(self.psk.materials)}) exceeds limit of {MAX_MATERIAL_COUNT}')
with open(path, 'wb') as fp:
self.write_section(fp, b'ACTRHEAD')
self.write_section(fp, b'PNTS0000', Vector3, self.psk.points)
# WEDGES
if len(self.psk.wedges) <= 65536:
wedge_type = Psk.Wedge16
else:
wedge_type = Psk.Wedge32
wedges = []
for index, w in enumerate(self.psk.wedges):
wedge = wedge_type()
wedge = Psk.Wedge16()
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'VTXW0000', Psk.Wedge16, 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)
self.write_section(fp, b'RAWWEIGHTS', Psk.Weight, self.psk.weights)
class PskExportOperator(Operator, ExportHelper):
bl_idname = 'export.psk'
bl_label = 'Export'
__doc__ = 'PSK Exporter (.psk)'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSK file',
maxlen=1024,
default='')
def invoke(self, context, event):
try:
PskBuilder.get_input_objects(context)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
builder = PskBuilder()
psk = builder.build(context)
exporter = PskExporter(psk)
exporter.export(self.filepath)
return {'FINISHED'}

View File

@@ -1,17 +1,25 @@
import os
import bpy
import bmesh
import mathutils
from typing import Optional
from .data import Psk
from mathutils import Quaternion, Vector, Matrix
from .reader import PskReader
from bpy.props import StringProperty
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper
class PskImporter(object):
def __init__(self):
pass
def import_psk(self, psk: Psk, context):
def import_psk(self, psk: Psk, name: str, context):
# ARMATURE
armature_data = bpy.data.armatures.new('armature')
armature_object = bpy.data.objects.new('new_ao', 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)
try:
@@ -24,19 +32,68 @@ class PskImporter(object):
bpy.ops.object.mode_set(mode='EDIT')
for bone in psk.bones:
edit_bone = armature_data.edit_bones.new(bone.name.decode('utf-8'))
edit_bone.parent = armature_data.edit_bones[bone.parent_index]
edit_bone.head = (bone.location.x, bone.location.y, bone.location.z)
rotation = mathutils.Quaternion(*bone.rotation)
edit_bone.tail = edit_bone.head + (mathutils.Vector(0, 0, 1) @ rotation)
# Intermediate bone type for the purpose of construction.
class ImportBone(object):
def __init__(self, index: int, psk_bone: Psk.Bone):
self.index: int = index
self.psk_bone: Psk.Bone = psk_bone
self.parent: Optional[ImportBone] = None
self.local_rotation: Quaternion = Quaternion()
self.local_translation: Vector = Vector()
self.world_rotation_matrix: Matrix = Matrix()
self.world_matrix: Matrix = Matrix()
self.vertex_group = None
import_bones = []
should_invert_root = False
new_bone_size = 8.0
for bone_index, psk_bone in enumerate(psk.bones):
import_bone = ImportBone(bone_index, psk_bone)
psk_bone.parent_index = max(0, psk_bone.parent_index)
import_bone.local_rotation = Quaternion(tuple(psk_bone.rotation))
import_bone.local_translation = Vector(tuple(psk_bone.location))
if psk_bone.parent_index == 0 and bone_index == 0:
if should_invert_root:
import_bone.world_rotation_matrix = import_bone.local_rotation.conjugated().to_matrix()
else:
import_bone.world_rotation_matrix = import_bone.local_rotation.to_matrix()
import_bone.world_matrix = Matrix.Translation(import_bone.local_translation)
import_bones.append(import_bone)
for bone_index, bone in enumerate(import_bones):
if bone.psk_bone.parent_index == 0 and bone_index == 0:
continue
parent = import_bones[bone.psk_bone.parent_index]
bone.parent = parent
bone.world_matrix = parent.world_rotation_matrix.to_4x4()
translation = bone.local_translation.copy()
translation.rotate(parent.world_rotation_matrix)
bone.world_matrix.translation = parent.world_matrix.translation + translation
bone.world_rotation_matrix = bone.local_rotation.conjugated().to_matrix()
bone.world_rotation_matrix.rotate(parent.world_rotation_matrix)
for bone in import_bones:
edit_bone = armature_data.edit_bones.new(bone.psk_bone.name.decode('utf-8'))
if bone.parent is not None:
edit_bone.parent = armature_data.edit_bones[bone.psk_bone.parent_index]
elif not should_invert_root:
bone.local_rotation.conjugate()
post_quat = bone.local_rotation.conjugated()
edit_bone.tail = Vector((0.0, new_bone_size, 0.0))
m = post_quat.copy()
m.rotate(bone.world_matrix)
m = m.to_matrix().to_4x4()
m.translation = bone.world_matrix.translation
edit_bone.matrix = m
# MESH
mesh_data = bpy.data.meshes.new('mesh')
mesh_object = bpy.data.objects.new('new_mo', mesh_data)
mesh_data = bpy.data.meshes.new(name)
mesh_object = bpy.data.objects.new(name, mesh_data)
# MATERIALS
for material in psk.materials:
# TODO: re-use of materials should be an option
bpy_material = bpy.data.materials.new(material.name.decode('utf-8'))
mesh_data.materials.append(bpy_material)
@@ -44,7 +101,7 @@ class PskImporter(object):
# VERTICES
for point in psk.points:
bm.verts.new((point.x, point.y, point.z))
bm.verts.new(tuple(point))
bm.verts.ensure_lookup_table()
@@ -64,8 +121,47 @@ class PskImporter(object):
uv_layer.data[data_index].uv = wedge.u, 1.0 - wedge.v
data_index += 1
bm.normal_update()
bm.free()
# TODO: weights (vertex grorups etc.)
# 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 bone_index in sorted(list(vertex_group_bone_indices)):
import_bones[bone_index].vertex_group = mesh_object.vertex_groups.new(name=import_bones[bone_index].psk_bone.name.decode('windows-1252'))
for weight in psk.weights:
import_bones[weight.bone_index].vertex_group.add((weight.point_index,), weight.weight, 'ADD')
# Add armature modifier to our mesh object.
armature_modifier = mesh_object.modifiers.new(name='Armature', type='ARMATURE')
armature_modifier.object = armature_object
mesh_object.parent = armature_object
context.scene.collection.objects.link(mesh_object)
try:
bpy.ops.object.mode_set(mode='OBJECT')
except:
pass
class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk'
bl_label = 'Export'
__doc__ = 'PSK Importer (.psk)'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSK file',
maxlen=1024,
default='')
def execute(self, context):
reader = PskReader()
psk = reader.read(self.filepath)
name = os.path.splitext(os.path.basename(self.filepath))[0]
PskImporter().import_psk(psk, name, context)
return {'FINISHED'}

View File

@@ -1,57 +0,0 @@
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, FloatProperty
from .builder import PskBuilder
from .exporter import PskExporter
from .reader import PskReader
from .importer import PskImporter
class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk'
bl_label = 'Export'
__doc__ = 'PSK Importer (.psk)'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSK file',
maxlen=1024,
default='')
def execute(self, context):
reader = PskReader()
psk = reader.read(self.filepath)
PskImporter().import_psk(psk, context)
return {'FINISHED'}
class PskExportOperator(Operator, ExportHelper):
bl_idname = 'export.psk'
bl_label = 'Export'
__doc__ = 'PSK Exporter (.psk)'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for exporting the PSK file',
maxlen=1024,
default='')
def invoke(self, context, event):
try:
PskBuilder.get_input_objects(context)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
builder = PskBuilder()
psk = builder.build(context)
exporter = PskExporter(psk)
exporter.export(self.filepath)
return {'FINISHED'}