Handle importing PSAs with duplicate bone names gracefully

For Harry Potter 2 models, the rig and the PSA files have bones with identical names. This would cause the PSA importer to fail because it was expecting that each bone name would be unique. The new behavior is to ignore duplicate bones when importing the f-curve data, but print a warning to let the user know something is wacky with their PSA rig.

For example, if there are two bones, A and B, with the same name, the first bone encountered (A) in the tree will be mapped to the armature bone, but bone B will be ignored.
This commit is contained in:
Colin Basnett
2023-07-26 16:25:52 -07:00
parent 0428c2bd72
commit 84f168a9c0

View File

@@ -2,7 +2,6 @@ import fnmatch
import os import os
import re import re
import typing import typing
from collections import Counter
from typing import List, Optional from typing import List, Optional
import bpy import bpy
@@ -64,6 +63,23 @@ class PsaImportResult:
self.warnings: List[str] = [] self.warnings: List[str] = []
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]:
"""
@param psa_bone_name: The name of the PSA bone.
@param armature_bone_names: The names of the bones in the armature.
@param bone_mapping_mode: One of 'EXACT' or 'CASE_INSENSITIVE'.
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
"""
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
if bone_mapping_mode == 'CASE_INSENSITIVE':
if armature_bone_name.lower() == psa_bone_name.lower():
return armature_bone_index
else:
if armature_bone_name == psa_bone_name:
return armature_bone_index
return None
def import_psa(psa_reader: PsaReader, armature_object: bpy.types.Object, options: PsaImportOptions) -> PsaImportResult: def import_psa(psa_reader: PsaReader, armature_object: bpy.types.Object, options: PsaImportOptions) -> PsaImportResult:
result = PsaImportResult() result = PsaImportResult()
sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names) sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names)
@@ -71,37 +87,33 @@ def import_psa(psa_reader: PsaReader, armature_object: bpy.types.Object, options
# Create an index mapping from bones in the PSA to bones in the target armature. # Create an index mapping from bones in the PSA to bones in the target armature.
psa_to_armature_bone_indices = {} psa_to_armature_bone_indices = {}
armature_to_psa_bone_indices = {}
armature_bone_names = [x.name for x in armature_data.bones] armature_bone_names = [x.name for x in armature_data.bones]
psa_bone_names = [] psa_bone_names = []
duplicate_mappings = []
for psa_bone_index, psa_bone in enumerate(psa_reader.bones): for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
psa_bone_name: str = psa_bone.name.decode('windows-1252') psa_bone_name: str = psa_bone.name.decode('windows-1252')
try: armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping_mode)
if armature_bone_index is not None:
# Ensure that no other PSA bone has been mapped to this armature bone yet.
if armature_bone_index not in armature_to_psa_bone_indices:
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name) psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
except ValueError: armature_to_psa_bone_indices[armature_bone_index] = psa_bone_index
# PSA bone could not be mapped directly to an armature bone by name. else:
# Attempt to create a bone mapping by ignoring the case of the names. # This armature bone has already been mapped to a PSA bone.
if options.bone_mapping_mode == 'CASE_INSENSITIVE': duplicate_mappings.append((psa_bone_index, armature_bone_index, armature_to_psa_bone_indices[armature_bone_index]))
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names): psa_bone_names.append(armature_bone_names[armature_bone_index])
if armature_bone_name.upper() == psa_bone_name.upper(): else:
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_index
psa_bone_name = armature_bone_name
break
psa_bone_names.append(psa_bone_name) psa_bone_names.append(psa_bone_name)
# Remove ambiguous bone mappings (where multiple PSA bones correspond to the same armature bone). # Warn about duplicate bone mappings.
armature_bone_index_counts = Counter(psa_to_armature_bone_indices.values()) if len(duplicate_mappings) > 0:
for armature_bone_index, count in armature_bone_index_counts.items(): for (psa_bone_index, armature_bone_index, mapped_psa_bone_index) in duplicate_mappings:
if count > 1: psa_bone_name = psa_bone_names[psa_bone_index]
psa_bone_indices = [] armature_bone_name = armature_bone_names[armature_bone_index]
for psa_bone_index, mapped_bone_index in psa_to_armature_bone_indices: mapped_psa_bone_name = psa_bone_names[mapped_psa_bone_index]
if mapped_bone_index == armature_bone_index: result.warnings.append(f'PSA bone {psa_bone_index} ({psa_bone_name}) could not be mapped to armature bone {armature_bone_index} ({armature_bone_name}) because the armature bone already mapped to PSA bone {mapped_psa_bone_index} ({mapped_psa_bone_name})')
psa_bone_indices.append(psa_bone_index)
ambiguous_psa_bone_names = list(sorted([psa_bone_names[x] for x in psa_bone_indices]))
result.warnings.append(
f'Ambiguous mapping for bone {armature_bone_names[armature_bone_index]}!\n'
f'The following PSA bones all map to the same armature bone: {ambiguous_psa_bone_names}\n'
f'These bones will be ignored.'
)
# Report if there are missing bones in the target armature. # Report if there are missing bones in the target armature.
missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names)) missing_bone_names = set(psa_bone_names).difference(set(armature_bone_names))