Added support for collection exporters
This commit is contained in:
@@ -108,7 +108,6 @@ def load_psa_file(context, filepath: str):
|
|||||||
pg.psa_error = str(e)
|
pg.psa_error = str(e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def on_psa_file_path_updated(cls, context):
|
def on_psa_file_path_updated(cls, context):
|
||||||
load_psa_file(context, cls.filepath)
|
load_psa_file(context, cls.filepath)
|
||||||
|
|
||||||
@@ -261,6 +260,7 @@ class PSA_FH_import(FileHandler):
|
|||||||
bl_idname = 'PSA_FH_import'
|
bl_idname = 'PSA_FH_import'
|
||||||
bl_label = 'File handler for Unreal PSA import'
|
bl_label = 'File handler for Unreal PSA import'
|
||||||
bl_import_operator = 'psa_import.import'
|
bl_import_operator = 'psa_import.import'
|
||||||
|
bl_export_operator = 'psa_export.export'
|
||||||
bl_file_extensions = '.psa'
|
bl_file_extensions = '.psa'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Optional
|
|||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Armature, Material
|
from bpy.types import Armature, Material, Collection, Context
|
||||||
|
|
||||||
from .data import *
|
from .data import *
|
||||||
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
||||||
@@ -20,30 +20,28 @@ class PskBuildOptions(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bone_filter_mode = 'ALL'
|
self.bone_filter_mode = 'ALL'
|
||||||
self.bone_collection_indices: List[int] = []
|
self.bone_collection_indices: List[int] = []
|
||||||
self.use_raw_mesh_data = True
|
self.object_eval_state = 'EVALUATED'
|
||||||
self.materials: List[Material] = []
|
self.materials: List[Material] = []
|
||||||
self.should_enforce_bone_name_restrictions = False
|
self.should_enforce_bone_name_restrictions = False
|
||||||
|
|
||||||
|
|
||||||
def get_psk_input_objects(context) -> PskInputObjects:
|
def get_mesh_objects_for_collection(collection: Collection):
|
||||||
input_objects = PskInputObjects()
|
for obj in collection.all_objects:
|
||||||
for selected_object in context.view_layer.objects.selected:
|
if obj.type == 'MESH':
|
||||||
if selected_object.type == 'MESH':
|
yield obj
|
||||||
input_objects.mesh_objects.append(selected_object)
|
|
||||||
|
|
||||||
if len(input_objects.mesh_objects) == 0:
|
|
||||||
raise RuntimeError('At least one mesh must be selected')
|
|
||||||
|
|
||||||
for mesh_object in input_objects.mesh_objects:
|
def get_mesh_objects_for_context(context: Context):
|
||||||
if len(mesh_object.data.materials) == 0:
|
for obj in context.view_layer.objects.selected:
|
||||||
raise RuntimeError(f'Mesh "{mesh_object.name}" must have at least one material')
|
if obj.type == 'MESH':
|
||||||
|
yield obj
|
||||||
|
|
||||||
# Ensure that there are either no armature modifiers (static mesh)
|
|
||||||
# or that there is exactly one armature modifier object shared between
|
def get_armature_for_mesh_objects(mesh_objects: List[Object]) -> Optional[Object]:
|
||||||
# all selected meshes
|
# Ensure that there are either no armature modifiers (static mesh) or that there is exactly one armature modifier
|
||||||
|
# object shared between all meshes.
|
||||||
armature_modifier_objects = set()
|
armature_modifier_objects = set()
|
||||||
|
for mesh_object in mesh_objects:
|
||||||
for mesh_object in input_objects.mesh_objects:
|
|
||||||
modifiers = [x for x in mesh_object.modifiers if x.type == 'ARMATURE']
|
modifiers = [x for x in mesh_object.modifiers if x.type == 'ARMATURE']
|
||||||
if len(modifiers) == 0:
|
if len(modifiers) == 0:
|
||||||
continue
|
continue
|
||||||
@@ -53,21 +51,46 @@ def get_psk_input_objects(context) -> PskInputObjects:
|
|||||||
|
|
||||||
if len(armature_modifier_objects) > 1:
|
if len(armature_modifier_objects) > 1:
|
||||||
armature_modifier_names = [x.name for x in armature_modifier_objects]
|
armature_modifier_names = [x.name for x in armature_modifier_objects]
|
||||||
raise RuntimeError(f'All selected meshes must have the same armature modifier, encountered {len(armature_modifier_names)} ({", ".join(armature_modifier_names)})')
|
raise RuntimeError(
|
||||||
|
f'All meshes must have the same armature modifier, encountered {len(armature_modifier_names)} ({", ".join(armature_modifier_names)})')
|
||||||
elif len(armature_modifier_objects) == 1:
|
elif len(armature_modifier_objects) == 1:
|
||||||
input_objects.armature_object = list(armature_modifier_objects)[0]
|
return list(armature_modifier_objects)[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_psk_input_objects(mesh_objects: List[Object]) -> PskInputObjects:
|
||||||
|
if len(mesh_objects) == 0:
|
||||||
|
raise RuntimeError('At least one mesh must be selected')
|
||||||
|
|
||||||
|
for mesh_object in mesh_objects:
|
||||||
|
if len(mesh_object.data.materials) == 0:
|
||||||
|
raise RuntimeError(f'Mesh "{mesh_object.name}" must have at least one material')
|
||||||
|
|
||||||
|
input_objects = PskInputObjects()
|
||||||
|
input_objects.mesh_objects = mesh_objects
|
||||||
|
input_objects.armature_object = get_armature_for_mesh_objects(mesh_objects)
|
||||||
|
|
||||||
return input_objects
|
return input_objects
|
||||||
|
|
||||||
|
|
||||||
|
def get_psk_input_objects_for_context(context: Context) -> PskInputObjects:
|
||||||
|
mesh_objects = list(get_mesh_objects_for_context(context))
|
||||||
|
return _get_psk_input_objects(mesh_objects)
|
||||||
|
|
||||||
|
|
||||||
|
def get_psk_input_objects_for_collection(collection: Collection) -> PskInputObjects:
|
||||||
|
mesh_objects = list(get_mesh_objects_for_collection(collection))
|
||||||
|
return _get_psk_input_objects(mesh_objects)
|
||||||
|
|
||||||
|
|
||||||
class PskBuildResult(object):
|
class PskBuildResult(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.psk = None
|
self.psk = None
|
||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
def build_psk(context, options: PskBuildOptions) -> PskBuildResult:
|
def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
||||||
input_objects = get_psk_input_objects(context)
|
|
||||||
armature_object: bpy.types.Object = input_objects.armature_object
|
armature_object: bpy.types.Object = input_objects.armature_object
|
||||||
|
|
||||||
result = PskBuildResult()
|
result = PskBuildResult()
|
||||||
@@ -160,47 +183,48 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult:
|
|||||||
material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots]
|
material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots]
|
||||||
|
|
||||||
# MESH DATA
|
# MESH DATA
|
||||||
if options.use_raw_mesh_data:
|
match options.object_eval_state:
|
||||||
mesh_object = input_mesh_object
|
case 'ORIGINAL':
|
||||||
mesh_data = input_mesh_object.data
|
mesh_object = input_mesh_object
|
||||||
else:
|
mesh_data = input_mesh_object.data
|
||||||
# Create a copy of the mesh object after non-armature modifiers are applied.
|
case 'EVALUATED':
|
||||||
|
# Create a copy of the mesh object after non-armature modifiers are applied.
|
||||||
|
|
||||||
# Temporarily force the armature into the rest position.
|
# Temporarily force the armature into the rest position.
|
||||||
# We will undo this later.
|
# We will undo this later.
|
||||||
old_pose_position = None
|
old_pose_position = None
|
||||||
if armature_object is not None:
|
if armature_object is not None:
|
||||||
old_pose_position = armature_object.data.pose_position
|
old_pose_position = armature_object.data.pose_position
|
||||||
armature_object.data.pose_position = 'REST'
|
armature_object.data.pose_position = 'REST'
|
||||||
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
bm = bmesh.new()
|
bm = bmesh.new()
|
||||||
bm.from_object(input_mesh_object, depsgraph)
|
bm.from_object(input_mesh_object, depsgraph)
|
||||||
mesh_data = bpy.data.meshes.new('')
|
mesh_data = bpy.data.meshes.new('')
|
||||||
bm.to_mesh(mesh_data)
|
bm.to_mesh(mesh_data)
|
||||||
del bm
|
del bm
|
||||||
mesh_object = bpy.data.objects.new('', mesh_data)
|
mesh_object = bpy.data.objects.new('', mesh_data)
|
||||||
mesh_object.matrix_world = input_mesh_object.matrix_world
|
mesh_object.matrix_world = input_mesh_object.matrix_world
|
||||||
|
|
||||||
scale = (input_mesh_object.scale.x, input_mesh_object.scale.y, input_mesh_object.scale.z)
|
scale = (input_mesh_object.scale.x, input_mesh_object.scale.y, input_mesh_object.scale.z)
|
||||||
|
|
||||||
# Negative scaling in Blender results in inverted normals after the scale is applied. However, if the scale
|
# Negative scaling in Blender results in inverted normals after the scale is applied. However, if the scale
|
||||||
# is not applied, the normals will appear unaffected in the viewport. The evaluated mesh data used in the
|
# is not applied, the normals will appear unaffected in the viewport. The evaluated mesh data used in the
|
||||||
# export will have the scale applied, but this behavior is not obvious to the user.
|
# export will have the scale applied, but this behavior is not obvious to the user.
|
||||||
#
|
#
|
||||||
# In order to have the exporter be as WYSIWYG as possible, we need to check for negative scaling and invert
|
# In order to have the exporter be as WYSIWYG as possible, we need to check for negative scaling and invert
|
||||||
# the normals if necessary. If two axes have negative scaling and the third has positive scaling, the
|
# the normals if necessary. If two axes have negative scaling and the third has positive scaling, the
|
||||||
# normals will be correct. We can detect this by checking if the number of negative scaling axes is odd. If
|
# normals will be correct. We can detect this by checking if the number of negative scaling axes is odd. If
|
||||||
# it is, we need to invert the normals of the mesh by swapping the order of the vertices in each face.
|
# it is, we need to invert the normals of the mesh by swapping the order of the vertices in each face.
|
||||||
should_flip_normals = sum(1 for x in scale if x < 0) % 2 == 1
|
should_flip_normals = sum(1 for x in scale if x < 0) % 2 == 1
|
||||||
|
|
||||||
# Copy the vertex groups
|
# Copy the vertex groups
|
||||||
for vertex_group in input_mesh_object.vertex_groups:
|
for vertex_group in input_mesh_object.vertex_groups:
|
||||||
mesh_object.vertex_groups.new(name=vertex_group.name)
|
mesh_object.vertex_groups.new(name=vertex_group.name)
|
||||||
|
|
||||||
# Restore the previous pose position on the armature.
|
# Restore the previous pose position on the armature.
|
||||||
if old_pose_position is not None:
|
if old_pose_position is not None:
|
||||||
armature_object.data.pose_position = old_pose_position
|
armature_object.data.pose_position = old_pose_position
|
||||||
|
|
||||||
vertex_offset = len(psk.points)
|
vertex_offset = len(psk.points)
|
||||||
|
|
||||||
@@ -305,7 +329,7 @@ def build_psk(context, options: PskBuildOptions) -> PskBuildResult:
|
|||||||
w.weight = weight
|
w.weight = weight
|
||||||
psk.weights.append(w)
|
psk.weights.append(w)
|
||||||
|
|
||||||
if not options.use_raw_mesh_data:
|
if options.object_eval_state == 'EVALUATED':
|
||||||
bpy.data.objects.remove(mesh_object)
|
bpy.data.objects.remove(mesh_object)
|
||||||
bpy.data.meshes.remove(mesh_data)
|
bpy.data.meshes.remove(mesh_data)
|
||||||
del mesh_data
|
del mesh_data
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
from bpy.props import StringProperty
|
from typing import List
|
||||||
from bpy.types import Operator
|
|
||||||
|
import bpy
|
||||||
|
from bpy.props import StringProperty, BoolProperty, EnumProperty
|
||||||
|
from bpy.types import Operator, Context, Object
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
from ..builder import build_psk, PskBuildOptions, get_psk_input_objects
|
from .properties import object_eval_state_items
|
||||||
|
from ..builder import build_psk, PskBuildOptions, get_psk_input_objects_for_context, \
|
||||||
|
get_psk_input_objects_for_collection
|
||||||
from ..writer import write_psk
|
from ..writer import write_psk
|
||||||
from ...shared.helpers import populate_bone_collection_list
|
from ...shared.helpers import populate_bone_collection_list
|
||||||
|
|
||||||
|
|
||||||
def is_bone_filter_mode_item_available(context, identifier):
|
def is_bone_filter_mode_item_available(context, identifier):
|
||||||
input_objects = get_psk_input_objects(context)
|
input_objects = get_psk_input_objects_for_context(context)
|
||||||
armature_object = input_objects.armature_object
|
armature_object = input_objects.armature_object
|
||||||
if identifier == 'BONE_COLLECTIONS':
|
if identifier == 'BONE_COLLECTIONS':
|
||||||
if armature_object is None or armature_object.data is None or len(armature_object.data.collections) == 0:
|
if armature_object is None or armature_object.data is None or len(armature_object.data.collections) == 0:
|
||||||
return False
|
return False
|
||||||
# else if... you can set up other conditions if you add more options
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def populate_material_list(mesh_objects, material_list):
|
def get_materials_for_mesh_objects(mesh_objects: List[Object]):
|
||||||
material_list.clear()
|
|
||||||
|
|
||||||
materials = []
|
materials = []
|
||||||
for mesh_object in mesh_objects:
|
for mesh_object in mesh_objects:
|
||||||
for i, material_slot in enumerate(mesh_object.material_slots):
|
for i, material_slot in enumerate(mesh_object.material_slots):
|
||||||
material = material_slot.material
|
material = material_slot.material
|
||||||
# TODO: put this in the poll arg?
|
|
||||||
if material is None:
|
if material is None:
|
||||||
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
||||||
if material not in materials:
|
if material not in materials:
|
||||||
materials.append(material)
|
materials.append(material)
|
||||||
|
return materials
|
||||||
|
|
||||||
|
def populate_material_list(mesh_objects, material_list):
|
||||||
|
materials = get_materials_for_mesh_objects(mesh_objects)
|
||||||
|
material_list.clear()
|
||||||
for index, material in enumerate(materials):
|
for index, material in enumerate(materials):
|
||||||
m = material_list.add()
|
m = material_list.add()
|
||||||
m.material = material
|
m.material = material
|
||||||
@@ -72,6 +77,84 @@ class PSK_OT_material_list_move_down(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class PSK_OT_export_collection(Operator, ExportHelper):
|
||||||
|
bl_idname = 'export.psk_collection'
|
||||||
|
bl_label = 'Export'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
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='',
|
||||||
|
subtype='FILE_PATH')
|
||||||
|
collection: StringProperty(options={'HIDDEN'})
|
||||||
|
|
||||||
|
object_eval_state: EnumProperty(
|
||||||
|
items=object_eval_state_items,
|
||||||
|
name='Object Evaluation State',
|
||||||
|
default='EVALUATED'
|
||||||
|
)
|
||||||
|
should_enforce_bone_name_restrictions: BoolProperty(
|
||||||
|
default=False,
|
||||||
|
name='Enforce Bone Name Restrictions',
|
||||||
|
description='Enforce that bone names must only contain letters, numbers, spaces, hyphens and underscores.\n\n'
|
||||||
|
'Depending on the engine, improper bone names might not be referenced correctly by scripts'
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
collection = bpy.data.collections.get(self.collection)
|
||||||
|
|
||||||
|
try:
|
||||||
|
input_objects = get_psk_input_objects_for_collection(collection)
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
options = PskBuildOptions()
|
||||||
|
options.bone_filter_mode = 'ALL'
|
||||||
|
options.object_eval_state = self.object_eval_state
|
||||||
|
options.materials = get_materials_for_mesh_objects(input_objects.mesh_objects)
|
||||||
|
options.should_enforce_bone_name_restrictions = self.should_enforce_bone_name_restrictions
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = build_psk(context, input_objects, options)
|
||||||
|
for warning in result.warnings:
|
||||||
|
self.report({'WARNING'}, warning)
|
||||||
|
write_psk(result.psk, self.filepath)
|
||||||
|
if len(result.warnings) > 0:
|
||||||
|
self.report({'WARNING'}, f'PSK export successful with {len(result.warnings)} warnings')
|
||||||
|
else:
|
||||||
|
self.report({'INFO'}, f'PSK export successful')
|
||||||
|
except RuntimeError as e:
|
||||||
|
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def draw(self, context: Context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
# MESH
|
||||||
|
mesh_header, mesh_panel = layout.panel('Mesh', default_closed=False)
|
||||||
|
mesh_header.label(text='Mesh', icon='MESH_DATA')
|
||||||
|
if mesh_panel:
|
||||||
|
flow = mesh_panel.grid_flow(row_major=True)
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(self, 'object_eval_state', text='Data')
|
||||||
|
|
||||||
|
# BONES
|
||||||
|
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
||||||
|
bones_header.label(text='Bones', icon='BONE_DATA')
|
||||||
|
if bones_panel:
|
||||||
|
flow = bones_panel.grid_flow(row_major=True)
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(self, 'should_enforce_bone_name_restrictions')
|
||||||
|
|
||||||
|
|
||||||
class PSK_OT_export(Operator, ExportHelper):
|
class PSK_OT_export(Operator, ExportHelper):
|
||||||
bl_idname = 'export.psk'
|
bl_idname = 'export.psk'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
@@ -88,7 +171,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
try:
|
try:
|
||||||
input_objects = get_psk_input_objects(context)
|
input_objects = get_psk_input_objects_for_context(context)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
@@ -110,7 +193,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
try:
|
try:
|
||||||
get_psk_input_objects(context)
|
get_psk_input_objects_for_context(context)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
cls.poll_message_set(str(e))
|
cls.poll_message_set(str(e))
|
||||||
return False
|
return False
|
||||||
@@ -118,16 +201,20 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
pg = getattr(context.scene, 'psk_export')
|
pg = getattr(context.scene, 'psk_export')
|
||||||
|
|
||||||
# MESH
|
# MESH
|
||||||
mesh_header, mesh_panel = layout.panel('01_mesh', default_closed=False)
|
mesh_header, mesh_panel = layout.panel('Mesh', default_closed=False)
|
||||||
mesh_header.label(text='Mesh', icon='MESH_DATA')
|
mesh_header.label(text='Mesh', icon='MESH_DATA')
|
||||||
if mesh_panel:
|
if mesh_panel:
|
||||||
mesh_panel.prop(pg, 'use_raw_mesh_data')
|
flow = mesh_panel.grid_flow(row_major=True)
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(pg, 'object_eval_state', text='Data')
|
||||||
|
|
||||||
# BONES
|
# BONES
|
||||||
bones_header, bones_panel = layout.panel('02_bones', default_closed=False)
|
bones_header, bones_panel = layout.panel('Bones', default_closed=False)
|
||||||
bones_header.label(text='Bones', icon='BONE_DATA')
|
bones_header.label(text='Bones', icon='BONE_DATA')
|
||||||
if bones_panel:
|
if bones_panel:
|
||||||
bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static
|
bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static
|
||||||
@@ -146,7 +233,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
bones_panel.prop(pg, 'should_enforce_bone_name_restrictions')
|
bones_panel.prop(pg, 'should_enforce_bone_name_restrictions')
|
||||||
|
|
||||||
# MATERIALS
|
# MATERIALS
|
||||||
materials_header, materials_panel = layout.panel('03_materials', default_closed=False)
|
materials_header, materials_panel = layout.panel('Materials', default_closed=False)
|
||||||
materials_header.label(text='Materials', icon='MATERIAL')
|
materials_header.label(text='Materials', icon='MATERIAL')
|
||||||
if materials_panel:
|
if materials_panel:
|
||||||
row = materials_panel.row()
|
row = materials_panel.row()
|
||||||
@@ -157,16 +244,19 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = context.scene.psk_export
|
pg = getattr(context.scene, 'psk_export')
|
||||||
|
|
||||||
|
input_objects = get_psk_input_objects_for_context(context)
|
||||||
|
|
||||||
options = PskBuildOptions()
|
options = PskBuildOptions()
|
||||||
options.bone_filter_mode = pg.bone_filter_mode
|
options.bone_filter_mode = pg.bone_filter_mode
|
||||||
options.bone_collection_indices = [x.index for x in pg.bone_collection_list if x.is_selected]
|
options.bone_collection_indices = [x.index for x in pg.bone_collection_list if x.is_selected]
|
||||||
options.use_raw_mesh_data = pg.use_raw_mesh_data
|
options.object_eval_state = pg.object_eval_state
|
||||||
options.materials = [m.material for m in pg.material_list]
|
options.materials = [m.material for m in pg.material_list]
|
||||||
options.should_enforce_bone_name_restrictions = pg.should_enforce_bone_name_restrictions
|
options.should_enforce_bone_name_restrictions = pg.should_enforce_bone_name_restrictions
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = build_psk(context, options)
|
result = build_psk(context, input_objects, options)
|
||||||
for warning in result.warnings:
|
for warning in result.warnings:
|
||||||
self.report({'WARNING'}, warning)
|
self.report({'WARNING'}, warning)
|
||||||
write_psk(result.psk, self.filepath)
|
write_psk(result.psk, self.filepath)
|
||||||
@@ -185,4 +275,5 @@ classes = (
|
|||||||
PSK_OT_material_list_move_up,
|
PSK_OT_material_list_move_up,
|
||||||
PSK_OT_material_list_move_down,
|
PSK_OT_material_list_move_down,
|
||||||
PSK_OT_export,
|
PSK_OT_export,
|
||||||
|
PSK_OT_export_collection,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ from ...shared.types import PSX_PG_bone_collection_list_item
|
|||||||
|
|
||||||
empty_set = set()
|
empty_set = set()
|
||||||
|
|
||||||
|
|
||||||
|
object_eval_state_items = (
|
||||||
|
('EVALUATED', 'Evaluated', 'Use data from fully evaluated object'),
|
||||||
|
('ORIGINAL', 'Original', 'Use data from original object with no modifiers applied'),
|
||||||
|
)
|
||||||
|
|
||||||
class PSK_PG_material_list_item(PropertyGroup):
|
class PSK_PG_material_list_item(PropertyGroup):
|
||||||
material: PointerProperty(type=Material)
|
material: PointerProperty(type=Material)
|
||||||
index: IntProperty()
|
index: IntProperty()
|
||||||
@@ -23,7 +29,11 @@ class PSK_PG_export(PropertyGroup):
|
|||||||
)
|
)
|
||||||
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
||||||
bone_collection_list_index: IntProperty(default=0)
|
bone_collection_list_index: IntProperty(default=0)
|
||||||
use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh')
|
object_eval_state: EnumProperty(
|
||||||
|
items=object_eval_state_items,
|
||||||
|
name='Object Evaluation State',
|
||||||
|
default='EVALUATED'
|
||||||
|
)
|
||||||
material_list: CollectionProperty(type=PSK_PG_material_list_item)
|
material_list: CollectionProperty(type=PSK_PG_material_list_item)
|
||||||
material_list_index: IntProperty(default=0)
|
material_list_index: IntProperty(default=0)
|
||||||
should_enforce_bone_name_restrictions: BoolProperty(
|
should_enforce_bone_name_restrictions: BoolProperty(
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ empty_set = set()
|
|||||||
|
|
||||||
class PSK_FH_import(FileHandler):
|
class PSK_FH_import(FileHandler):
|
||||||
bl_idname = 'PSK_FH_import'
|
bl_idname = 'PSK_FH_import'
|
||||||
bl_label = 'File handler for Unreal PSK/PSKX import'
|
bl_label = 'Unreal PSK'
|
||||||
bl_import_operator = 'import_scene.psk'
|
bl_import_operator = 'import_scene.psk'
|
||||||
|
bl_export_operator = 'export.psk_collection'
|
||||||
bl_file_extensions = '.psk;.pskx'
|
bl_file_extensions = '.psk;.pskx'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from ctypes import Structure, sizeof
|
from ctypes import Structure, sizeof
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ def write_psk(psk: Psk, path: str):
|
|||||||
elif len(psk.bones) == 0:
|
elif len(psk.bones) == 0:
|
||||||
raise RuntimeError(f'At least one bone must be marked for export')
|
raise RuntimeError(f'At least one bone must be marked for export')
|
||||||
|
|
||||||
|
# Make the directory for the file if it doesn't exist.
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
|
||||||
with open(path, 'wb') as fp:
|
with open(path, 'wb') as fp:
|
||||||
_write_section(fp, b'ACTRHEAD')
|
_write_section(fp, b'ACTRHEAD')
|
||||||
_write_section(fp, b'PNTS0000', Vector3, psk.points)
|
_write_section(fp, b'PNTS0000', Vector3, psk.points)
|
||||||
|
|||||||
Reference in New Issue
Block a user