UV Transfer Tool
A simple UV Transfer tool for Maya that has helped me a lot.
My delusional self, thought it was easier to develop a tool than to manually transfer UVs between 100+ screws back in 2021…
#Gerardo Contreras 2025 August, Version 2.0
from maya import cmds
class UVTransferTool:
def __init__(self):
self.source = []
self.target = []
self.transfer_method = 5 # Default: Topology
self.symmetry_axis_scale = (-1, 1, 1) # Default X axis
self.win_name = "UVTransferToolWin"
self.match_names = True # Default ON for Group tab
self.current_tab = 0 # 0 = Duplicates, 1 = Group, 2 = Symmetry
self.sample_space_group = None
self.sample_space_buttons = {}
# ---------------- Utilities ----------------
def delete_history(self, objs=None):
if objs is None:
objs = cmds.ls(selection=True)
if not objs:
return
for obj in objs:
if cmds.objExists(obj):
cmds.delete(obj, constructionHistory=True)
def notify(self, msg="Operation Completed", success=True):
color = "white" if success else "#FF4444"
cmds.inViewMessage(
amg=f'<span style="color:{color};">{msg}</span>',
pos="topCenter",
fade=True
)
def reset_tool(self, *args):
# Reset internal state
self.source = []
self.target = []
self.transfer_method = 5
self.symmetry_axis_scale = (-1, 1, 1)
self.match_names = True
self.current_tab = 0
# Reset Sample Space radio group
if self.sample_space_group and "Topology" in self.sample_space_buttons:
cmds.radioCollection(self.sample_space_group, edit=True, select=self.sample_space_buttons["Topology"])
# Reset symmetry axis radios
if hasattr(self, "axis_radio_collection") and hasattr(self, "axis_radio_buttons"):
try:
cmds.radioCollection(self.axis_radio_collection, edit=True, select=self.axis_radio_buttons["X"])
except Exception:
pass
# Reset the Group tab checkbox if it exists
if hasattr(self, "match_checkbox"):
try:
cmds.checkBox(self.match_checkbox, edit=True, value=True)
except Exception:
pass
# Jump back to first tab
if hasattr(self, "tab_layout") and cmds.tabLayout(self.tab_layout, exists=True):
try:
cmds.tabLayout(self.tab_layout, edit=True, selectTabIndex=1)
except Exception:
pass
self.notify("Tool reset to defaults", success=True)
def show_help(self, *args):
help_text = (
"Instructions:\n"
"\n--- Duplicates ---\n"
"Select source object first, then one or more targets.\n"
"\n--- Group ---\n"
"Set Source and Target groups with the buttons in the Group tab.\n"
"Match Name will transfer to objects with the exact name ignoring the __pasted suffix created by maya\n"
"\n--- Symmetry ---\n"
"Select exactly 2 objects: source to mirror first, then target.\n Then make sure your axis matches the direction of the target.\n"
"\n--- General ---\n"
"After every transfer, the tool will run Delete All by Type > History."
)
cmds.confirmDialog(
title="UV Transfer Tool - Help",
message=help_text,
button=["OK"],
defaultButton="OK"
)
# ---------------- UI Builders ----------------
def build_transfer_mode_ui(self):
cmds.text("Sample Space:", align="left")
row = cmds.rowLayout(numberOfColumns=5, adjustableColumn=5,
columnAlign=(1, "center"), columnAttach5=["both"]*5)
self.sample_space_group = cmds.radioCollection()
options = ["World", "Local", "UV Space", "Component", "Topology"]
sample_spaces = [0, 1, 3, 4, 5]
for name, space in zip(options, sample_spaces):
self.sample_space_buttons[name] = cmds.radioButton(
label=name,
onCommand=lambda _, m=space: self.set_transfer_method(m)
)
cmds.radioCollection(self.sample_space_group, edit=True, select=self.sample_space_buttons["Topology"])
cmds.setParent("..")
def set_transfer_method(self, method):
self.transfer_method = method
def build_name_match_ui(self):
self.match_checkbox = cmds.checkBox(
label="Match exact name objects and ignores [__pasted] suffix",
value=True,
onc=lambda *_: self.set_match_names(True),
ofc=lambda *_: self.set_match_names(False)
)
def set_match_names(self, state):
self.match_names = state
def build_symmetry_axis_ui(self):
cmds.text("Mirror Axis:", align="left")
row = cmds.rowLayout(numberOfColumns=3, adjustableColumn=3, columnAlign=(1, "center"))
self.axis_radio_collection = cmds.radioCollection()
self.axis_radio_buttons = {}
axes = ["X", "Y", "Z"]
scales = [(-1, 1, 1), (1, -1, 1), (1, 1, -1)]
for name, scale in zip(axes, scales):
self.axis_radio_buttons[name] = cmds.radioButton(
label=name,
onCommand=lambda _, s=scale: self.set_symmetry_axis(s)
)
cmds.radioCollection(self.axis_radio_collection, edit=True, select=self.axis_radio_buttons["X"])
cmds.setParent("..")
def set_symmetry_axis(self, scale_tuple):
self.symmetry_axis_scale = scale_tuple
# ---------------- Selection Helpers ----------------
def select_source(self, *args):
self.source = cmds.ls(sl=1, dag=1, type=['mesh'])
self.notify(
f"Source set: {len(self.source)} mesh(es)" if self.source else "No source selected",
success=bool(self.source)
)
def select_target(self, *args):
self.target = cmds.ls(sl=1, dag=1, type=['mesh'])
self.notify(
f"Target set: {len(self.target)} mesh(es)" if self.target else "No target selected",
success=bool(self.target)
)
# ---------------- Core Functions ----------------
def copy_uvs_duplicates(self):
selected = cmds.ls(sl=True, type='transform')
if not selected or len(selected) < 2:
raise RuntimeError("Select at least 2 objects (source + target).")
driver = selected.pop(0)
for obj in selected:
print(f"[TRANSFER] source: {driver}, target: {obj}, sampleSpace: {self.transfer_method}")
cmds.select([driver, obj], r=True)
cmds.transferAttributes(
sampleSpace=self.transfer_method,
transferUVs=2,
transferColors=2
)
cmds.delete(all=True, constructionHistory=True)
def copy_uvs_group(self):
if not self.source or not self.target:
raise RuntimeError("Please select source and target first.")
if self.match_names:
for src in self.source:
src_name = src.split("|")[-1].replace("pasted__", "")
for tgt in self.target:
tgt_name = tgt.split("|")[-1].replace("pasted__", "")
if src_name != tgt_name:
continue
print(f"[TRANSFER] source: {src}, target: {tgt}, sampleSpace: {self.transfer_method}")
cmds.select([src, tgt], r=True)
cmds.transferAttributes(
transferUVs=2,
transferColors=2,
sampleSpace=self.transfer_method
)
else:
for src in self.source:
for tgt in self.target:
print(f"[TRANSFER] source: {src}, target: {tgt}, sampleSpace: {self.transfer_method}")
cmds.select([src, tgt], r=True)
cmds.transferAttributes(
transferUVs=2,
transferColors=2,
sampleSpace=self.transfer_method
)
cmds.delete(all=True, constructionHistory=True)
def symmetrical_copy(self):
selected = cmds.ls(sl=True, type='transform')
if not selected or len(selected) != 2:
raise RuntimeError("Select exactly 2 objects: source to mirror + target.")
source_obj = selected[0]
target_obj = selected[1]
duplicate_list = cmds.duplicate(source_obj, name=f"{source_obj}_symDuplicate")
duplicate = duplicate_list[0]
cmds.scale(*self.symmetry_axis_scale, duplicate)
cmds.makeIdentity(duplicate, apply=True, t=1, r=1, s=1, n=0, pn=1)
self.delete_history(duplicate)
print(f"[TRANSFER] source: {duplicate}, target: {target_obj}, sampleSpace: {self.transfer_method}")
cmds.select([duplicate, target_obj], r=True)
cmds.transferAttributes(
sampleSpace=self.transfer_method,
transferUVs=2,
transferColors=2
)
self.delete_history([duplicate, source_obj, target_obj])
cmds.delete(duplicate)
cmds.delete(all=True, constructionHistory=True)
def run_transfer(self, *args):
try:
if self.current_tab == 0:
self.copy_uvs_duplicates()
self.notify("UV Transfer Completed (Duplicates)", success=True)
elif self.current_tab == 1:
self.copy_uvs_group()
self.notify("UV Transfer Completed (Group)", success=True)
elif self.current_tab == 2:
self.symmetrical_copy()
self.notify("UV Transfer Completed (Symmetry)", success=True)
except Exception as e:
self.notify(f"UV Transfer Failed: {str(e)}", success=False)
raise
# ---------------- UI ----------------
def build_ui(self):
if cmds.window(self.win_name, exists=True):
cmds.deleteUI(self.win_name)
window = cmds.window(self.win_name, title="UV Transfer Tool", widthHeight=(600, 420), sizeable=True)
# Toolbar (menu bar style)
cmds.menuBarLayout()
cmds.menu(label="Edit")
cmds.menuItem(label="Reset Tool", command=self.reset_tool)
cmds.menu(label="Help")
cmds.menuItem(label="Instructions", command=self.show_help)
main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=8)
# Shared Sample Space
self.build_transfer_mode_ui()
cmds.separator(h=10, style="in")
# Tabs
self.tab_layout = cmds.tabLayout(innerMarginWidth=10, innerMarginHeight=10, changeCommand=self.update_tab)
tabs = []
# --- Duplicates tab ---
tab1 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.text(label="Copy UVs to Duplicates:\nSelect source object first, then one or more targets.", align="left")
tabs.append(tab1)
cmds.setParent("..")
# --- Group tab ---
tab2 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.text(label="Copy UVs between Groups:\nSet Source and Target groups with the buttons below.", align="left")
self.build_name_match_ui()
cmds.rowLayout(numberOfColumns=2, columnWidth2=(270, 270),
adjustableColumn=2, columnAlign2=("center", "center"))
cmds.button(label="Select Source Group", command=self.select_source, h=30)
cmds.button(label="Select Target Group", command=self.select_target, h=30)
cmds.setParent("..")
tabs.append(tab2)
cmds.setParent("..")
# --- Symmetry tab ---
tab3 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.text(label="Symmetry Transfer:\nSelect exactly 2 objects: source to mirror, then target.", align="left")
self.build_symmetry_axis_ui()
tabs.append(tab3)
cmds.setParent("..")
cmds.tabLayout(self.tab_layout, edit=True,
tabLabel=[(tabs[0], "Duplicates"), (tabs[1], "Group"), (tabs[2], "Symmetry")])
# Bottom action buttons
cmds.setParent("..")
cmds.separator(h=15, style="in")
cmds.button(label="Transfer", command=self.run_transfer, h=30)
cmds.separator(h=10, style="in")
cmds.button(label="Close", command=lambda *_: cmds.deleteUI(window, window=True), h=30)
cmds.showWindow(window)
def update_tab(self, *_):
# Update current tab index (1-based -> 0-based)
self.current_tab = cmds.tabLayout(self.tab_layout, query=True, selectTabIndex=True) - 1
# Run the tool
tool = UVTransferTool()
tool.build_ui()