2024-02-06 13:26:48 -08:00
from typing import Optional
2022-11-01 11:32:25 -07:00
import bmesh
import bpy
2024-02-06 13:26:48 -08:00
import numpy as np
2024-02-28 00:51:33 -08:00
from bpy . types import Armature , Material
2022-04-24 22:08:36 -07:00
2019-12-10 02:52:09 -08:00
from . data import *
2024-02-29 00:32:42 -08:00
from . properties import triangle_type_and_bit_flags_to_poly_flags
2024-05-27 14:56:37 -07:00
from . . shared . helpers import *
2019-12-01 18:18:05 -08:00
2021-08-02 22:42:39 -07:00
class PskInputObjects ( object ) :
def __init__ ( self ) :
self . mesh_objects = [ ]
2024-02-06 13:26:48 -08:00
self . armature_object : Optional [ Object ] = None
2021-08-02 22:42:39 -07:00
2021-09-07 21:40:42 -07:00
2022-06-27 18:10:37 -07:00
class PskBuildOptions ( object ) :
2022-01-22 18:11:41 -08:00
def __init__ ( self ) :
self . bone_filter_mode = ' ALL '
2023-09-17 21:18:41 -07:00
self . bone_collection_indices : List [ int ] = [ ]
2022-05-11 15:57:22 -07:00
self . use_raw_mesh_data = True
2024-02-28 00:51:33 -08:00
self . materials : List [ Material ] = [ ]
2023-07-29 20:51:23 -07:00
self . should_enforce_bone_name_restrictions = False
2022-01-22 18:11:41 -08:00
2022-06-27 18:10:37 -07:00
def get_psk_input_objects ( context ) - > PskInputObjects :
input_objects = PskInputObjects ( )
for selected_object in context . view_layer . objects . selected :
if selected_object . type != ' MESH ' :
raise RuntimeError ( f ' Selected object " { selected_object . name } " is not a mesh ' )
input_objects . mesh_objects = context . view_layer . objects . selected
2019-12-01 18:18:05 -08:00
2022-06-27 18:10:37 -07:00
if len ( input_objects . mesh_objects ) == 0 :
raise RuntimeError ( ' At least one mesh must be selected ' )
2021-08-02 22:42:39 -07:00
2022-06-27 18:10:37 -07:00
for mesh_object in input_objects . mesh_objects :
if len ( mesh_object . data . materials ) == 0 :
raise RuntimeError ( f ' Mesh " { mesh_object . name } " must have at least one material ' )
2021-08-02 22:42:39 -07:00
2022-06-27 18:10:37 -07:00
# Ensure that there are either no armature modifiers (static mesh)
# or that there is exactly one armature modifier object shared between
# all selected meshes
armature_modifier_objects = set ( )
2019-12-02 01:53:32 -08:00
2022-06-27 18:10:37 -07:00
for mesh_object in input_objects . mesh_objects :
modifiers = [ x for x in mesh_object . modifiers if x . type == ' ARMATURE ' ]
if len ( modifiers ) == 0 :
continue
elif len ( modifiers ) > 1 :
raise RuntimeError ( f ' Mesh " { mesh_object . name } " must have only one armature modifier ' )
armature_modifier_objects . add ( modifiers [ 0 ] . object )
2019-12-01 18:18:05 -08:00
2022-06-27 18:10:37 -07:00
if len ( armature_modifier_objects ) > 1 :
2023-07-22 17:09:28 -07:00
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 ) } ) ' )
2022-06-27 18:10:37 -07:00
elif len ( armature_modifier_objects ) == 1 :
input_objects . armature_object = list ( armature_modifier_objects ) [ 0 ]
2019-12-03 01:27:50 -08:00
2022-06-27 18:10:37 -07:00
return input_objects
2019-12-03 01:27:50 -08:00
2019-12-01 18:18:05 -08:00
2023-09-30 00:53:37 -07:00
class PskBuildResult ( object ) :
def __init__ ( self ) :
self . psk = None
2024-02-06 13:26:48 -08:00
self . warnings : List [ str ] = [ ]
2023-09-30 00:53:37 -07:00
2019-12-01 18:18:05 -08:00
2023-09-30 00:53:37 -07:00
def build_psk ( context , options : PskBuildOptions ) - > PskBuildResult :
input_objects = get_psk_input_objects ( context )
2022-11-22 12:57:06 -08:00
armature_object : bpy . types . Object = input_objects . armature_object
2021-08-01 13:30:14 -07:00
2023-09-30 00:53:37 -07:00
result = PskBuildResult ( )
2022-06-27 18:10:37 -07:00
psk = Psk ( )
bones = [ ]
2022-01-22 18:11:41 -08:00
2024-03-01 15:14:37 -08:00
if armature_object is None or len ( armature_object . data . bones ) == 0 :
# If the mesh has no armature object or no bones, simply assign it a dummy bone at the root to satisfy the
# requirement that a PSK file must have at least one bone.
2022-06-27 18:10:37 -07:00
psk_bone = Psk . Bone ( )
psk_bone . name = bytes ( ' root ' , encoding = ' windows-1252 ' )
psk_bone . flags = 0
psk_bone . children_count = 0
psk_bone . parent_index = 0
psk_bone . location = Vector3 . zero ( )
psk_bone . rotation = Quaternion . identity ( )
psk . bones . append ( psk_bone )
else :
2023-09-17 21:18:41 -07:00
bone_names = get_export_bone_names ( armature_object , options . bone_filter_mode , options . bone_collection_indices )
2022-11-22 12:57:06 -08:00
armature_data = typing . cast ( Armature , armature_object . data )
bones = [ armature_data . bones [ bone_name ] for bone_name in bone_names ]
2021-08-01 13:30:14 -07:00
2022-06-27 18:10:37 -07:00
# Check that all bone names are valid.
2023-07-29 20:51:23 -07:00
if options . should_enforce_bone_name_restrictions :
2022-11-01 11:28:55 -07:00
check_bone_names ( map ( lambda x : x . name , bones ) )
2022-06-27 18:10:37 -07:00
for bone in bones :
2019-12-09 21:18:51 -08:00
psk_bone = Psk . Bone ( )
2023-08-18 18:20:29 -07:00
try :
psk_bone . name = bytes ( bone . name , encoding = ' windows-1252 ' )
except UnicodeEncodeError :
raise RuntimeError (
f ' Bone name " { bone . name } " contains characters that cannot be encoded in the Windows-1252 codepage ' )
2019-12-09 21:18:51 -08:00
psk_bone . flags = 0
2021-08-14 18:00:16 -07:00
psk_bone . children_count = 0
2022-01-22 18:11:41 -08:00
2022-06-27 18:10:37 -07:00
try :
parent_index = bones . index ( bone . parent )
psk_bone . parent_index = parent_index
psk . bones [ parent_index ] . children_count + = 1
except ValueError :
2023-07-28 03:18:05 -07:00
psk_bone . parent_index = 0
2022-06-27 18:10:37 -07:00
if bone . parent is not None :
2022-06-27 18:12:21 -07:00
rotation = bone . matrix . to_quaternion ( ) . conjugated ( )
2022-11-22 12:57:06 -08:00
inverse_parent_rotation = bone . parent . matrix . to_quaternion ( ) . inverted ( )
parent_head = inverse_parent_rotation @ bone . parent . head
parent_tail = inverse_parent_rotation @ bone . parent . tail
2022-06-27 18:10:37 -07:00
location = ( parent_tail - parent_head ) + bone . head
else :
2022-11-22 12:55:56 -08:00
armature_local_matrix = armature_object . matrix_local
location = armature_local_matrix @ bone . head
bone_rotation = bone . matrix . to_quaternion ( ) . conjugated ( )
local_rotation = armature_local_matrix . to_3x3 ( ) . to_quaternion ( ) . conjugated ( )
rotation = bone_rotation @ local_rotation
rotation . conjugate ( )
2022-06-27 18:10:37 -07:00
psk_bone . location . x = location . x
psk_bone . location . y = location . y
psk_bone . location . z = location . z
2022-05-20 17:13:12 -07:00
2022-07-01 19:47:01 -07:00
psk_bone . rotation . w = rotation . w
2022-06-27 18:10:37 -07:00
psk_bone . rotation . x = rotation . x
psk_bone . rotation . y = rotation . y
psk_bone . rotation . z = rotation . z
2021-08-14 18:00:16 -07:00
2022-06-27 18:10:37 -07:00
psk . bones . append ( psk_bone )
2022-07-01 19:47:01 -07:00
# MATERIALS
2024-02-28 00:51:33 -08:00
for material in options . materials :
2022-07-01 19:47:01 -07:00
psk_material = Psk . Material ( )
2023-08-18 18:20:29 -07:00
try :
2024-02-28 00:51:33 -08:00
psk_material . name = bytes ( material . name , encoding = ' windows-1252 ' )
2023-08-18 18:20:29 -07:00
except UnicodeEncodeError :
2024-02-28 00:51:33 -08:00
raise RuntimeError ( f ' Material name " { material . name } " contains characters that cannot be encoded in the Windows-1252 codepage ' )
2022-07-01 19:47:01 -07:00
psk_material . texture_index = len ( psk . materials )
2024-02-29 00:32:42 -08:00
psk_material . poly_flags = triangle_type_and_bit_flags_to_poly_flags ( material . psk . mesh_triangle_type ,
material . psk . mesh_triangle_bit_flags )
2022-07-01 19:47:01 -07:00
psk . materials . append ( psk_material )
2023-11-22 19:20:16 -08:00
context . window_manager . progress_begin ( 0 , len ( input_objects . mesh_objects ) )
2024-02-28 00:51:33 -08:00
material_names = [ m . name for m in options . materials ]
2023-11-22 19:20:16 -08:00
for object_index , input_mesh_object in enumerate ( input_objects . mesh_objects ) :
2022-06-27 18:10:37 -07:00
2024-02-06 13:18:19 -08:00
should_flip_normals = False
2022-06-27 18:10:37 -07:00
# MATERIALS
2023-08-18 18:22:16 -07:00
material_indices = [ material_names . index ( material_slot . material . name ) for material_slot in input_mesh_object . material_slots ]
2022-06-27 18:10:37 -07:00
2022-11-01 11:32:25 -07:00
# MESH DATA
2022-06-27 18:10:37 -07:00
if options . use_raw_mesh_data :
mesh_object = input_mesh_object
mesh_data = input_mesh_object . data
else :
# Create a copy of the mesh object after non-armature modifiers are applied.
2022-11-01 11:32:25 -07:00
# Temporarily force the armature into the rest position.
# We will undo this later.
2023-09-05 23:35:39 -07:00
old_pose_position = None
if armature_object is not None :
old_pose_position = armature_object . data . pose_position
armature_object . data . pose_position = ' REST '
2022-06-27 18:10:37 -07:00
depsgraph = context . evaluated_depsgraph_get ( )
bm = bmesh . new ( )
bm . from_object ( input_mesh_object , depsgraph )
mesh_data = bpy . data . meshes . new ( ' ' )
bm . to_mesh ( mesh_data )
del bm
mesh_object = bpy . data . objects . new ( ' ' , mesh_data )
mesh_object . matrix_world = input_mesh_object . matrix_world
2023-09-30 00:53:37 -07:00
scale = ( input_mesh_object . scale . x , input_mesh_object . scale . y , input_mesh_object . scale . z )
2024-02-06 13:18:19 -08:00
# 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
# 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
# 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
# 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
2023-09-30 00:53:37 -07:00
2022-06-27 18:10:37 -07:00
# Copy the vertex groups
for vertex_group in input_mesh_object . vertex_groups :
mesh_object . vertex_groups . new ( name = vertex_group . name )
2022-11-01 11:32:25 -07:00
# Restore the previous pose position on the armature.
2023-09-05 23:35:39 -07:00
if old_pose_position is not None :
armature_object . data . pose_position = old_pose_position
2022-06-27 18:10:37 -07:00
vertex_offset = len ( psk . points )
# VERTICES
for vertex in mesh_data . vertices :
point = Vector3 ( )
v = mesh_object . matrix_world @ vertex . co
point . x = v . x
point . y = v . y
point . z = v . z
psk . points . append ( point )
uv_layer = mesh_data . uv_layers . active . data
# WEDGES
mesh_data . calc_loop_triangles ( )
# Build a list of non-unique wedges.
wedges = [ ]
for loop_index , loop in enumerate ( mesh_data . loops ) :
2024-02-06 13:26:48 -08:00
wedges . append ( Psk . Wedge (
point_index = loop . vertex_index + vertex_offset ,
u = uv_layer [ loop_index ] . uv [ 0 ] ,
v = 1.0 - uv_layer [ loop_index ] . uv [ 1 ]
) )
2022-06-27 18:10:37 -07:00
# Assign material indices to the wedges.
for triangle in mesh_data . loop_triangles :
for loop_index in triangle . loops :
wedges [ loop_index ] . material_index = material_indices [ triangle . material_index ]
# Populate the list of wedges with unique wedges & build a look-up table of loop indices to wedge indices
2024-02-06 13:26:48 -08:00
wedge_indices = dict ( )
loop_wedge_indices = np . full ( len ( mesh_data . loops ) , - 1 )
2022-06-27 18:10:37 -07:00
for loop_index , wedge in enumerate ( wedges ) :
wedge_hash = hash ( wedge )
if wedge_hash in wedge_indices :
loop_wedge_indices [ loop_index ] = wedge_indices [ wedge_hash ]
else :
wedge_index = len ( psk . wedges )
wedge_indices [ wedge_hash ] = wedge_index
psk . wedges . append ( wedge )
loop_wedge_indices [ loop_index ] = wedge_index
# FACES
poly_groups , groups = mesh_data . calc_smooth_groups ( use_bitflags = True )
2024-02-06 13:18:19 -08:00
psk_face_start_index = len ( psk . faces )
2022-06-27 18:10:37 -07:00
for f in mesh_data . loop_triangles :
face = Psk . Face ( )
face . material_index = material_indices [ f . material_index ]
face . wedge_indices [ 0 ] = loop_wedge_indices [ f . loops [ 2 ] ]
face . wedge_indices [ 1 ] = loop_wedge_indices [ f . loops [ 1 ] ]
face . wedge_indices [ 2 ] = loop_wedge_indices [ f . loops [ 0 ] ]
face . smoothing_groups = poly_groups [ f . polygon_index ]
psk . faces . append ( face )
2024-02-06 13:18:19 -08:00
if should_flip_normals :
# Invert the normals of the faces.
for face in psk . faces [ psk_face_start_index : ] :
face . wedge_indices [ 0 ] , face . wedge_indices [ 2 ] = face . wedge_indices [ 2 ] , face . wedge_indices [ 0 ]
2022-06-27 18:10:37 -07:00
# WEIGHTS
if armature_object is not None :
2022-11-22 12:57:06 -08:00
armature_data = typing . cast ( Armature , armature_object . data )
2022-06-27 18:10:37 -07:00
# Because the vertex groups may contain entries for which there is no matching bone in the armature,
# we must filter them out and not export any weights for these vertex groups.
bone_names = [ x . name for x in bones ]
vertex_group_names = [ x . name for x in mesh_object . vertex_groups ]
vertex_group_bone_indices = dict ( )
for vertex_group_index , vertex_group_name in enumerate ( vertex_group_names ) :
2021-08-14 18:00:16 -07:00
try :
2022-06-27 18:10:37 -07:00
vertex_group_bone_indices [ vertex_group_index ] = bone_names . index ( vertex_group_name )
2021-08-14 18:00:16 -07:00
except ValueError :
2022-06-27 18:10:37 -07:00
# The vertex group does not have a matching bone in the list of bones to be exported.
# Check to see if there is an associated bone for this vertex group that exists in the armature.
# If there is, we can traverse the ancestors of that bone to find an alternate bone to use for
# weighting the vertices belonging to this vertex group.
2022-11-22 12:57:06 -08:00
if vertex_group_name in armature_data . bones :
bone = armature_data . bones [ vertex_group_name ]
2022-06-27 18:10:37 -07:00
while bone is not None :
try :
bone_index = bone_names . index ( bone . name )
vertex_group_bone_indices [ vertex_group_index ] = bone_index
break
except ValueError :
bone = bone . parent
for vertex_group_index , vertex_group in enumerate ( mesh_object . vertex_groups ) :
if vertex_group_index not in vertex_group_bone_indices :
# Vertex group has no associated bone, skip it.
continue
bone_index = vertex_group_bone_indices [ vertex_group_index ]
for vertex_index in range ( len ( mesh_data . vertices ) ) :
2022-01-22 18:11:41 -08:00
try :
2022-06-27 18:10:37 -07:00
weight = vertex_group . weight ( vertex_index )
except RuntimeError :
continue
if weight == 0.0 :
2022-01-22 18:11:41 -08:00
continue
2022-06-27 18:10:37 -07:00
w = Psk . Weight ( )
w . bone_index = bone_index
w . point_index = vertex_offset + vertex_index
w . weight = weight
psk . weights . append ( w )
if not options . use_raw_mesh_data :
bpy . data . objects . remove ( mesh_object )
bpy . data . meshes . remove ( mesh_data )
del mesh_data
2023-11-22 19:20:16 -08:00
context . window_manager . progress_update ( object_index )
context . window_manager . progress_end ( )
2023-09-30 00:53:37 -07:00
result . psk = psk
return result