Multi-armature mesh and animation export now working
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from bpy.types import Bone, Action, PoseBone
|
from bpy.types import Action, PoseBone
|
||||||
|
|
||||||
from .data import *
|
from .data import Psa
|
||||||
from ..shared.helpers import *
|
from ..shared.helpers import *
|
||||||
|
|
||||||
|
|
||||||
@@ -23,10 +23,11 @@ class PsaBuildSequence:
|
|||||||
|
|
||||||
class PsaBuildOptions:
|
class PsaBuildOptions:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.armature_objects: List[Object] = []
|
||||||
self.animation_data: Optional[AnimData] = None
|
self.animation_data: Optional[AnimData] = None
|
||||||
self.sequences: List[PsaBuildSequence] = []
|
self.sequences: List[PsaBuildSequence] = []
|
||||||
self.bone_filter_mode: str = 'ALL'
|
self.bone_filter_mode: str = 'ALL'
|
||||||
self.bone_collection_indices: List[int] = []
|
self.bone_collection_indices: List[Tuple[str, int]] = []
|
||||||
self.sequence_name_prefix: str = ''
|
self.sequence_name_prefix: str = ''
|
||||||
self.sequence_name_suffix: str = ''
|
self.sequence_name_suffix: str = ''
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
@@ -34,90 +35,95 @@ class PsaBuildOptions:
|
|||||||
self.export_space = 'WORLD'
|
self.export_space = 'WORLD'
|
||||||
self.forward_axis = 'X'
|
self.forward_axis = 'X'
|
||||||
self.up_axis = 'Z'
|
self.up_axis = 'Z'
|
||||||
|
self.root_bone_name = 'ROOT'
|
||||||
|
|
||||||
|
|
||||||
def _get_pose_bone_location_and_rotation(
|
def _get_pose_bone_location_and_rotation(
|
||||||
pose_bone: PoseBone,
|
pose_bone: Optional[PoseBone],
|
||||||
armature_object: Object,
|
armature_object: Optional[Object],
|
||||||
export_space: str,
|
export_space: str,
|
||||||
scale: Vector,
|
scale: Vector,
|
||||||
coordinate_system_transform: Matrix) -> Tuple[Vector, Quaternion]:
|
coordinate_system_transform: Matrix,
|
||||||
if pose_bone.parent is not None:
|
has_false_root_bone: bool,
|
||||||
|
) -> Tuple[Vector, Quaternion]:
|
||||||
|
# TODO: my kingdom for a Rust monad.
|
||||||
|
is_false_root_bone = pose_bone is None and armature_object is None
|
||||||
|
if is_false_root_bone:
|
||||||
|
pose_bone_matrix = coordinate_system_transform
|
||||||
|
elif pose_bone.parent is not None:
|
||||||
pose_bone_matrix = pose_bone.matrix
|
pose_bone_matrix = pose_bone.matrix
|
||||||
pose_bone_parent_matrix = pose_bone.parent.matrix
|
pose_bone_parent_matrix = pose_bone.parent.matrix
|
||||||
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
|
pose_bone_matrix = pose_bone_parent_matrix.inverted() @ pose_bone_matrix
|
||||||
else:
|
else:
|
||||||
# Get the bone's pose matrix and transform it into the export space.
|
# Root bone
|
||||||
# In the case of an 'ARMATURE' export space, this will be the inverse of armature object's world matrix.
|
if has_false_root_bone:
|
||||||
# Otherwise, it will be the identity matrix.
|
pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix
|
||||||
# TODO: taking the pose bone matrix puts this in armature space.
|
else:
|
||||||
pose_bone_matrix = Matrix.Identity(4)
|
# Get the bone's pose matrix and transform it into the export space.
|
||||||
match export_space:
|
# In the case of an 'ARMATURE' export space, this will be the inverse of armature object's world matrix.
|
||||||
case 'ARMATURE':
|
# Otherwise, it will be the identity matrix.
|
||||||
pose_bone_matrix = pose_bone.matrix
|
# TODO: taking the pose bone matrix puts this in armature space.
|
||||||
case 'WORLD':
|
match export_space:
|
||||||
pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix
|
case 'ARMATURE':
|
||||||
case 'ROOT':
|
pose_bone_matrix = pose_bone.matrix
|
||||||
pose_bone_matrix = Matrix.Identity(4)
|
case 'WORLD':
|
||||||
|
pose_bone_matrix = armature_object.matrix_world @ pose_bone.matrix
|
||||||
|
case 'ROOT':
|
||||||
|
pose_bone_matrix = Matrix.Identity(4)
|
||||||
|
case _:
|
||||||
|
assert False, f'Invalid export space: {export_space}'
|
||||||
|
|
||||||
# The root bone is the only bone that should be transformed by the coordinate system transform, since all
|
# The root bone is the only bone that should be transformed by the coordinate system transform, since all
|
||||||
# other bones are relative to their parent bones.
|
# other bones are relative to their parent bones.
|
||||||
pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix
|
pose_bone_matrix = coordinate_system_transform @ pose_bone_matrix
|
||||||
|
|
||||||
location = pose_bone_matrix.to_translation()
|
location = pose_bone_matrix.to_translation()
|
||||||
rotation = pose_bone_matrix.to_quaternion().normalized()
|
rotation = pose_bone_matrix.to_quaternion().normalized()
|
||||||
|
|
||||||
location *= scale
|
# TODO: this has gotten way more complicated than it needs to be.
|
||||||
|
# TODO: don't apply scale to the root bone of armatures if we have a false root:
|
||||||
|
if not has_false_root_bone or (pose_bone is None or pose_bone.parent is not None):
|
||||||
|
location *= scale
|
||||||
|
|
||||||
if pose_bone.parent is not None:
|
if has_false_root_bone:
|
||||||
|
is_child_bone = not is_false_root_bone
|
||||||
|
else:
|
||||||
|
is_child_bone = pose_bone.parent is not None
|
||||||
|
|
||||||
|
if is_child_bone is not None:
|
||||||
rotation.conjugate()
|
rotation.conjugate()
|
||||||
|
|
||||||
return location, rotation
|
return location, rotation
|
||||||
|
|
||||||
|
|
||||||
def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
||||||
active_object = context.view_layer.objects.active
|
|
||||||
|
|
||||||
psa = Psa()
|
psa = Psa()
|
||||||
|
|
||||||
armature_object = active_object
|
psx_bone_create_result = create_psx_bones(
|
||||||
evaluated_armature_object = armature_object.evaluated_get(context.evaluated_depsgraph_get())
|
armature_objects=options.armature_objects,
|
||||||
|
|
||||||
armature_data = typing.cast(Armature, armature_object.data)
|
|
||||||
bones: List[Bone] = list(iter(armature_data.bones))
|
|
||||||
|
|
||||||
# The order of the armature bones and the pose bones is not guaranteed to be the same.
|
|
||||||
# As a result, we need to reconstruct the list of pose bones in the same order as the
|
|
||||||
# armature bones.
|
|
||||||
bone_names = [x.name for x in bones]
|
|
||||||
|
|
||||||
# Get a list of all the bone indices and instigator bones for the bone filter settings.
|
|
||||||
export_bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, options.bone_collection_indices)
|
|
||||||
bone_indices = [bone_names.index(x) for x in export_bone_names]
|
|
||||||
|
|
||||||
# Make the bone lists contain only the bones that are going to be exported.
|
|
||||||
bones = [bones[bone_index] for bone_index in bone_indices]
|
|
||||||
|
|
||||||
# No bones are going to be exported.
|
|
||||||
if len(bones) == 0:
|
|
||||||
raise RuntimeError('No bones available for export')
|
|
||||||
|
|
||||||
# The bone building code should be shared between the PSK and PSA exporters, since they both need to build a nearly identical bone list.
|
|
||||||
|
|
||||||
# TODO: The PSA bones are just here to validate the hierarchy. The pose information is not used by the engine.
|
|
||||||
# Build list of PSA bones.
|
|
||||||
psa.bones = convert_blender_bones_to_psx_bones(
|
|
||||||
bones=bones,
|
|
||||||
bone_class=Psa.Bone,
|
|
||||||
export_space=options.export_space,
|
export_space=options.export_space,
|
||||||
armature_object_matrix_world=armature_object.matrix_world, # evaluated_armature_object.matrix_world,
|
root_bone_name=options.root_bone_name,
|
||||||
scale=options.scale,
|
|
||||||
forward_axis=options.forward_axis,
|
forward_axis=options.forward_axis,
|
||||||
up_axis=options.up_axis
|
up_axis=options.up_axis,
|
||||||
|
scale=options.scale,
|
||||||
|
bone_filter_mode=options.bone_filter_mode,
|
||||||
|
bone_collection_indices=options.bone_collection_indices,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: technically wrong, might not necessarily be true (i.e., if the armature object has no contributing bones).
|
||||||
|
has_false_root_bone = len(options.armature_objects) > 1
|
||||||
|
|
||||||
|
# Build list of PSA bones.
|
||||||
|
# Note that the PSA bones are just here to validate the hierarchy. The bind pose information is not used by the
|
||||||
|
# engine.
|
||||||
|
psa.bones = [psx_bone for psx_bone, _ in psx_bone_create_result.bones]
|
||||||
|
|
||||||
|
# No bones are going to be exported.
|
||||||
|
if len(psa.bones) == 0:
|
||||||
|
raise RuntimeError('No bones available for export')
|
||||||
|
|
||||||
# We invert the export space matrix so that we neutralize the transform of the armature object.
|
# We invert the export space matrix so that we neutralize the transform of the armature object.
|
||||||
export_space_matrix_inverse = get_export_space_matrix(options.export_space, armature_object)
|
# export_space_matrix_inverse = get_export_space_matrix(options.export_space, armature_object)
|
||||||
|
|
||||||
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
# Add prefixes and suffices to the names of the export sequences and strip whitespace.
|
||||||
for export_sequence in options.sequences:
|
for export_sequence in options.sequences:
|
||||||
@@ -126,6 +132,7 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
|
|
||||||
# Save the current action and frame so that we can restore the state once we are done.
|
# Save the current action and frame so that we can restore the state once we are done.
|
||||||
saved_frame_current = context.scene.frame_current
|
saved_frame_current = context.scene.frame_current
|
||||||
|
|
||||||
saved_action = options.animation_data.action
|
saved_action = options.animation_data.action
|
||||||
|
|
||||||
# Now build the PSA sequences.
|
# Now build the PSA sequences.
|
||||||
@@ -137,16 +144,6 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
||||||
|
|
||||||
for export_sequence_index, export_sequence in enumerate(options.sequences):
|
for export_sequence_index, export_sequence in enumerate(options.sequences):
|
||||||
# Look up the pose bones for the bones that are going to be exported.
|
|
||||||
pose_bones = [(bone_names.index(bone.name), bone) for bone in armature_object.pose.bones]
|
|
||||||
pose_bones.sort(key=lambda x: x[0])
|
|
||||||
pose_bones = [x[1] for x in pose_bones]
|
|
||||||
pose_bones = [pose_bones[bone_index] for bone_index in bone_indices]
|
|
||||||
|
|
||||||
# Link the action to the animation data and update view layer.
|
|
||||||
export_sequence.anim_data.action = export_sequence.nla_state.action
|
|
||||||
context.view_layer.update()
|
|
||||||
|
|
||||||
frame_start = export_sequence.nla_state.frame_start
|
frame_start = export_sequence.nla_state.frame_start
|
||||||
frame_end = export_sequence.nla_state.frame_end
|
frame_end = export_sequence.nla_state.frame_end
|
||||||
|
|
||||||
@@ -160,26 +157,34 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
frame_step = 0.0
|
frame_step = 0.0
|
||||||
|
|
||||||
sequence_duration = frame_count_raw / export_sequence.fps
|
|
||||||
|
|
||||||
# If this is a reverse sequence, we need to reverse the frame step.
|
# If this is a reverse sequence, we need to reverse the frame step.
|
||||||
if frame_start > frame_end:
|
if frame_start > frame_end:
|
||||||
frame_step = -frame_step
|
frame_step = -frame_step
|
||||||
|
|
||||||
|
sequence_duration = frame_count_raw / export_sequence.fps
|
||||||
|
|
||||||
psa_sequence = Psa.Sequence()
|
psa_sequence = Psa.Sequence()
|
||||||
try:
|
try:
|
||||||
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
raise RuntimeError(f'Sequence name "{export_sequence.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
|
raise RuntimeError(
|
||||||
|
f'Sequence name "{export_sequence.name}" contains characters that cannot be encoded in the Windows-1252 codepage')
|
||||||
psa_sequence.frame_count = frame_count
|
psa_sequence.frame_count = frame_count
|
||||||
psa_sequence.frame_start_index = frame_start_index
|
psa_sequence.frame_start_index = frame_start_index
|
||||||
psa_sequence.fps = frame_count / sequence_duration
|
psa_sequence.fps = frame_count / sequence_duration
|
||||||
psa_sequence.bone_count = len(pose_bones)
|
psa_sequence.bone_count = len(psa.bones)
|
||||||
psa_sequence.track_time = frame_count
|
psa_sequence.track_time = frame_count
|
||||||
psa_sequence.key_reduction = 1.0
|
psa_sequence.key_reduction = 1.0
|
||||||
|
|
||||||
frame = float(frame_start)
|
frame = float(frame_start)
|
||||||
|
|
||||||
|
# Link the action to the animation data and update view layer.
|
||||||
|
for armature_object in options.armature_objects:
|
||||||
|
# TODO: change this to assign it to each armature object's animation data.
|
||||||
|
armature_object.animation_data.action = export_sequence.nla_state.action
|
||||||
|
|
||||||
|
context.view_layer.update()
|
||||||
|
|
||||||
def add_key(location: Vector, rotation: Quaternion):
|
def add_key(location: Vector, rotation: Quaternion):
|
||||||
key = Psa.Key()
|
key = Psa.Key()
|
||||||
key.location.x = location.x
|
key.location.x = location.x
|
||||||
@@ -192,12 +197,40 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
key.time = 1.0 / psa_sequence.fps
|
key.time = 1.0 / psa_sequence.fps
|
||||||
psa.keys.append(key)
|
psa.keys.append(key)
|
||||||
|
|
||||||
# TODO: In _get_pose_bone_location_and_rotation, we need to factor in the evaluated armature object's scale.
|
class PsaExportBone:
|
||||||
# Then also, if we're in ARMATURE export space, invert the pose bone matrix so that it's the identity matrix.
|
def __init__(self, pose_bone: Optional[PoseBone], armature_object: Optional[Object], scale: Vector):
|
||||||
|
self.pose_bone = pose_bone
|
||||||
|
self.armature_object = armature_object
|
||||||
|
self.scale = scale
|
||||||
|
|
||||||
# TODO: extract the scale out of the evaluated_armature_object.matrix_world.
|
armature_scales: Dict[Object, Vector] = {}
|
||||||
_, _, scale = evaluated_armature_object.matrix_world.decompose()
|
|
||||||
scale *= options.scale
|
# Extract the scale from the world matrix of the evaluated armature object.
|
||||||
|
for armature_object in options.armature_objects:
|
||||||
|
evaluated_armature_object = armature_object.evaluated_get(context.evaluated_depsgraph_get())
|
||||||
|
_, _, scale = evaluated_armature_object.matrix_world.decompose()
|
||||||
|
scale *= options.scale
|
||||||
|
armature_scales[armature_object] = scale
|
||||||
|
|
||||||
|
# Create a list of export pose bones, in the same order as the bones as they appear in the armature.
|
||||||
|
# The object contains the pose bone, the armature object, and a pre-calculated scaling value to apply to the
|
||||||
|
# locations.
|
||||||
|
export_bones: List[PsaExportBone] = []
|
||||||
|
|
||||||
|
for psx_bone, armature_object in psx_bone_create_result.bones:
|
||||||
|
print(psx_bone, armature_object)
|
||||||
|
# TODO: look up the pose bone from the name in the PSX bone.
|
||||||
|
if armature_object is None:
|
||||||
|
export_bones.append(PsaExportBone(None, None, Vector((1.0, 1.0, 1.0))))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO: we need to look up the pose bones using the name.
|
||||||
|
pose_bone = armature_object.pose.bones[psx_bone.name.decode('windows-1252')]
|
||||||
|
|
||||||
|
export_bones.append(PsaExportBone(pose_bone, armature_object, armature_scales[armature_object]))
|
||||||
|
|
||||||
|
for export_bone in export_bones:
|
||||||
|
print(export_bone.pose_bone, export_bone.armature_object, export_bone.scale)
|
||||||
|
|
||||||
match options.sampling_mode:
|
match options.sampling_mode:
|
||||||
case 'INTERPOLATED':
|
case 'INTERPOLATED':
|
||||||
@@ -221,13 +254,14 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
else:
|
else:
|
||||||
last_frame_bone_poses.clear()
|
last_frame_bone_poses.clear()
|
||||||
context.scene.frame_set(frame=last_frame)
|
context.scene.frame_set(frame=last_frame)
|
||||||
for pose_bone in pose_bones:
|
for export_bone in export_bones:
|
||||||
location, rotation = _get_pose_bone_location_and_rotation(
|
location, rotation = _get_pose_bone_location_and_rotation(
|
||||||
pose_bone,
|
export_bone.pose_bone,
|
||||||
armature_object,
|
export_bone.armature_object,
|
||||||
options.export_space,
|
options.export_space,
|
||||||
scale,
|
export_bone.scale,
|
||||||
coordinate_system_transform=coordinate_system_transform
|
coordinate_system_transform=coordinate_system_transform,
|
||||||
|
has_false_root_bone=has_false_root_bone,
|
||||||
)
|
)
|
||||||
last_frame_bone_poses.append((location, rotation))
|
last_frame_bone_poses.append((location, rotation))
|
||||||
|
|
||||||
@@ -236,26 +270,27 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
|
|
||||||
# If this is not a subframe, just use the last frame's bone poses.
|
# If this is not a subframe, just use the last frame's bone poses.
|
||||||
if frame % 1.0 == 0:
|
if frame % 1.0 == 0:
|
||||||
for i in range(len(pose_bones)):
|
for i in range(len(export_bones)):
|
||||||
add_key(*last_frame_bone_poses[i])
|
add_key(*last_frame_bone_poses[i])
|
||||||
else:
|
else:
|
||||||
# Otherwise, this is a subframe, so we need to interpolate the pose between the next frame and the last frame.
|
# Otherwise, this is a subframe, so we need to interpolate the pose between the next frame and the last frame.
|
||||||
if next_frame is None:
|
if next_frame is None:
|
||||||
next_frame = last_frame + 1
|
next_frame = last_frame + 1
|
||||||
context.scene.frame_set(frame=next_frame)
|
context.scene.frame_set(frame=next_frame)
|
||||||
for pose_bone in pose_bones:
|
for export_bone in export_bones:
|
||||||
location, rotation = _get_pose_bone_location_and_rotation(
|
location, rotation = _get_pose_bone_location_and_rotation(
|
||||||
pose_bone,
|
pose_bone=export_bone.pose_bone,
|
||||||
armature_object,
|
armature_object=export_bone.armature_object,
|
||||||
options.export_space,
|
export_space=options.export_space,
|
||||||
scale,
|
scale=export_bone.scale,
|
||||||
coordinate_system_transform=coordinate_system_transform
|
coordinate_system_transform=coordinate_system_transform,
|
||||||
|
has_false_root_bone=has_false_root_bone,
|
||||||
)
|
)
|
||||||
next_frame_bone_poses.append((location, rotation))
|
next_frame_bone_poses.append((location, rotation))
|
||||||
|
|
||||||
factor = frame % 1.0
|
factor = frame % 1.0
|
||||||
|
|
||||||
for i in range(len(pose_bones)):
|
for i in range(len(export_bones)):
|
||||||
last_location, last_rotation = last_frame_bone_poses[i]
|
last_location, last_rotation = last_frame_bone_poses[i]
|
||||||
next_location, next_rotation = next_frame_bone_poses[i]
|
next_location, next_rotation = next_frame_bone_poses[i]
|
||||||
|
|
||||||
@@ -269,13 +304,14 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
for _ in range(frame_count):
|
for _ in range(frame_count):
|
||||||
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
|
context.scene.frame_set(frame=int(frame), subframe=frame % 1.0)
|
||||||
|
|
||||||
for pose_bone in pose_bones:
|
for export_bone in export_bones:
|
||||||
location, rotation = _get_pose_bone_location_and_rotation(
|
location, rotation = _get_pose_bone_location_and_rotation(
|
||||||
pose_bone,
|
pose_bone=export_bone.pose_bone,
|
||||||
armature_object,
|
armature_object=export_bone.armature_object,
|
||||||
options.export_space,
|
export_space=options.export_space,
|
||||||
scale,
|
scale=export_bone.scale,
|
||||||
coordinate_system_transform=coordinate_system_transform
|
coordinate_system_transform=coordinate_system_transform,
|
||||||
|
has_false_root_bone=has_false_root_bone,
|
||||||
)
|
)
|
||||||
add_key(location, rotation)
|
add_key(location, rotation)
|
||||||
|
|
||||||
@@ -288,7 +324,9 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa:
|
|||||||
context.window_manager.progress_update(export_sequence_index)
|
context.window_manager.progress_update(export_sequence_index)
|
||||||
|
|
||||||
# Restore the previous action & frame.
|
# Restore the previous action & frame.
|
||||||
|
# TODO: store each armature object's previous action
|
||||||
options.animation_data.action = saved_action
|
options.animation_data.action = saved_action
|
||||||
|
|
||||||
context.scene.frame_set(saved_frame_current)
|
context.scene.frame_set(saved_frame_current)
|
||||||
|
|
||||||
context.window_manager.progress_end()
|
context.window_manager.progress_end()
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ Use the PsaReader::get_sequence_keys to get the keys for a sequence.
|
|||||||
|
|
||||||
|
|
||||||
class Psa:
|
class Psa:
|
||||||
class Bone(Structure):
|
|
||||||
_fields_ = [
|
|
||||||
('name', c_char * 64),
|
|
||||||
('flags', c_int32),
|
|
||||||
('children_count', c_int32),
|
|
||||||
('parent_index', c_int32),
|
|
||||||
('rotation', Quaternion),
|
|
||||||
('location', Vector3),
|
|
||||||
('padding', c_char * 16)
|
|
||||||
]
|
|
||||||
|
|
||||||
class Sequence(Structure):
|
class Sequence(Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
@@ -59,6 +49,6 @@ class Psa:
|
|||||||
return repr((self.location, self.rotation, self.time))
|
return repr((self.location, self.rotation, self.time))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bones: List[Psa.Bone] = []
|
self.bones: List[PsxBone] = []
|
||||||
self.sequences: typing.OrderedDict[str, Psa.Sequence] = OrderedDict()
|
self.sequences: typing.OrderedDict[str, Psa.Sequence] = OrderedDict()
|
||||||
self.keys: List[Psa.Key] = []
|
self.keys: List[Psa.Key] = []
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ from typing import List, Iterable, Dict, Tuple, Optional
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty
|
||||||
from bpy.types import Context, Action, Object, AnimData, TimelineMarker
|
from bpy.types import Context, Action, Object, AnimData, TimelineMarker, Operator
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from bpy_types import Operator
|
|
||||||
|
|
||||||
from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences, \
|
from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences, \
|
||||||
get_sequences_from_name_and_frame_range
|
get_sequences_from_name_and_frame_range
|
||||||
@@ -63,7 +62,7 @@ def is_action_for_object(obj: Object, action: Action):
|
|||||||
return any(obj in slot.users() for slot in action.slots)
|
return any(obj in slot.users() for slot in action.slots)
|
||||||
|
|
||||||
|
|
||||||
def update_actions_and_timeline_markers(context: Context):
|
def update_actions_and_timeline_markers(context: Context, armature_objects: Iterable[Object]):
|
||||||
pg = getattr(context.scene, 'psa_export')
|
pg = getattr(context.scene, 'psa_export')
|
||||||
|
|
||||||
# Clear actions and markers.
|
# Clear actions and markers.
|
||||||
@@ -72,6 +71,7 @@ def update_actions_and_timeline_markers(context: Context):
|
|||||||
pg.active_action_list.clear()
|
pg.active_action_list.clear()
|
||||||
|
|
||||||
# Get animation data.
|
# Get animation data.
|
||||||
|
# TODO: Not sure how to handle this with multiple armatures.
|
||||||
animation_data_object = get_animation_data_object(context)
|
animation_data_object = get_animation_data_object(context)
|
||||||
animation_data = animation_data_object.animation_data if animation_data_object else None
|
animation_data = animation_data_object.animation_data if animation_data_object else None
|
||||||
|
|
||||||
@@ -80,7 +80,8 @@ def update_actions_and_timeline_markers(context: Context):
|
|||||||
|
|
||||||
# Populate actions list.
|
# Populate actions list.
|
||||||
for action in bpy.data.actions:
|
for action in bpy.data.actions:
|
||||||
if not is_action_for_object(context.active_object, action):
|
if not any(map(lambda armature_object: is_action_for_object(armature_object, action), armature_objects)):
|
||||||
|
# This action is not applicable to any of the selected armatures.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for (name, frame_start, frame_end) in get_sequences_from_action(action):
|
for (name, frame_start, frame_end) in get_sequences_from_action(action):
|
||||||
@@ -170,7 +171,7 @@ def get_animation_data_object(context: Context) -> Object:
|
|||||||
active_object = context.view_layer.objects.active
|
active_object = context.view_layer.objects.active
|
||||||
|
|
||||||
if active_object.type != 'ARMATURE':
|
if active_object.type != 'ARMATURE':
|
||||||
raise RuntimeError('Selected object must be an Armature')
|
raise RuntimeError('Active object must be an Armature')
|
||||||
|
|
||||||
if pg.sequence_source != 'ACTIONS' and pg.should_override_animation_data:
|
if pg.sequence_source != 'ACTIONS' and pg.should_override_animation_data:
|
||||||
animation_data_object = pg.animation_data_override
|
animation_data_object = pg.animation_data_override
|
||||||
@@ -275,7 +276,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.armature_object = None
|
self.armature_objects: List[Object] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -380,6 +381,14 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
|
bones_panel.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index',
|
||||||
rows=rows)
|
rows=rows)
|
||||||
|
|
||||||
|
bones_advanced_header, bones_advanced_panel = layout.panel('Advanced', default_closed=False)
|
||||||
|
bones_advanced_header.label(text='Advanced')
|
||||||
|
if bones_advanced_panel:
|
||||||
|
flow = bones_advanced_panel.grid_flow()
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(pg, 'root_bone_name', text='Root Bone Name')
|
||||||
|
|
||||||
# TRANSFORM
|
# TRANSFORM
|
||||||
transform_header, transform_panel = layout.panel('Advanced', default_closed=False)
|
transform_header, transform_panel = layout.panel('Advanced', default_closed=False)
|
||||||
transform_header.label(text='Transform')
|
transform_header.label(text='Transform')
|
||||||
@@ -401,18 +410,10 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
if context.view_layer.objects.active.type != 'ARMATURE':
|
if context.view_layer.objects.active.type != 'ARMATURE':
|
||||||
raise RuntimeError('The active object must be an armature')
|
raise RuntimeError('The active object must be an armature')
|
||||||
|
|
||||||
# If we have multiple armatures selected, make sure that they all use the same underlying armature data.
|
|
||||||
armature_objects = [obj for obj in context.selected_objects if obj.type == 'ARMATURE']
|
|
||||||
|
|
||||||
for obj in armature_objects:
|
|
||||||
if obj.data != context.view_layer.objects.active.data:
|
|
||||||
raise RuntimeError(f'All selected armatures must use the same armature data block.\n\n'
|
|
||||||
f'\The armature data block for "{obj.name}\" (\'{obj.data.name}\') does not match '
|
|
||||||
f'the active armature data block (\'{context.view_layer.objects.active.name}\')')
|
|
||||||
|
|
||||||
if context.scene.is_nla_tweakmode:
|
if context.scene.is_nla_tweakmode:
|
||||||
raise RuntimeError('Cannot export PSA while in NLA tweak mode')
|
raise RuntimeError('Cannot export PSA while in NLA tweak mode')
|
||||||
|
|
||||||
|
|
||||||
def invoke(self, context, _event):
|
def invoke(self, context, _event):
|
||||||
try:
|
try:
|
||||||
self._check_context(context)
|
self._check_context(context)
|
||||||
@@ -421,16 +422,16 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
pg: PSA_PG_export = getattr(context.scene, 'psa_export')
|
pg: PSA_PG_export = getattr(context.scene, 'psa_export')
|
||||||
|
|
||||||
self.armature_object = context.view_layer.objects.active
|
self.armature_objects = [x for x in context.view_layer.objects.selected if x.type == 'ARMATURE']
|
||||||
|
|
||||||
if self.armature_object.animation_data is None:
|
for armature_object in self.armature_objects:
|
||||||
# This is required otherwise the action list will be empty if the armature has never had its animation
|
# This is required otherwise the action list will be empty if the armature has never had its animation
|
||||||
# data created before (i.e. if no action was ever assigned to it).
|
# data created before (i.e. if no action was ever assigned to it).
|
||||||
self.armature_object.animation_data_create()
|
if armature_object.animation_data is None:
|
||||||
|
armature_object.animation_data_create()
|
||||||
|
|
||||||
update_actions_and_timeline_markers(context)
|
update_actions_and_timeline_markers(context, self.armature_objects)
|
||||||
|
populate_bone_collection_list(self.armature_objects, pg.bone_collection_list)
|
||||||
populate_bone_collection_list(self.armature_object, pg.bone_collection_list)
|
|
||||||
|
|
||||||
context.window_manager.fileselect_add(self)
|
context.window_manager.fileselect_add(self)
|
||||||
|
|
||||||
@@ -442,9 +443,11 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
# Ensure that we actually have items that we are going to be exporting.
|
# Ensure that we actually have items that we are going to be exporting.
|
||||||
if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0:
|
if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0:
|
||||||
raise RuntimeError('No actions were selected for export')
|
raise RuntimeError('No actions were selected for export')
|
||||||
elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
|
|
||||||
|
if pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0:
|
||||||
raise RuntimeError('No timeline markers were selected for export')
|
raise RuntimeError('No timeline markers were selected for export')
|
||||||
elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0:
|
|
||||||
|
if pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0:
|
||||||
raise RuntimeError('No NLA track strips were selected for export')
|
raise RuntimeError('No NLA track strips were selected for export')
|
||||||
|
|
||||||
# Populate the export sequence list.
|
# Populate the export sequence list.
|
||||||
@@ -511,6 +514,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
options = PsaBuildOptions()
|
options = PsaBuildOptions()
|
||||||
|
options.armature_objects = self.armature_objects
|
||||||
options.animation_data = animation_data
|
options.animation_data = animation_data
|
||||||
options.sequences = export_sequences
|
options.sequences = export_sequences
|
||||||
options.bone_filter_mode = pg.bone_filter_mode
|
options.bone_filter_mode = pg.bone_filter_mode
|
||||||
@@ -522,6 +526,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
options.export_space = pg.export_space
|
options.export_space = pg.export_space
|
||||||
options.forward_axis = pg.forward_axis
|
options.forward_axis = pg.forward_axis
|
||||||
options.up_axis = pg.up_axis
|
options.up_axis = pg.up_axis
|
||||||
|
options.root_bone_name = pg.root_bone_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
psa = build_psa(context, options)
|
psa = build_psa(context, options)
|
||||||
|
|||||||
@@ -217,6 +217,11 @@ class PSA_PG_export(PropertyGroup, ForwardUpAxisMixin, ExportSpaceMixin):
|
|||||||
),
|
),
|
||||||
default='INTERPOLATED'
|
default='INTERPOLATED'
|
||||||
)
|
)
|
||||||
|
root_bone_name: StringProperty(
|
||||||
|
name='Root Bone Name',
|
||||||
|
description='The name of the generated root bone when exporting multiple armatures',
|
||||||
|
default='ROOT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from bpy.types import FCurve, Object, Context
|
|||||||
from mathutils import Vector, Quaternion
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
||||||
from .data import Psa
|
|
||||||
from .reader import PsaReader
|
from .reader import PsaReader
|
||||||
|
from ..shared.data import PsxBone
|
||||||
|
|
||||||
|
|
||||||
class PsaImportOptions(object):
|
class PsaImportOptions(object):
|
||||||
@@ -45,8 +45,8 @@ class PsaImportOptions(object):
|
|||||||
|
|
||||||
|
|
||||||
class ImportBone(object):
|
class ImportBone(object):
|
||||||
def __init__(self, psa_bone: Psa.Bone):
|
def __init__(self, psa_bone: PsxBone):
|
||||||
self.psa_bone: Psa.Bone = psa_bone
|
self.psa_bone: PsxBone = psa_bone
|
||||||
self.parent: Optional[ImportBone] = None
|
self.parent: Optional[ImportBone] = None
|
||||||
self.armature_bone = None
|
self.armature_bone = None
|
||||||
self.pose_bone = None
|
self.pose_bone = None
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import ctypes
|
from ctypes import sizeof
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .data import *
|
from .data import Psa, Section, PsxBone
|
||||||
|
|
||||||
|
|
||||||
def _try_fix_cue4parse_issue_103(sequences) -> bool:
|
def _try_fix_cue4parse_issue_103(sequences) -> bool:
|
||||||
@@ -101,11 +102,11 @@ class PsaReader(object):
|
|||||||
psa = Psa()
|
psa = Psa()
|
||||||
while fp.read(1):
|
while fp.read(1):
|
||||||
fp.seek(-1, 1)
|
fp.seek(-1, 1)
|
||||||
section = Section.from_buffer_copy(fp.read(ctypes.sizeof(Section)))
|
section = Section.from_buffer_copy(fp.read(sizeof(Section)))
|
||||||
if section.name == b'ANIMHEAD':
|
if section.name == b'ANIMHEAD':
|
||||||
pass
|
pass
|
||||||
elif section.name == b'BONENAMES':
|
elif section.name == b'BONENAMES':
|
||||||
PsaReader._read_types(fp, Psa.Bone, section, psa.bones)
|
PsaReader._read_types(fp, PsxBone, section, psa.bones)
|
||||||
elif section.name == b'ANIMINFO':
|
elif section.name == b'ANIMINFO':
|
||||||
sequences = []
|
sequences = []
|
||||||
PsaReader._read_types(fp, Psa.Sequence, section, sequences)
|
PsaReader._read_types(fp, Psa.Sequence, section, sequences)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from ctypes import Structure, sizeof
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from .data import Psa
|
from .data import Psa
|
||||||
from ..shared.data import Section
|
from ..shared.data import Section, PsxBone
|
||||||
|
|
||||||
|
|
||||||
def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None):
|
def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None):
|
||||||
@@ -20,6 +20,6 @@ def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list
|
|||||||
def write_psa(psa: Psa, path: str):
|
def write_psa(psa: Psa, path: str):
|
||||||
with open(path, 'wb') as fp:
|
with open(path, 'wb') as fp:
|
||||||
write_section(fp, b'ANIMHEAD')
|
write_section(fp, b'ANIMHEAD')
|
||||||
write_section(fp, b'BONENAMES', Psa.Bone, psa.bones)
|
write_section(fp, b'BONENAMES', PsxBone, psa.bones)
|
||||||
write_section(fp, b'ANIMINFO', Psa.Sequence, list(psa.sequences.values()))
|
write_section(fp, b'ANIMINFO', Psa.Sequence, list(psa.sequences.values()))
|
||||||
write_section(fp, b'ANIMKEYS', Psa.Key, psa.keys)
|
write_section(fp, b'ANIMKEYS', Psa.Key, psa.keys)
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import typing
|
import typing
|
||||||
from collections import Counter
|
from typing import Dict, Generator, Set, Iterable, Optional, cast, Tuple
|
||||||
from typing import Dict, Generator, Set, Iterable, Optional, cast
|
|
||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bpy.types import Material, Collection, Context, Object, Armature, Bone
|
from bpy.types import Collection, Context, Object, Armature
|
||||||
|
from mathutils import Matrix
|
||||||
|
|
||||||
from .data import *
|
from .data import *
|
||||||
|
from .export.operators import get_materials_for_mesh_objects
|
||||||
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
from .properties import triangle_type_and_bit_flags_to_poly_flags
|
||||||
from ..shared.dfs import dfs_collection_objects, dfs_view_layer_objects, DfsObject
|
from ..shared.dfs import dfs_collection_objects, dfs_view_layer_objects, DfsObject
|
||||||
from ..shared.helpers import get_coordinate_system_transform, convert_string_to_cp1252_bytes, \
|
from ..shared.helpers import convert_string_to_cp1252_bytes, \
|
||||||
get_export_bone_names, convert_blender_bones_to_psx_bones
|
create_psx_bones, get_coordinate_system_transform
|
||||||
|
|
||||||
|
|
||||||
class PskInputObjects(object):
|
class PskInputObjects(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.mesh_objects: List[DfsObject] = []
|
self.mesh_dfs_objects: List[DfsObject] = []
|
||||||
self.armature_objects: Set[Object] = set()
|
self.armature_objects: Set[Object] = set()
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ class PskBuildOptions(object):
|
|||||||
self.bone_filter_mode = 'ALL'
|
self.bone_filter_mode = 'ALL'
|
||||||
self.bone_collection_indices: List[Tuple[str, int]] = []
|
self.bone_collection_indices: List[Tuple[str, int]] = []
|
||||||
self.object_eval_state = 'EVALUATED'
|
self.object_eval_state = 'EVALUATED'
|
||||||
self.materials: List[Material] = []
|
self.material_order_mode = 'AUTOMATIC'
|
||||||
|
self.material_name_list: List[str] = []
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
self.export_space = 'WORLD'
|
self.export_space = 'WORLD'
|
||||||
self.forward_axis = 'X'
|
self.forward_axis = 'X'
|
||||||
@@ -63,14 +65,14 @@ def get_armatures_for_mesh_objects(mesh_objects: Iterable[Object]) -> Generator[
|
|||||||
yield modifiers[0].object
|
yield modifiers[0].object
|
||||||
|
|
||||||
|
|
||||||
def _get_psk_input_objects(mesh_objects: Iterable[DfsObject]) -> PskInputObjects:
|
def _get_psk_input_objects(mesh_dfs_objects: Iterable[DfsObject]) -> PskInputObjects:
|
||||||
mesh_objects = list(mesh_objects)
|
mesh_dfs_objects = list(mesh_dfs_objects)
|
||||||
if len(mesh_objects) == 0:
|
if len(mesh_dfs_objects) == 0:
|
||||||
raise RuntimeError('At least one mesh must be selected')
|
raise RuntimeError('At least one mesh must be selected')
|
||||||
|
|
||||||
input_objects = PskInputObjects()
|
input_objects = PskInputObjects()
|
||||||
input_objects.mesh_objects = mesh_objects
|
input_objects.mesh_dfs_objects = mesh_dfs_objects
|
||||||
input_objects.armature_objects |= set(get_armatures_for_mesh_objects(map(lambda x: x.obj, mesh_objects)))
|
input_objects.armature_objects |= set(get_armatures_for_mesh_objects(map(lambda x: x.obj, mesh_dfs_objects)))
|
||||||
|
|
||||||
return input_objects
|
return input_objects
|
||||||
|
|
||||||
@@ -133,98 +135,39 @@ def _get_material_name_indices(obj: Object, material_names: List[str]) -> Iterab
|
|||||||
yield 0
|
yield 0
|
||||||
|
|
||||||
|
|
||||||
def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuildOptions) -> PskBuildResult:
|
||||||
armature_objects = list(input_objects.armature_objects)
|
armature_objects = list(input_objects.armature_objects)
|
||||||
|
|
||||||
result = PskBuildResult()
|
result = PskBuildResult()
|
||||||
psk = Psk()
|
psk = Psk()
|
||||||
bones: List[Bone] = []
|
|
||||||
|
|
||||||
if options.export_space != 'WORLD' and len(armature_objects) > 1:
|
psx_bone_create_result = create_psx_bones(
|
||||||
raise RuntimeError('When exporting multiple armatures, the Export Space must be World')
|
armature_objects=armature_objects,
|
||||||
|
export_space=options.export_space,
|
||||||
|
forward_axis=options.forward_axis,
|
||||||
|
up_axis=options.up_axis,
|
||||||
|
scale=options.scale,
|
||||||
|
root_bone_name=options.root_bone_name,
|
||||||
|
bone_filter_mode=options.bone_filter_mode,
|
||||||
|
bone_collection_indices=options.bone_collection_indices
|
||||||
|
)
|
||||||
|
|
||||||
coordinate_system_matrix = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
psk.bones = [psx_bone for psx_bone, _ in psx_bone_create_result.bones]
|
||||||
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
|
|
||||||
|
|
||||||
scale_matrix = Matrix.Scale(options.scale, 4)
|
|
||||||
|
|
||||||
total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects)
|
|
||||||
|
|
||||||
# Store the index of the root bone for each armature object.
|
|
||||||
# We will need this later to correctly assign vertex weights.
|
|
||||||
armature_object_root_bone_indices = dict()
|
|
||||||
|
|
||||||
# Store the bone names to be exported for each armature object.
|
|
||||||
armature_object_bone_names: Dict[Object, List[str]] = dict()
|
|
||||||
for armature_object in armature_objects:
|
|
||||||
bone_collection_indices = [x[1] for x in options.bone_collection_indices if x[0] == armature_object.name]
|
|
||||||
bone_names = get_export_bone_names(armature_object, options.bone_filter_mode, bone_collection_indices)
|
|
||||||
armature_object_bone_names[armature_object] = bone_names
|
|
||||||
|
|
||||||
if len(armature_objects) == 0 or total_bone_count == 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.
|
|
||||||
psk_bone = Psk.Bone()
|
|
||||||
psk_bone.name = convert_string_to_cp1252_bytes(options.root_bone_name)
|
|
||||||
psk_bone.flags = 0
|
|
||||||
psk_bone.children_count = 0
|
|
||||||
psk_bone.parent_index = 0
|
|
||||||
psk_bone.location = Vector3.zero()
|
|
||||||
psk_bone.rotation = Quaternion.from_bpy_quaternion(coordinate_system_default_rotation)
|
|
||||||
psk.bones.append(psk_bone)
|
|
||||||
|
|
||||||
armature_object_root_bone_indices[None] = 0
|
|
||||||
else:
|
|
||||||
# If we have multiple armature objects, create a root bone at the world origin.
|
|
||||||
if len(armature_objects) > 1:
|
|
||||||
psk_bone = Psk.Bone()
|
|
||||||
psk_bone.name = convert_string_to_cp1252_bytes(options.root_bone_name)
|
|
||||||
psk_bone.flags = 0
|
|
||||||
psk_bone.children_count = total_bone_count
|
|
||||||
psk_bone.parent_index = 0
|
|
||||||
psk_bone.location = Vector3.zero()
|
|
||||||
psk_bone.rotation = Quaternion.from_bpy_quaternion(coordinate_system_default_rotation)
|
|
||||||
psk.bones.append(psk_bone)
|
|
||||||
|
|
||||||
armature_object_root_bone_indices[None] = 0
|
|
||||||
|
|
||||||
root_bone = psk.bones[0] if len(psk.bones) > 0 else None
|
|
||||||
|
|
||||||
for armature_object in armature_objects:
|
|
||||||
bone_names = armature_object_bone_names[armature_object]
|
|
||||||
armature_data = typing.cast(Armature, armature_object.data)
|
|
||||||
bones = [armature_data.bones[bone_name] for bone_name in bone_names]
|
|
||||||
|
|
||||||
psk_bones = convert_blender_bones_to_psx_bones(
|
|
||||||
bones=bones,
|
|
||||||
bone_class=Psk.Bone,
|
|
||||||
export_space=options.export_space,
|
|
||||||
armature_object_matrix_world=armature_object.matrix_world,
|
|
||||||
scale=options.scale,
|
|
||||||
forward_axis=options.forward_axis,
|
|
||||||
up_axis=options.up_axis,
|
|
||||||
root_bone=root_bone,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we are appending these bones to an existing list of bones, we need to adjust the parent indices.
|
|
||||||
if len(psk.bones) > 0:
|
|
||||||
parent_index_offset = len(psk.bones)
|
|
||||||
for bone in psk_bones[1:]:
|
|
||||||
bone.parent_index += parent_index_offset
|
|
||||||
|
|
||||||
armature_object_root_bone_indices[armature_object] = len(psk.bones)
|
|
||||||
|
|
||||||
psk.bones.extend(psk_bones)
|
|
||||||
|
|
||||||
# Check if there are bone name conflicts between armatures.
|
|
||||||
bone_name_counts = Counter(x.name.decode('windows-1252').upper() for x in psk.bones)
|
|
||||||
|
|
||||||
for bone_name, count in bone_name_counts.items():
|
|
||||||
if count > 1:
|
|
||||||
raise RuntimeError(f'Found {count} bones with the name "{bone_name}". Bone names must be unique when compared case-insensitively.')
|
|
||||||
|
|
||||||
# Materials
|
# Materials
|
||||||
for material in options.materials:
|
match options.material_order_mode:
|
||||||
|
case 'AUTOMATIC':
|
||||||
|
mesh_objects = [dfs_object.obj for dfs_object in input_objects.mesh_dfs_objects]
|
||||||
|
materials = list(get_materials_for_mesh_objects(context.evaluated_depsgraph_get(), mesh_objects))
|
||||||
|
case 'MANUAL':
|
||||||
|
# The material name list may contain materials that are not on the mesh objects.
|
||||||
|
# Therefore, we can take the material_name_list as gospel and simply use it as a lookup table.
|
||||||
|
# If a look-up fails, replace it with an empty material.
|
||||||
|
materials = [bpy.data.materials.get(x.material_name, None) for x in options.material_name_list]
|
||||||
|
case _:
|
||||||
|
assert False, f'Invalid material order mode: {options.material_order_mode}'
|
||||||
|
|
||||||
|
for material in materials:
|
||||||
psk_material = Psk.Material()
|
psk_material = Psk.Material()
|
||||||
psk_material.name = convert_string_to_cp1252_bytes(material.name)
|
psk_material.name = convert_string_to_cp1252_bytes(material.name)
|
||||||
psk_material.texture_index = len(psk.materials)
|
psk_material.texture_index = len(psk.materials)
|
||||||
@@ -243,9 +186,11 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
|
|||||||
psk_material.name = convert_string_to_cp1252_bytes('None')
|
psk_material.name = convert_string_to_cp1252_bytes('None')
|
||||||
psk.materials.append(psk_material)
|
psk.materials.append(psk_material)
|
||||||
|
|
||||||
context.window_manager.progress_begin(0, len(input_objects.mesh_objects))
|
context.window_manager.progress_begin(0, len(input_objects.mesh_dfs_objects))
|
||||||
|
|
||||||
|
coordinate_system_matrix = get_coordinate_system_transform(options.forward_axis, options.up_axis)
|
||||||
mesh_export_space_matrix = _get_mesh_export_space_matrix(armature_objects, options.export_space)
|
mesh_export_space_matrix = _get_mesh_export_space_matrix(armature_objects, options.export_space)
|
||||||
|
scale_matrix = Matrix.Scale(options.scale, 4)
|
||||||
vertex_transform_matrix = scale_matrix @ coordinate_system_matrix @ mesh_export_space_matrix
|
vertex_transform_matrix = scale_matrix @ coordinate_system_matrix @ mesh_export_space_matrix
|
||||||
|
|
||||||
original_armature_object_pose_positions = {armature_object: armature_object.data.pose_position for armature_object in armature_objects}
|
original_armature_object_pose_positions = {armature_object: armature_object.data.pose_position for armature_object in armature_objects}
|
||||||
@@ -255,9 +200,9 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
|
|||||||
for armature_object in armature_objects:
|
for armature_object in armature_objects:
|
||||||
armature_object.data.pose_position = 'REST'
|
armature_object.data.pose_position = 'REST'
|
||||||
|
|
||||||
material_names = [m.name for m in options.materials]
|
material_names = [m.name for m in materials]
|
||||||
|
|
||||||
for object_index, input_mesh_object in enumerate(input_objects.mesh_objects):
|
for object_index, input_mesh_object in enumerate(input_objects.mesh_dfs_objects):
|
||||||
obj, instance_objects, matrix_world = input_mesh_object.obj, input_mesh_object.instance_objects, input_mesh_object.matrix_world
|
obj, instance_objects, matrix_world = input_mesh_object.obj, input_mesh_object.instance_objects, input_mesh_object.matrix_world
|
||||||
|
|
||||||
armature_object = get_armature_for_mesh_object(obj)
|
armature_object = get_armature_for_mesh_object(obj)
|
||||||
@@ -384,11 +329,11 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
|
|||||||
# Weights
|
# Weights
|
||||||
if armature_object is not None:
|
if armature_object is not None:
|
||||||
armature_data = typing.cast(Armature, armature_object.data)
|
armature_data = typing.cast(Armature, armature_object.data)
|
||||||
bone_index_offset = armature_object_root_bone_indices[armature_object]
|
bone_index_offset = psx_bone_create_result.armature_object_root_bone_indices[armature_object]
|
||||||
# Because the vertex groups may contain entries for which there is no matching bone in the armature,
|
# 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.
|
# we must filter them out and not export any weights for these vertex groups.
|
||||||
|
|
||||||
bone_names = armature_object_bone_names[armature_object]
|
bone_names = psx_bone_create_result.armature_object_bone_names[armature_object]
|
||||||
vertex_group_names = [x.name for x in mesh_object.vertex_groups]
|
vertex_group_names = [x.name for x in mesh_object.vertex_groups]
|
||||||
vertex_group_bone_indices: Dict[int, int] = dict()
|
vertex_group_bone_indices: Dict[int, int] = dict()
|
||||||
for vertex_group_index, vertex_group_name in enumerate(vertex_group_names):
|
for vertex_group_index, vertex_group_name in enumerate(vertex_group_names):
|
||||||
@@ -433,7 +378,7 @@ def build_psk(context, input_objects: PskInputObjects, options: PskBuildOptions)
|
|||||||
vertices_assigned_weights[vertex_index] = True
|
vertices_assigned_weights[vertex_index] = True
|
||||||
|
|
||||||
# Assign vertices that have not been assigned weights to the root bone of the armature.
|
# Assign vertices that have not been assigned weights to the root bone of the armature.
|
||||||
fallback_weight_bone_index = armature_object_root_bone_indices[armature_object]
|
fallback_weight_bone_index = psx_bone_create_result.armature_object_root_bone_indices[armature_object]
|
||||||
for vertex_index, assigned in enumerate(vertices_assigned_weights):
|
for vertex_index, assigned in enumerate(vertices_assigned_weights):
|
||||||
if not assigned:
|
if not assigned:
|
||||||
w = Psk.Weight()
|
w = Psk.Weight()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from ctypes import Structure, c_uint32, c_float, c_int32, c_uint8, c_int8, c_int16, c_char, c_uint16
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..shared.data import *
|
from ..shared.data import Vector3, Quaternion, Color, Vector2, PsxBone
|
||||||
|
|
||||||
|
|
||||||
class Psk(object):
|
class Psk(object):
|
||||||
@@ -118,7 +119,7 @@ class Psk(object):
|
|||||||
self.faces: List[Psk.Face] = []
|
self.faces: List[Psk.Face] = []
|
||||||
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[PsxBone] = []
|
||||||
self.extra_uvs: List[Vector2] = []
|
self.extra_uvs: List[Vector2] = []
|
||||||
self.vertex_colors: List[Color] = []
|
self.vertex_colors: List[Color] = []
|
||||||
self.vertex_normals: List[Vector3] = []
|
self.vertex_normals: List[Vector3] = []
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
from typing import List, Optional, cast, Iterable
|
from typing import List, Optional, cast, Iterable
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty, BoolProperty
|
||||||
from bpy.types import Operator, Context, Object, Collection, SpaceProperties, Depsgraph, Material
|
from bpy.types import Operator, Context, Object, Collection, SpaceProperties, Depsgraph, Material
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
@@ -27,8 +27,14 @@ def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[
|
|||||||
yield material
|
yield material
|
||||||
|
|
||||||
|
|
||||||
def populate_material_name_list(depsgraph: Depsgraph, mesh_objects, material_list):
|
def populate_material_name_list(depsgraph: Depsgraph, mesh_objects: Iterable[Object], material_list):
|
||||||
materials = get_materials_for_mesh_objects(depsgraph, mesh_objects)
|
materials = list(get_materials_for_mesh_objects(depsgraph, mesh_objects))
|
||||||
|
|
||||||
|
# Order the mesh object materials by the order any existing entries in the material list.
|
||||||
|
# This way, if the user has already set up the material list, we don't change the order.
|
||||||
|
material_names = [x.material_name for x in material_list]
|
||||||
|
materials = get_sorted_materials_by_names(materials, material_names)
|
||||||
|
|
||||||
material_list.clear()
|
material_list.clear()
|
||||||
for index, material in enumerate(materials):
|
for index, material in enumerate(materials):
|
||||||
m = material_list.add()
|
m = material_list.add()
|
||||||
@@ -60,8 +66,8 @@ def get_collection_export_operator_from_context(context: Context) -> Optional[ob
|
|||||||
return exporter.export_properties
|
return exporter.export_properties
|
||||||
|
|
||||||
|
|
||||||
class PSK_OT_populate_bone_collection_list(Operator):
|
class PSK_OT_bone_collection_list_populate(Operator):
|
||||||
bl_idname = 'psk.export_populate_bone_collection_list'
|
bl_idname = 'psk.bone_collection_list_populate'
|
||||||
bl_label = 'Populate Bone Collection List'
|
bl_label = 'Populate Bone Collection List'
|
||||||
bl_description = 'Populate the bone collection list from the armature that will be used in this collection export'
|
bl_description = 'Populate the bone collection list from the armature that will be used in this collection export'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -83,6 +89,24 @@ class PSK_OT_populate_bone_collection_list(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class PSK_OT_bone_collection_list_select_all(Operator):
|
||||||
|
bl_idname = 'psk.bone_collection_list_select_all'
|
||||||
|
bl_label = 'Select All'
|
||||||
|
bl_description = 'Select all bone collections'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
is_selected: BoolProperty(default=True)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
export_operator = get_collection_export_operator_from_context(context)
|
||||||
|
if export_operator is None:
|
||||||
|
self.report({'ERROR_INVALID_CONTEXT'}, 'No valid export operator found in context')
|
||||||
|
return {'CANCELLED'}
|
||||||
|
for item in export_operator.bone_collection_list:
|
||||||
|
item.is_selected = self.is_selected
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class PSK_OT_populate_material_name_list(Operator):
|
class PSK_OT_populate_material_name_list(Operator):
|
||||||
bl_idname = 'psk.export_populate_material_name_list'
|
bl_idname = 'psk.export_populate_material_name_list'
|
||||||
bl_label = 'Populate Material Name List'
|
bl_label = 'Populate Material Name List'
|
||||||
@@ -97,7 +121,7 @@ class PSK_OT_populate_material_name_list(Operator):
|
|||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
input_objects = get_psk_input_objects_for_collection(context.collection)
|
input_objects = get_psk_input_objects_for_collection(context.collection)
|
||||||
try:
|
try:
|
||||||
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_objects], export_operator.material_name_list)
|
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_dfs_objects], export_operator.material_name_list)
|
||||||
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'}
|
||||||
@@ -116,7 +140,7 @@ class PSK_OT_material_list_name_add(Operator):
|
|||||||
bl_description = 'Add a material to the material name list (useful if you want to add a material slot that is not actually used in the mesh)'
|
bl_description = 'Add a material to the material name list (useful if you want to add a material slot that is not actually used in the mesh)'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
name: StringProperty(search=material_list_names_search_cb)
|
name: StringProperty(search=material_list_names_search_cb, name='Material Name', default='None')
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
@@ -232,7 +256,7 @@ def get_sorted_materials_by_names(materials: Iterable[Material], material_names:
|
|||||||
return materials_in_collection + materials_not_in_collection
|
return materials_in_collection + materials_not_in_collection
|
||||||
|
|
||||||
|
|
||||||
def get_psk_build_options_from_property_group(pg: 'PSK_PG_export') -> PskBuildOptions:
|
def get_psk_build_options_from_property_group(pg: PskExportMixin) -> PskBuildOptions:
|
||||||
options = PskBuildOptions()
|
options = PskBuildOptions()
|
||||||
options.object_eval_state = pg.object_eval_state
|
options.object_eval_state = pg.object_eval_state
|
||||||
options.export_space = pg.export_space
|
options.export_space = pg.export_space
|
||||||
@@ -242,15 +266,8 @@ def get_psk_build_options_from_property_group(pg: 'PSK_PG_export') -> PskBuildOp
|
|||||||
options.forward_axis = pg.forward_axis
|
options.forward_axis = pg.forward_axis
|
||||||
options.up_axis = pg.up_axis
|
options.up_axis = pg.up_axis
|
||||||
options.root_bone_name = pg.root_bone_name
|
options.root_bone_name = pg.root_bone_name
|
||||||
|
options.material_order_mode = pg.material_order_mode
|
||||||
# TODO: perhaps move this into the build function and replace the materials list with a material names list.
|
options.material_name_list = pg.material_name_list
|
||||||
# materials = get_materials_for_mesh_objects(depsgraph, mesh_objects)
|
|
||||||
|
|
||||||
# The material name list may contain materials that are not on the mesh objects.
|
|
||||||
# Therefore, we can perhaps take the material_name_list as gospel and simply use it as a lookup table.
|
|
||||||
# If a look-up fails, replace it with an empty material.
|
|
||||||
options.materials = [bpy.data.materials.get(x.material_name, None) for x in pg.material_name_list]
|
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
@@ -319,9 +336,16 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
|||||||
draw_bone_filter_mode(bones_panel, self, True)
|
draw_bone_filter_mode(bones_panel, self, True)
|
||||||
|
|
||||||
if self.bone_filter_mode == 'BONE_COLLECTIONS':
|
if self.bone_filter_mode == 'BONE_COLLECTIONS':
|
||||||
bones_panel.operator(PSK_OT_populate_bone_collection_list.bl_idname, icon='FILE_REFRESH')
|
row = bones_panel.row()
|
||||||
rows = max(3, min(len(self.bone_collection_list), 10))
|
rows = max(3, min(len(self.bone_collection_list), 10))
|
||||||
bones_panel.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows)
|
row.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows)
|
||||||
|
col = row.column(align=True)
|
||||||
|
col.operator(PSK_OT_bone_collection_list_populate.bl_idname, text='', icon='FILE_REFRESH')
|
||||||
|
col.separator()
|
||||||
|
op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_HLT')
|
||||||
|
op.is_selected = True
|
||||||
|
op = col.operator(PSK_OT_bone_collection_list_select_all.bl_idname, text='', icon='CHECKBOX_DEHLT')
|
||||||
|
op.is_selected = False
|
||||||
|
|
||||||
advanced_bones_header, advanced_bones_panel = bones_panel.panel('Advanced', default_closed=True)
|
advanced_bones_header, advanced_bones_panel = bones_panel.panel('Advanced', default_closed=True)
|
||||||
advanced_bones_header.label(text='Advanced')
|
advanced_bones_header.label(text='Advanced')
|
||||||
@@ -336,15 +360,22 @@ class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
|||||||
materials_header.label(text='Materials', icon='MATERIAL')
|
materials_header.label(text='Materials', icon='MATERIAL')
|
||||||
|
|
||||||
if materials_panel:
|
if materials_panel:
|
||||||
materials_panel.operator(PSK_OT_populate_material_name_list.bl_idname, icon='FILE_REFRESH')
|
flow = materials_panel.grid_flow(row_major=True)
|
||||||
rows = max(3, min(len(self.material_name_list), 10))
|
flow.use_property_split = True
|
||||||
row = materials_panel.row()
|
flow.use_property_decorate = False
|
||||||
row.template_list('PSK_UL_material_names', '', self, 'material_name_list', self, 'material_name_list_index', rows=rows)
|
flow.prop(self, 'material_order_mode', text='Material Order')
|
||||||
col = row.column(align=True)
|
|
||||||
col.operator(PSK_OT_material_list_name_move_up.bl_idname, text='', icon='TRIA_UP')
|
if self.material_order_mode == 'MANUAL':
|
||||||
col.operator(PSK_OT_material_list_name_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
rows = max(3, min(len(self.material_name_list), 10))
|
||||||
col.separator()
|
row = materials_panel.row()
|
||||||
col.operator(PSK_OT_material_list_name_add.bl_idname, text='', icon='ADD')
|
row.template_list('PSK_UL_material_names', '', self, 'material_name_list', self, 'material_name_list_index', rows=rows)
|
||||||
|
col = row.column(align=True)
|
||||||
|
col.operator(PSK_OT_populate_material_name_list.bl_idname, text='', icon='FILE_REFRESH')
|
||||||
|
col.separator()
|
||||||
|
col.operator(PSK_OT_material_list_name_move_up.bl_idname, text='', icon='TRIA_UP')
|
||||||
|
col.operator(PSK_OT_material_list_name_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
||||||
|
col.separator()
|
||||||
|
col.operator(PSK_OT_material_list_name_add.bl_idname, text='', icon='ADD')
|
||||||
|
|
||||||
# Transform
|
# Transform
|
||||||
transform_header, transform_panel = layout.panel('Transform', default_closed=False)
|
transform_header, transform_panel = layout.panel('Transform', default_closed=False)
|
||||||
@@ -364,7 +395,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
bl_idname = 'psk.export'
|
bl_idname = 'psk.export'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
bl_description = 'Export mesh and armature to PSK'
|
bl_description = 'Export selected meshes to PSK'
|
||||||
filename_ext = '.psk'
|
filename_ext = '.psk'
|
||||||
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
|
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
|
||||||
filepath: StringProperty(
|
filepath: StringProperty(
|
||||||
@@ -387,7 +418,7 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_objects], pg.material_name_list)
|
populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_dfs_objects], pg.material_name_list)
|
||||||
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'}
|
||||||
@@ -419,17 +450,30 @@ class PSK_OT_export(Operator, ExportHelper):
|
|||||||
row = bones_panel.row()
|
row = bones_panel.row()
|
||||||
rows = max(3, min(len(pg.bone_collection_list), 10))
|
rows = max(3, min(len(pg.bone_collection_list), 10))
|
||||||
row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows)
|
row.template_list('PSX_UL_bone_collection_list', '', pg, 'bone_collection_list', pg, 'bone_collection_list_index', rows=rows)
|
||||||
|
bones_advanced_header, bones_advanced_panel = bones_panel.panel('Advanced', default_closed=True)
|
||||||
|
bones_advanced_header.label(text='Advanced')
|
||||||
|
if bones_advanced_panel:
|
||||||
|
flow = bones_advanced_panel.grid_flow(row_major=True)
|
||||||
|
flow.use_property_split = True
|
||||||
|
flow.use_property_decorate = False
|
||||||
|
flow.prop(pg, 'root_bone_name')
|
||||||
|
|
||||||
# Materials
|
# Materials
|
||||||
materials_header, materials_panel = layout.panel('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()
|
flow = materials_panel.grid_flow(row_major=True)
|
||||||
rows = max(3, min(len(pg.bone_collection_list), 10))
|
flow.use_property_split = True
|
||||||
row.template_list('PSK_UL_material_names', '', pg, 'material_name_list', pg, 'material_name_list_index', rows=rows)
|
flow.use_property_decorate = False
|
||||||
col = row.column(align=True)
|
flow.prop(pg, 'material_order_mode', text='Material Order')
|
||||||
col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP')
|
|
||||||
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
if pg.material_order_mode == 'MANUAL':
|
||||||
|
row = materials_panel.row()
|
||||||
|
rows = max(3, min(len(pg.bone_collection_list), 10))
|
||||||
|
row.template_list('PSK_UL_material_names', '', pg, 'material_name_list', pg, 'material_name_list_index', rows=rows)
|
||||||
|
col = row.column(align=True)
|
||||||
|
col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP')
|
||||||
|
col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN')
|
||||||
|
|
||||||
# Transform
|
# Transform
|
||||||
transform_header, transform_panel = layout.panel('Transform', default_closed=False)
|
transform_header, transform_panel = layout.panel('Transform', default_closed=False)
|
||||||
@@ -470,7 +514,8 @@ classes = (
|
|||||||
PSK_OT_material_list_move_down,
|
PSK_OT_material_list_move_down,
|
||||||
PSK_OT_export,
|
PSK_OT_export,
|
||||||
PSK_OT_export_collection,
|
PSK_OT_export_collection,
|
||||||
PSK_OT_populate_bone_collection_list,
|
PSK_OT_bone_collection_list_populate,
|
||||||
|
PSK_OT_bone_collection_list_select_all,
|
||||||
PSK_OT_populate_material_name_list,
|
PSK_OT_populate_material_name_list,
|
||||||
PSK_OT_material_list_name_move_up,
|
PSK_OT_material_list_name_move_up,
|
||||||
PSK_OT_material_list_name_move_down,
|
PSK_OT_material_list_name_move_down,
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ object_eval_state_items = (
|
|||||||
('ORIGINAL', 'Original', 'Use data from original object with no modifiers applied'),
|
('ORIGINAL', 'Original', 'Use data from original object with no modifiers applied'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
material_order_mode_items = (
|
||||||
|
('AUTOMATIC', 'Automatic', 'Automatically order the materials'),
|
||||||
|
('MANUAL', 'Manual', 'Manually arrange the materials'),
|
||||||
|
)
|
||||||
|
|
||||||
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()
|
||||||
@@ -47,6 +52,12 @@ class PskExportMixin(ExportSpaceMixin, ForwardUpAxisMixin):
|
|||||||
)
|
)
|
||||||
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)
|
||||||
|
material_order_mode: EnumProperty(
|
||||||
|
name='Material Order',
|
||||||
|
description='The order in which to export the materials',
|
||||||
|
items=material_order_mode_items,
|
||||||
|
default='AUTOMATIC'
|
||||||
|
)
|
||||||
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
|
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
|
||||||
material_name_list_index: IntProperty(default=0)
|
material_name_list_index: IntProperty(default=0)
|
||||||
root_bone_name: StringProperty(
|
root_bone_name: StringProperty(
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ class PSK_UL_material_names(UIList):
|
|||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
material = bpy.data.materials.get(item.material_name, None)
|
material = bpy.data.materials.get(item.material_name, None)
|
||||||
row.prop(item, 'material_name', text='', emboss=False, icon_value=layout.icon(material) if material else 0)
|
icon_value = layout.icon(material) if material else 0
|
||||||
|
row.prop(item, 'material_name', text='', emboss=False, icon_value=icon_value, icon='BLANK1' if icon_value == 0 else 'NONE')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from mathutils import Quaternion, Vector, Matrix
|
|||||||
|
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
from .properties import poly_flags_to_triangle_type_and_bit_flags
|
from .properties import poly_flags_to_triangle_type_and_bit_flags
|
||||||
|
from ..shared.data import PsxBone
|
||||||
from ..shared.helpers import rgb_to_srgb, is_bdk_addon_loaded
|
from ..shared.helpers import rgb_to_srgb, is_bdk_addon_loaded
|
||||||
|
|
||||||
|
|
||||||
@@ -31,9 +32,9 @@ class ImportBone:
|
|||||||
'''
|
'''
|
||||||
Intermediate bone type for the purpose of construction.
|
Intermediate bone type for the purpose of construction.
|
||||||
'''
|
'''
|
||||||
def __init__(self, index: int, psk_bone: Psk.Bone):
|
def __init__(self, index: int, psk_bone: PsxBone):
|
||||||
self.index: int = index
|
self.index: int = index
|
||||||
self.psk_bone: Psk.Bone = psk_bone
|
self.psk_bone: PsxBone = psk_bone
|
||||||
self.parent: Optional[ImportBone] = None
|
self.parent: Optional[ImportBone] = None
|
||||||
self.local_rotation: Quaternion = Quaternion()
|
self.local_rotation: Quaternion = Quaternion()
|
||||||
self.local_translation: Vector = Vector()
|
self.local_translation: Vector = Vector()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def read_psk(path: str) -> Psk:
|
|||||||
case b'MATT0000':
|
case b'MATT0000':
|
||||||
_read_types(fp, Psk.Material, section, psk.materials)
|
_read_types(fp, Psk.Material, section, psk.materials)
|
||||||
case b'REFSKELT':
|
case b'REFSKELT':
|
||||||
_read_types(fp, Psk.Bone, section, psk.bones)
|
_read_types(fp, PsxBone, section, psk.bones)
|
||||||
case b'RAWWEIGHTS':
|
case b'RAWWEIGHTS':
|
||||||
_read_types(fp, Psk.Weight, section, psk.weights)
|
_read_types(fp, Psk.Weight, section, psk.weights)
|
||||||
case b'FACE3200':
|
case b'FACE3200':
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from ctypes import Structure, sizeof
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from .data import Psk
|
from .data import Psk
|
||||||
from ..shared.data import Section, Vector3
|
from ..shared.data import Section, Vector3, PsxBone
|
||||||
|
|
||||||
MAX_WEDGE_COUNT = 65536
|
MAX_WEDGE_COUNT = 65536
|
||||||
MAX_POINT_COUNT = 4294967296
|
MAX_POINT_COUNT = 4294967296
|
||||||
@@ -55,7 +55,7 @@ def write_psk(psk: Psk, path: str):
|
|||||||
_write_section(fp, b'VTXW0000', Psk.Wedge16, wedges)
|
_write_section(fp, b'VTXW0000', Psk.Wedge16, wedges)
|
||||||
_write_section(fp, b'FACE0000', Psk.Face, psk.faces)
|
_write_section(fp, b'FACE0000', Psk.Face, psk.faces)
|
||||||
_write_section(fp, b'MATT0000', Psk.Material, psk.materials)
|
_write_section(fp, b'MATT0000', Psk.Material, psk.materials)
|
||||||
_write_section(fp, b'REFSKELT', Psk.Bone, psk.bones)
|
_write_section(fp, b'REFSKELT', PsxBone, psk.bones)
|
||||||
_write_section(fp, b'RAWWEIGHTS', Psk.Weight, psk.weights)
|
_write_section(fp, b'RAWWEIGHTS', Psk.Weight, psk.weights)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
raise RuntimeError(f'The current user "{os.getlogin()}" does not have permission to write to "{path}"') from e
|
raise RuntimeError(f'The current user "{os.getlogin()}" does not have permission to write to "{path}"') from e
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from ctypes import *
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from bpy.props import EnumProperty
|
from bpy.props import EnumProperty
|
||||||
from mathutils import Vector, Matrix, Quaternion as BpyQuaternion
|
from mathutils import Quaternion as BpyQuaternion
|
||||||
|
|
||||||
|
|
||||||
class Color(Structure):
|
class Color(Structure):
|
||||||
@@ -94,6 +94,19 @@ class Quaternion(Structure):
|
|||||||
return quaternion
|
return quaternion
|
||||||
|
|
||||||
|
|
||||||
|
class PsxBone(Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('name', c_char * 64),
|
||||||
|
('flags', c_int32),
|
||||||
|
('children_count', c_int32),
|
||||||
|
('parent_index', c_int32),
|
||||||
|
('rotation', Quaternion),
|
||||||
|
('location', Vector3),
|
||||||
|
('length', c_float),
|
||||||
|
('size', Vector3)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Section(Structure):
|
class Section(Structure):
|
||||||
_fields_ = [
|
_fields_ = [
|
||||||
('name', c_char * 20),
|
('name', c_char * 20),
|
||||||
@@ -171,30 +184,3 @@ class ExportSpaceMixin:
|
|||||||
items=export_space_items,
|
items=export_space_items,
|
||||||
default='WORLD'
|
default='WORLD'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
|
|
||||||
match axis_identifier:
|
|
||||||
case 'X':
|
|
||||||
return Vector((1.0, 0.0, 0.0))
|
|
||||||
case 'Y':
|
|
||||||
return Vector((0.0, 1.0, 0.0))
|
|
||||||
case 'Z':
|
|
||||||
return Vector((0.0, 0.0, 1.0))
|
|
||||||
case '-X':
|
|
||||||
return Vector((-1.0, 0.0, 0.0))
|
|
||||||
case '-Y':
|
|
||||||
return Vector((0.0, -1.0, 0.0))
|
|
||||||
case '-Z':
|
|
||||||
return Vector((0.0, 0.0, -1.0))
|
|
||||||
|
|
||||||
|
|
||||||
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
|
|
||||||
forward = get_vector_from_axis_identifier(forward_axis)
|
|
||||||
up = get_vector_from_axis_identifier(up_axis)
|
|
||||||
left = up.cross(forward)
|
|
||||||
return Matrix((
|
|
||||||
(forward.x, forward.y, forward.z, 0.0),
|
|
||||||
(left.x, left.y, left.z, 0.0),
|
|
||||||
(up.x, up.y, up.z, 0.0),
|
|
||||||
(0.0, 0.0, 0.0, 1.0)
|
|
||||||
))
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from typing import List, Iterable, cast, Optional
|
from collections import Counter
|
||||||
|
from typing import List, Iterable, cast, Optional, Dict, Tuple
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import CollectionProperty
|
from bpy.props import CollectionProperty
|
||||||
from bpy.types import AnimData, Object
|
from bpy.types import AnimData, Object
|
||||||
from bpy.types import Armature
|
from bpy.types import Armature
|
||||||
from mathutils import Matrix
|
from mathutils import Matrix, Vector
|
||||||
|
|
||||||
from .data import get_coordinate_system_transform
|
from .data import Vector3, Quaternion
|
||||||
|
from ..shared.data import PsxBone
|
||||||
|
|
||||||
|
|
||||||
def rgb_to_srgb(c: float):
|
def rgb_to_srgb(c: float):
|
||||||
@@ -171,16 +173,15 @@ def convert_string_to_cp1252_bytes(string: str) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Perhaps export space should just be a transform matrix, since the below is not actually used unless we're using WORLD space.
|
# TODO: Perhaps export space should just be a transform matrix, since the below is not actually used unless we're using WORLD space.
|
||||||
def convert_blender_bones_to_psx_bones(
|
def create_psx_bones_from_blender_bones(
|
||||||
bones: Iterable[bpy.types.Bone],
|
bones: List[bpy.types.Bone],
|
||||||
bone_class: type,
|
|
||||||
export_space: str = 'WORLD',
|
export_space: str = 'WORLD',
|
||||||
armature_object_matrix_world: Matrix = Matrix.Identity(4),
|
armature_object_matrix_world: Matrix = Matrix.Identity(4),
|
||||||
scale = 1.0,
|
scale = 1.0,
|
||||||
forward_axis: str = 'X',
|
forward_axis: str = 'X',
|
||||||
up_axis: str = 'Z',
|
up_axis: str = 'Z',
|
||||||
root_bone: Optional = None,
|
root_bone: Optional = None,
|
||||||
) -> Iterable:
|
) -> List[PsxBone]:
|
||||||
|
|
||||||
scale_matrix = Matrix.Scale(scale, 4)
|
scale_matrix = Matrix.Scale(scale, 4)
|
||||||
|
|
||||||
@@ -189,7 +190,7 @@ def convert_blender_bones_to_psx_bones(
|
|||||||
|
|
||||||
psx_bones = []
|
psx_bones = []
|
||||||
for bone in bones:
|
for bone in bones:
|
||||||
psx_bone = bone_class()
|
psx_bone = PsxBone()
|
||||||
psx_bone.name = convert_string_to_cp1252_bytes(bone.name)
|
psx_bone.name = convert_string_to_cp1252_bytes(bone.name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -282,3 +283,148 @@ def get_export_space_matrix(export_space: str, armature_object: Optional[Object]
|
|||||||
pass
|
pass
|
||||||
case _:
|
case _:
|
||||||
assert False, f'Invalid export space: {export_space}'
|
assert False, f'Invalid export space: {export_space}'
|
||||||
|
|
||||||
|
|
||||||
|
class PsxBoneCreateResult:
|
||||||
|
def __init__(self,
|
||||||
|
bones: List[Tuple[PsxBone, Optional[Object]]], # List of tuples of (psx_bone, armature_object)
|
||||||
|
armature_object_root_bone_indices: Dict[Object, int],
|
||||||
|
armature_object_bone_names: Dict[Object, List[str]],
|
||||||
|
):
|
||||||
|
self.bones = bones
|
||||||
|
self.armature_object_root_bone_indices = armature_object_root_bone_indices
|
||||||
|
self.armature_object_bone_names = armature_object_bone_names
|
||||||
|
|
||||||
|
|
||||||
|
def create_psx_bones(
|
||||||
|
armature_objects: List[Object],
|
||||||
|
export_space: str = 'WORLD',
|
||||||
|
root_bone_name: str = 'ROOT',
|
||||||
|
forward_axis: str = 'X',
|
||||||
|
up_axis: str = 'Z',
|
||||||
|
scale: float = 1.0,
|
||||||
|
bone_filter_mode: str = 'ALL',
|
||||||
|
bone_collection_indices: Optional[List[Tuple[str, int]]] = None,
|
||||||
|
) -> PsxBoneCreateResult:
|
||||||
|
'''
|
||||||
|
Creates a list of PSX bones from the given armature objects and options.
|
||||||
|
This function will throw a RuntimeError if multiple armature objects are passed in and the export space is not WORLD.
|
||||||
|
It will also throw a RuntimeError if the bone names are not unique when compared case-insensitively.
|
||||||
|
'''
|
||||||
|
if bone_collection_indices is None:
|
||||||
|
bone_collection_indices = []
|
||||||
|
|
||||||
|
bones: List[Tuple[PsxBone, Optional[Object]]] = []
|
||||||
|
|
||||||
|
if export_space != 'WORLD' and len(armature_objects) > 1:
|
||||||
|
raise RuntimeError('When exporting multiple armatures, the Export Space must be World')
|
||||||
|
|
||||||
|
coordinate_system_matrix = get_coordinate_system_transform(forward_axis, up_axis)
|
||||||
|
coordinate_system_default_rotation = coordinate_system_matrix.to_quaternion()
|
||||||
|
|
||||||
|
total_bone_count = sum(len(armature_object.data.bones) for armature_object in armature_objects)
|
||||||
|
|
||||||
|
# Store the index of the root bone for each armature object.
|
||||||
|
# We will need this later to correctly assign vertex weights.
|
||||||
|
armature_object_root_bone_indices = dict()
|
||||||
|
|
||||||
|
# Store the bone names to be exported for each armature object.
|
||||||
|
armature_object_bone_names: Dict[Object, List[str]] = dict()
|
||||||
|
for armature_object in armature_objects:
|
||||||
|
armature_bone_collection_indices = [x[1] for x in bone_collection_indices if x[0] == armature_object.name]
|
||||||
|
bone_names = get_export_bone_names(armature_object, bone_filter_mode, armature_bone_collection_indices)
|
||||||
|
armature_object_bone_names[armature_object] = bone_names
|
||||||
|
|
||||||
|
if len(armature_objects) == 0 or total_bone_count == 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.
|
||||||
|
psx_bone = PsxBone()
|
||||||
|
psx_bone.name = convert_string_to_cp1252_bytes(root_bone_name)
|
||||||
|
psx_bone.flags = 0
|
||||||
|
psx_bone.children_count = 0
|
||||||
|
psx_bone.parent_index = 0
|
||||||
|
psx_bone.location = Vector3.zero()
|
||||||
|
psx_bone.rotation = Quaternion.from_bpy_quaternion(coordinate_system_default_rotation)
|
||||||
|
bones.append((psx_bone, None))
|
||||||
|
|
||||||
|
armature_object_root_bone_indices[None] = 0
|
||||||
|
else:
|
||||||
|
# If we have multiple armature objects, create a root bone at the world origin.
|
||||||
|
if len(armature_objects) > 1:
|
||||||
|
psx_bone = PsxBone()
|
||||||
|
psx_bone.name = convert_string_to_cp1252_bytes(root_bone_name)
|
||||||
|
psx_bone.flags = 0
|
||||||
|
psx_bone.children_count = total_bone_count
|
||||||
|
psx_bone.parent_index = 0
|
||||||
|
psx_bone.location = Vector3.zero()
|
||||||
|
psx_bone.rotation = Quaternion.from_bpy_quaternion(coordinate_system_default_rotation)
|
||||||
|
bones.append((psx_bone, None))
|
||||||
|
|
||||||
|
armature_object_root_bone_indices[None] = 0
|
||||||
|
|
||||||
|
root_bone = bones[0][0] if len(bones) > 0 else None
|
||||||
|
|
||||||
|
for armature_object in armature_objects:
|
||||||
|
bone_names = armature_object_bone_names[armature_object]
|
||||||
|
armature_data = cast(Armature, armature_object.data)
|
||||||
|
armature_bones = [armature_data.bones[bone_name] for bone_name in bone_names]
|
||||||
|
|
||||||
|
armature_psx_bones = create_psx_bones_from_blender_bones(
|
||||||
|
bones=armature_bones,
|
||||||
|
export_space=export_space,
|
||||||
|
armature_object_matrix_world=armature_object.matrix_world,
|
||||||
|
scale=scale,
|
||||||
|
forward_axis=forward_axis,
|
||||||
|
up_axis=up_axis,
|
||||||
|
root_bone=root_bone,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we are appending these bones to an existing list of bones, we need to adjust the parent indices.
|
||||||
|
if len(bones) > 0:
|
||||||
|
parent_index_offset = len(bones)
|
||||||
|
for bone in armature_psx_bones[1:]:
|
||||||
|
bone.parent_index += parent_index_offset
|
||||||
|
|
||||||
|
armature_object_root_bone_indices[armature_object] = len(bones)
|
||||||
|
|
||||||
|
bones.extend((psx_bone, armature_object) for psx_bone in armature_psx_bones)
|
||||||
|
|
||||||
|
# Check if there are bone name conflicts between armatures.
|
||||||
|
bone_name_counts = Counter(bone[0].name.decode('windows-1252').upper() for bone in bones)
|
||||||
|
for bone_name, count in bone_name_counts.items():
|
||||||
|
if count > 1:
|
||||||
|
raise RuntimeError(f'Found {count} bones with the name "{bone_name}". Bone names must be unique when compared case-insensitively.')
|
||||||
|
|
||||||
|
return PsxBoneCreateResult(
|
||||||
|
bones=bones,
|
||||||
|
armature_object_root_bone_indices=armature_object_root_bone_indices,
|
||||||
|
armature_object_bone_names=armature_object_bone_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vector_from_axis_identifier(axis_identifier: str) -> Vector:
|
||||||
|
match axis_identifier:
|
||||||
|
case 'X':
|
||||||
|
return Vector((1.0, 0.0, 0.0))
|
||||||
|
case 'Y':
|
||||||
|
return Vector((0.0, 1.0, 0.0))
|
||||||
|
case 'Z':
|
||||||
|
return Vector((0.0, 0.0, 1.0))
|
||||||
|
case '-X':
|
||||||
|
return Vector((-1.0, 0.0, 0.0))
|
||||||
|
case '-Y':
|
||||||
|
return Vector((0.0, -1.0, 0.0))
|
||||||
|
case '-Z':
|
||||||
|
return Vector((0.0, 0.0, -1.0))
|
||||||
|
|
||||||
|
|
||||||
|
def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix:
|
||||||
|
forward = get_vector_from_axis_identifier(forward_axis)
|
||||||
|
up = get_vector_from_axis_identifier(up_axis)
|
||||||
|
left = up.cross(forward)
|
||||||
|
return Matrix((
|
||||||
|
(forward.x, forward.y, forward.z, 0.0),
|
||||||
|
(left.x, left.y, left.z, 0.0),
|
||||||
|
(up.x, up.y, up.z, 0.0),
|
||||||
|
(0.0, 0.0, 0.0, 1.0)
|
||||||
|
))
|
||||||
|
|||||||
Reference in New Issue
Block a user