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:
@@ -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)
|
||||||
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
|
if armature_bone_index is not None:
|
||||||
except ValueError:
|
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
||||||
# PSA bone could not be mapped directly to an armature bone by name.
|
if armature_bone_index not in armature_to_psa_bone_indices:
|
||||||
# Attempt to create a bone mapping by ignoring the case of the names.
|
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_names.index(psa_bone_name)
|
||||||
if options.bone_mapping_mode == 'CASE_INSENSITIVE':
|
armature_to_psa_bone_indices[armature_bone_index] = psa_bone_index
|
||||||
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
else:
|
||||||
if armature_bone_name.upper() == psa_bone_name.upper():
|
# This armature bone has already been mapped to a PSA bone.
|
||||||
psa_to_armature_bone_indices[psa_bone_index] = armature_bone_index
|
duplicate_mappings.append((psa_bone_index, armature_bone_index, armature_to_psa_bone_indices[armature_bone_index]))
|
||||||
psa_bone_name = armature_bone_name
|
psa_bone_names.append(armature_bone_names[armature_bone_index])
|
||||||
break
|
else:
|
||||||
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user