Files
Nexus/2023/scripts/rigging_tools/ngskintools2/api/transfer.py
2025-11-24 08:27:50 +08:00

111 lines
4.5 KiB
Python

import itertools
from ngSkinTools2.api.python_compatibility import Object
from ngSkinTools2.decorators import undoable
from . import plugin
from .influenceMapping import InfluenceMapping, InfluenceMappingConfig
from .layers import init_layers, target_info
from .mirror import Mirror
from .suspend_updates import suspend_updates
class VertexTransferMode(Object):
"""
Constants for vertex_transfer_mode argument
"""
closestPoint = 'closestPoint' #: When vertices from two surface are matched, each destination mesh vertex finds a closest point on source mesh, and weights are calculated based on the triangle weights of that closest point.
uvSpace = 'uvSpace' #: Similar to closestPoint strategy, but matching is done in UV space instead of XYZ space.
vertexId = 'vertexId' #: Vertices are matched by ID. Not usable for mirroring; this is used for transfer/import cases where meshes are known to be identical
class LayersTransfer(Object):
def __init__(self):
self.source = None
self.target = None
self.source_file = None
self.vertex_transfer_mode = VertexTransferMode.closestPoint
self.influences_mapping = InfluenceMapping()
self.influences_mapping.config = InfluenceMappingConfig.transfer_defaults()
self.keep_existing_layers = True
self.customize_callback = None
def load_source_from_file(self, file, format):
from .import_export import FileFormatWrapper
with FileFormatWrapper(file, format=format, read_mode=True) as f:
data = plugin.ngst2tools(
tool="importJsonFile",
file=f.plain_file,
)
self.source = "-reference-mesh-"
self.source_file = file
influences = target_info.unserialize_influences_from_json_data(data['influences'])
self.influences_mapping.influences = influences
def calc_influences_mapping_as_flat_list(self):
mapping_pairs = list(self.influences_mapping.asIntIntMapping(self.influences_mapping.calculate()).items())
if len(mapping_pairs) == 0:
raise Exception("no mapping between source and destination influences")
# convert dict to flat array
return list(itertools.chain.from_iterable(mapping_pairs))
def execute(self):
# sanity check: destination must be skinnable target
if target_info.get_related_skin_cluster(self.target) is None:
return False
if not self.influences_mapping.influences:
if target_info.get_related_skin_cluster(self.source) is None:
return False
self.influences_mapping.influences = target_info.list_influences(self.source)
self.influences_mapping.destinationInfluences = target_info.list_influences(self.target)
if self.customize_callback is None:
self.complete_execution()
else:
self.customize_callback(self)
@undoable
def complete_execution(self):
l = init_layers(self.target)
Mirror(self.target).recalculate_influences_mapping()
with suspend_updates(self.target):
if not self.keep_existing_layers:
l.clear()
plugin.ngst2tools(
tool="transfer",
source=self.source,
target=self.target,
vertexTransferMode=self.vertex_transfer_mode,
influencesMapping=self.calc_influences_mapping_as_flat_list(),
)
def transfer_layers(
source, destination, vertex_transfer_mode=VertexTransferMode.closestPoint, influences_mapping_config=InfluenceMappingConfig.transfer_defaults()
):
"""
Transfer skinning layers from source to destination mesh.
:param str source: source mesh or skin cluster node name
:param str destination: destination mesh or skin cluster node name
:param str vertex_transfer_mode: describes how source mesh vertices are mapped to destination vertices. Defaults to `closestPoint`
:param InfluenceMappingConfig influences_mapping_config: configuration for InfluenceMapping; supply this, or `influences_mapping` instance; default settings for transfer are used if this is not supplied.
:param InfluenceMapping influences_mapping: mapper instance to use for matching influences; if this is provided, `influences_mapping_config` is ignored.
:return:
"""
t = LayersTransfer()
t.source = source
t.target = destination
t.vertex_transfer_mode = vertex_transfer_mode
t.influences_mapping.config = influences_mapping_config
t.execute()