
import maya.cmds as cmds
import pymel.core as pm
import os
def remap_selected_texture(z_path):
# figure out what's selected
selected = pm.ls(sl=True)
workspace_path = cmds.workspace(query=True, rootDirectory=True)
print("workspace_path= "+workspace_path)
for item in selected:
test_path = pm.getAttr(item+".ftn")
fileName = test_path.split('/')[-1]
fileName = fileName.replace('//', '')
fileName = fileName.replace('/', '')
print("fileName2= "+fileName)
#if ':' not in test_path:
if(fileName=="GraceYong_ArmsColorD1001.jpg"):
fileName="GraceYong_ArmsColorD_1001.jpg"
if (1==1):
#print("fileName1= "+fileName)
#new_path = os.path.join(z_path, 'cartoon_room', second)
new_path = os.path.join(workspace_path,z_path, fileName)
new_path = new_path.replace('\\', '/')
new_path = new_path.replace('//', '/')
relative_path = os.path.join(z_path, fileName)
relative_path = relative_path.replace('\\', '/')
new_path = new_path.replace('//', '/')
if os.path.exists(new_path):
if(fileName==""):
print("item= "+str(item)+" is No fileName= "+ fileName)
cmds.select( str(item), r=True )
cmds.delete( str(item) )
else:
print("new_path= "+new_path+' is exists! OK')
pm.setAttr(item+'.ftn', new_path)
#pm.setAttr(item+'.ftn', relative_path)
else:
print("new_path= "+new_path+ ' not exists NG' )
if os.path.exists(test_path):
pass
else:
PersonalIndex=test_path.find("Personal")
if(PersonalIndex==-1):
pass
else:
print("test_path= "+test_path+ ' not exists NG' )
pm.setAttr(item+'.ftn', "")
cmds.select( str(item), r=True )
cmds.delete( str(item) )
cmds.select(selected)
print("--------------remap_selected_texture----------END")
remap_selected_texture("GraceYong.images")モデルフォルダの自動認識と出力フォルダ引数のパターン
#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os
def remap_selected_texture(z_path):
# figure out what's selected
selected = pm.ls(sl=True)
# workspace_project_path
workspace_path = cmds.workspace(query=True, rootDirectory=True)
print("workspace_path= "+workspace_path)
# ma_file_path
ma_file_path=cmds.file(q=True, sn=True)
print("ma_file_path= "+ma_file_path)
ma_file_path_arr = ma_file_path.split('/')
ma_Folder=ma_file_path_arr[-2]
print("ma_Folder= "+ma_Folder)
for item in selected:
item_path = pm.getAttr(item+".ftn")
print("item_path= "+item_path)
item_path_arr = item_path.split('/')
texFolderName = item_path_arr[-2]
print("texFolderName= "+texFolderName)
fileName = item_path_arr[-1]
print("fileName= "+fileName)
fileName = fileName.replace('//', '')
fileName = fileName.replace('/', '')
print("fileName2= "+fileName)
#if ':' not in test_path:
if (1==1):
#print("fileName1= "+fileName)
#new_path = os.path.join(z_path, 'cartoon_room', second)
new_path = os.path.join(workspace_path,z_path, fileName)
new_path = new_path.replace('\\', '/')
new_path = new_path.replace('//', '/')
#relative_path = os.path.join(z_path, fileName)
#====================================================
relative_path = ma_Folder+"/"+z_path+"/"+fileName
#====================================================
print("relative_path= "+relative_path)
#relative_path = relative_path.replace('\\', '/')
abs_path = os.path.abspath(workspace_path+"/"+relative_path)
abs_path = abs_path.replace('//', '/')
print(" abs_path= "+abs_path)
new_path= abs_path
print(" new_path= "+new_path)
print("----------------------------end--------------------------")
if os.path.exists(new_path):
if(fileName==""):
print("item= "+str(item)+" is No fileName= "+ fileName)
cmds.select( str(item), r=True )
cmds.delete( str(item) )
else:
print("new_path= "+new_path+' is exists! OK')
pm.setAttr(item+'.ftn', new_path)
else:
print("new_path= "+new_path+ ' not exists NG' )
cmds.select(selected)
print("--------------remap_selected_texture----------END")
remap_selected_texture("texture")Scene押してシーンディレクトリ取得、押してWalkでテクスチャファイルをwalkで自動検索
#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os
def dir_walk(walkDir,TextureFileName):
resultPath=""
for curDir, dirs, files in os.walk(walkDir):
print('===================')
print("現在のディレクトリ: " + curDir)
curDir=curDir.replace("\\","/")
curDir_arr=curDir.split("/")
curDir_endDirName = curDir_arr[-1]
print("curDir_endDirName= "+curDir_endDirName)
if(curDir_endDirName==".mayaSwatches"):
print("処理をスキップします。。。")
pass
else:
print("内包するディレクトリ:" + str(dirs))
print("内包するファイル: " + str(files))
for fileName in files:
if(fileName==TextureFileName):
print("Hit : fileName= "+fileName+ "== TextureFileName= "+TextureFileName)
resultPath=curDir+"/"+fileName
print('===================')
return resultPath
def remap_selected_texture2(sceme_path):
# figure out what's selected
selected = cmds.ls(sl=True)
# workspace_project_path
workspace_path = cmds.workspace(query=True, rootDirectory=True)
print("workspace_path= "+workspace_path)
# ma_file_path
ma_file_path=cmds.file(q=True, sn=True)
print("ma_file_path= "+ma_file_path)
ma_file_path_arr = ma_file_path.split('/')
ma_Folder=ma_file_path_arr[-2]
print("ma_Folder= "+ma_Folder)
for item in selected:
item_path = cmds.getAttr(item+".ftn")
print("item_path= "+item_path)
item_path_arr = item_path.split('/')
texFolderName = item_path_arr[-2]
print("texFolderName= "+texFolderName)
fileName = item_path_arr[-1]
print("fileName= "+fileName)
fileName = fileName.replace('//', '')
fileName = fileName.replace('/', '')
print("fileName2= "+fileName)
fixTexturePath=dir_walk(sceme_path,fileName)
print("fixTexturePath= "+fixTexturePath)
#if ':' not in test_path:
if (1==1):
#print("fileName1= "+fileName)
#new_path = os.path.join(z_path, 'cartoon_room', second)
#new_path = os.path.join(workspace_path,z_path, fileName)
new_path = fixTexturePath
new_path = new_path.replace('\\', '/')
new_path = new_path.replace('//', '/')
#relative_path = os.path.join(z_path, fileName)
#====================================================
#relative_path = ma_Folder+"/"+z_path+"/"+fileName
#====================================================
#print("relative_path= "+relative_path)
#relative_path = relative_path.replace('\\', '/')
#abs_path = os.path.abspath(workspace_path+"/"+relative_path)
#abs_path = abs_path.replace('//', '/')
#print(" abs_path= "+abs_path)
#new_path= abs_path
print(" new_path= "+new_path)
print("----------------------------end--------------------------")
if os.path.exists(new_path):
if(fileName==""):
print("item= "+str(item)+" is No fileName= "+ fileName)
cmds.select( str(item), r=True )
cmds.delete( str(item) )
else:
print("new_path= "+new_path+' is exists! OK')
#cmds.setAttr(item+'.ftn', new_path)
cmds.setAttr(item+'.fileTextureName', new_path,type='string')
else:
print("new_path= "+new_path+ ' not exists NG' )
cmds.select(selected)
print("--------------remap_selected_texture----------END")
def remap_fileNode_texture(self):
text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
textField_outputFolder = cmds.textField(text_Field_id, q=True, text=True)
remap_selected_texture2(textField_outputFolder)
def get_scenePath():
scenefilePath = cmds.file(q=1, sceneName=1)
mayaPath,mayaFile = os.path.split(scenefilePath)
#mayaPath = mayaPath + "/Usd/"
#mayaPath = mayaPath + "/"
mayaPath=os.path.abspath(mayaPath)
mayaPath=mayaPath.replace('\\', '/')
print("mayaPath= "+mayaPath)
mayaPath_len=len(mayaPath)
last_str=mayaPath[mayaPath_len-1:]
print("mayaPath= "+mayaPath+ " last_str= "+last_str)
if(last_str=="/"):
pass
else:
mayaPath=mayaPath+"/"
return mayaPath
def btn_scene(self):
scenePath = get_scenePath()
#textureFolder= cmds.textField('textField_outputFolder', q=True, text=True)
#lastExportDirPath=scenePath+textureFolder+"/"
set_lastExportDirPath(scenePath)
def set_lastExportDirPath(lastExportDirPath):
selectList=cmds.ls(sl=True)
if(str(selectList)== "[]"):
print("なにも選択されていません。0 set_lastExportDirPath")
text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
cmds.textField(text_Field_id, edit=True, text=lastExportDirPath)
cmds.select(selectList)
def createWindow():
scenefilePath = cmds.file(q=1, sceneName=1)
USD_window = cmds.window("remap_selected_filenode_texture_path_Window", widthHeight=(400, 200))
USD_layout = cmds.columnLayout("USD_layout",adjustableColumn=True, parent=USD_window)
cmds.text (label="選択したハイパーシェードのテクスチャのパスを置換するツールです。", align='left', parent=USD_layout)
cmds.text (label="シーンのパス以下のフォルダーから検索して自動的に置換します。", align='left', parent=USD_layout)
cmds.separator(parent=USD_layout)
cmds.text (label="", align='left', parent=USD_layout)
cmds.text (label="1、Sceneを押してください。", align='left', parent=USD_layout)
cmds.button(label="Scene", command=btn_scene, parent=USD_layout)
text_field = cmds.textField("pathTxtFld", parent="USD_layout",text="")
#cmds.text (label="2、テクスチャフォルダ名を指定してください。", align='left', parent=USD_layout)
#text_field = cmds.textField("textField_outputFolder", parent="USD_layout",text="texture")
cmds.separator(parent=USD_layout)
cmds.text (label="", align='left', parent=USD_layout)
cmds.text (label="2、HyperShadeのテクスチャTabで(例:file1,file2,複数可)を選択して、ボタンを押してください。", align='left', parent=USD_layout)
#CheckBox_Absolute_Path_Bool = cmds.checkBox('CheckBox_Absolute_Path', q=True, value=True)
cmds.button(label="Remap FileNode Texture ", command=remap_fileNode_texture, parent=USD_layout)
#cmds.separator(parent=USD_layout)
#cmds.button(label="Add USD Referernce/Payload... ", command=add_prim_xform_for_stage_layer, parent=USD_layout)
cmds.showWindow(USD_window)
return None
def remap_selected_filenode_texture_path():
if cmds.window("remap_selected_filenode_texture_path_Window",exists=True):
cmds.deleteUI("remap_selected_filenode_texture_path_Window")
createWindow()
remap_selected_filenode_texture_path()fileノードのパスのバックスラッシュに対応しました。

#remap_selected_filenode_texture_path
import maya.cmds as cmds
import pymel.core as pm
import os
def dir_walk(walkDir,TextureFileName):
resultPath=""
for curDir, dirs, files in os.walk(walkDir):
print('===================')
print("現在のディレクトリ: " + curDir)
curDir=curDir.replace("\\","/")
curDir_arr=curDir.split("/")
curDir_endDirName = curDir_arr[-1]
print("curDir_endDirName= "+curDir_endDirName)
if(curDir_endDirName==".mayaSwatches"):
print("処理をスキップします。。。")
pass
else:
print("内包するディレクトリ:" + str(dirs))
print("内包するファイル: " + str(files))
for fileName in files:
if(fileName==TextureFileName):
print("Hit : fileName= "+fileName+ "== TextureFileName= "+TextureFileName)
resultPath=curDir+"/"+fileName
print('===================')
return resultPath
def remap_selected_texture2(sceme_path):
# figure out what's selected
selected = cmds.ls(sl=True)
# workspace_project_path
workspace_path = cmds.workspace(query=True, rootDirectory=True)
print("workspace_path= "+workspace_path)
# ma_file_path
ma_file_path=cmds.file(q=True, sn=True)
print("ma_file_path= "+ma_file_path)
ma_file_path_arr = ma_file_path.split('/')
ma_Folder=ma_file_path_arr[-2]
print("ma_Folder= "+ma_Folder)
for item in selected:
item_path = cmds.getAttr(item+".ftn")
print("item_path= "+item_path)
safe_path = item_path.replace("\\", "/")
print("safe_path= "+safe_path)
item_path_arr = safe_path.split('/')
print("item_path_arr[0]= "+item_path_arr[0])
print("item_path_arr[-1]= "+item_path_arr[-1])
print("item_path_arr[-2]= "+item_path_arr[-2])
texFolderName = item_path_arr[-2]
print("texFolderName= "+texFolderName)
fileName = item_path_arr[-1]
print("fileName= "+fileName)
fileName = fileName.replace('//', '')
fileName = fileName.replace('/', '')
print("fileName2= "+fileName)
fixTexturePath=dir_walk(sceme_path,fileName)
print("fixTexturePath= "+fixTexturePath)
#if ':' not in test_path:
if (1==1):
#print("fileName1= "+fileName)
#new_path = os.path.join(z_path, 'cartoon_room', second)
#new_path = os.path.join(workspace_path,z_path, fileName)
new_path = fixTexturePath
new_path = new_path.replace('\\', '/')
new_path = new_path.replace('//', '/')
#relative_path = os.path.join(z_path, fileName)
#====================================================
#relative_path = ma_Folder+"/"+z_path+"/"+fileName
#====================================================
#print("relative_path= "+relative_path)
#relative_path = relative_path.replace('\\', '/')
#abs_path = os.path.abspath(workspace_path+"/"+relative_path)
#abs_path = abs_path.replace('//', '/')
#print(" abs_path= "+abs_path)
#new_path= abs_path
print(" new_path= "+new_path)
print("----------------------------end--------------------------")
if os.path.exists(new_path):
if(fileName==""):
print("item= "+str(item)+" is No fileName= "+ fileName)
cmds.select( str(item), r=True )
cmds.delete( str(item) )
else:
print("new_path= "+new_path+' is exists! OK')
#cmds.setAttr(item+'.ftn', new_path)
cmds.setAttr(item+'.fileTextureName', new_path,type='string')
else:
print("new_path= "+new_path+ ' not exists NG' )
cmds.select(selected)
print("--------------remap_selected_texture----------END")
def remap_fileNode_texture(self):
text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
textField_outputFolder = cmds.textField(text_Field_id, q=True, text=True)
remap_selected_texture2(textField_outputFolder)
def get_scenePath():
scenefilePath = cmds.file(q=1, sceneName=1)
mayaPath,mayaFile = os.path.split(scenefilePath)
#mayaPath = mayaPath + "/Usd/"
#mayaPath = mayaPath + "/"
mayaPath=os.path.abspath(mayaPath)
mayaPath=mayaPath.replace('\\', '/')
print("mayaPath= "+mayaPath)
mayaPath_len=len(mayaPath)
last_str=mayaPath[mayaPath_len-1:]
print("mayaPath= "+mayaPath+ " last_str= "+last_str)
if(last_str=="/"):
pass
else:
mayaPath=mayaPath+"/"
return mayaPath
def btn_scene(self):
scenePath = get_scenePath()
#textureFolder= cmds.textField('textField_outputFolder', q=True, text=True)
#lastExportDirPath=scenePath+textureFolder+"/"
set_lastExportDirPath(scenePath)
def set_lastExportDirPath(lastExportDirPath):
selectList=cmds.ls(sl=True)
if(str(selectList)== "[]"):
print("なにも選択されていません。0 set_lastExportDirPath")
text_Field_id="remap_selected_filenode_texture_path_Window|USD_layout|pathTxtFld"
cmds.textField(text_Field_id, edit=True, text=lastExportDirPath)
cmds.select(selectList)
def createWindow():
scenefilePath = cmds.file(q=1, sceneName=1)
USD_window = cmds.window("remap_selected_filenode_texture_path_Window", widthHeight=(400, 200))
USD_layout = cmds.columnLayout("USD_layout",adjustableColumn=True, parent=USD_window)
cmds.text (label="選択したハイパーシェードのテクスチャのパスを置換するツールです。", align='left', parent=USD_layout)
cmds.text (label="シーンのパス以下のフォルダーから検索して自動的に置換します。", align='left', parent=USD_layout)
cmds.separator(parent=USD_layout)
cmds.text (label="", align='left', parent=USD_layout)
cmds.text (label="1、Sceneを押してください。", align='left', parent=USD_layout)
cmds.button(label="Scene", command=btn_scene, parent=USD_layout)
text_field = cmds.textField("pathTxtFld", parent="USD_layout",text="")
#cmds.text (label="2、テクスチャフォルダ名を指定してください。", align='left', parent=USD_layout)
#text_field = cmds.textField("textField_outputFolder", parent="USD_layout",text="texture")
cmds.separator(parent=USD_layout)
cmds.text (label="", align='left', parent=USD_layout)
cmds.text (label="2、HyperShadeのテクスチャTabで(例:file1,file2,複数可)を選択して、ボタンを押してください。", align='left', parent=USD_layout)
#CheckBox_Absolute_Path_Bool = cmds.checkBox('CheckBox_Absolute_Path', q=True, value=True)
cmds.button(label="Remap FileNode Texture ", command=remap_fileNode_texture, parent=USD_layout)
#cmds.separator(parent=USD_layout)
#cmds.button(label="Add USD Referernce/Payload... ", command=add_prim_xform_for_stage_layer, parent=USD_layout)
cmds.showWindow(USD_window)
return None
def remap_selected_filenode_texture_path():
if cmds.window("remap_selected_filenode_texture_path_Window",exists=True):
cmds.deleteUI("remap_selected_filenode_texture_path_Window")
createWindow()
remap_selected_filenode_texture_path()もうちょっと汎用的なテクスチャパスだけを検索置換できるツールが欲しくなり作った

# -*- coding: utf-8 -*-
"""
TexturePath Replace (generic) for Maya
シーン内の「ファイルパスを持つアトリビュート」を自動検出し、
テクスチャパスだけを検索・置換するツール。
- file / aiImage / psdFileTex / Redshift / V-Ray など、ノードタイプを問わず
usedAsFilename 属性を持つアトリビュートを自動で拾う
- 通常文字列 / 正規表現、大文字小文字の区別、 / と \\ の同一視
- 置換前にプレビュー(置換後のファイルが実在するかもチェック、UDIM等のトークン対応)
- 行ごとのチェックで適用対象を選べる、1回の Undo で元に戻せる
使い方 (Maya スクリプトエディタ / Python):
import texturepath_replace
texturepath_replace.show()
Maya 2020 - 2026 (PySide2 / PySide6) 対応。
"""
from __future__ import print_function
import glob
import os
import re
try:
from maya import cmds
from maya import OpenMayaUI as omui
except ImportError: # Maya 外 (テスト用)
cmds = None
omui = None
try:
from PySide6 import QtCore, QtGui, QtWidgets
from shiboken6 import wrapInstance
except ImportError:
try:
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
except ImportError:
QtCore = QtGui = QtWidgets = wrapInstance = None
TOOL_TITLE = "TexturePath Replace"
WINDOW_OBJECT_NAME = "texturePathReplaceWindow"
# classification に "texture" を含まないが、テクスチャパスを持つ既知ノード
EXTRA_TEXTURE_TYPES = {
"file",
"psdFileTex",
"movie",
"aiImage",
"RedshiftNormalMap",
"RedshiftSprite",
"RedshiftDomeLight",
"VRayLightDomeShape",
"VRayLightRectShape",
}
# テクスチャ系フィルタを外しても対象にしないノード (タイプ指定時は対象にできる)
EXCLUDE_TYPES = {
"colorManagementGlobals",
}
# glob 用にトークンをワイルドカードへ変換 (<UDIM>, <UVTILE>, <f>, u<U>_v<V>, ####, _MAPID_ ...)
_TOKEN_RE = re.compile(r"<[^<>]+>|_MAPID_|#+|\$\{?F\d*\}?")
# ---------------------------------------------------------------------------
# Core (Maya 非依存)
# ---------------------------------------------------------------------------
def build_replacer(search, replace, use_regex=False, case_sensitive=False,
ignore_slash=True):
"""path -> (new_path, count) を返す関数を作る。不正な正規表現は re.error を送出。"""
if not search:
return lambda path: (path, 0)
flags = 0 if case_sensitive else re.IGNORECASE
if use_regex:
pattern = re.compile(search, flags)
repl = replace # \1 や \g<name> を使える
else:
parts = []
for ch in search:
if ignore_slash and ch in "/\\":
parts.append(r"[\\/]")
else:
parts.append(re.escape(ch))
pattern = re.compile("".join(parts), flags)
# Windows パスのバックスラッシュが re のエスケープとして解釈されないよう関数で返す
repl = lambda m: replace # noqa: E731
def _apply(path):
return pattern.subn(repl, path)
return _apply
def normalize_slashes(path):
"""区切りを / に統一し、重複スラッシュをまとめる (UNC の先頭 // は保持)。"""
p = path.replace("\\", "/")
head = "//" if p.startswith("//") else ""
body = re.sub(r"/{2,}", "/", p[len(head):])
return head + body
def resolve_path(path, root_dir=None):
"""環境変数とプロジェクト相対パスを展開した絶対パスを返す。"""
p = os.path.expandvars(os.path.expanduser(path))
if root_dir and not os.path.isabs(p):
p = os.path.join(root_dir, p)
return os.path.normpath(p)
def path_exists(path, root_dir=None):
"""ファイルの存在確認。UDIM / フレーム番号などのトークンは glob で判定。"""
if not path:
return False
p = resolve_path(path, root_dir)
if _TOKEN_RE.search(p):
return bool(glob.glob(_TOKEN_RE.sub("*", glob.escape(p) if hasattr(glob, "escape") else p)))
return os.path.isfile(p)
# ---------------------------------------------------------------------------
# Maya scene access
# ---------------------------------------------------------------------------
class PathEntry(object):
__slots__ = ("node", "node_type", "attr", "path", "new_path", "count",
"locked", "exists_old", "exists_new", "enabled")
def __init__(self, node, node_type, attr, path, locked=False):
self.node = node
self.node_type = node_type
self.attr = attr
self.path = path
self.new_path = path
self.count = 0
self.locked = locked
self.exists_old = None
self.exists_new = None
self.enabled = True
@property
def plug(self):
return "{}.{}".format(self.node, self.attr)
@property
def changed(self):
return self.count > 0 and self.new_path != self.path
def _is_texture_type(node_type, cache):
if node_type in cache:
return cache[node_type]
result = node_type in EXTRA_TEXTURE_TYPES
if not result:
try:
for c in cmds.getClassification(node_type) or []:
if "texture" in c.lower():
result = True
break
except RuntimeError:
pass
cache[node_type] = result
return result
def _selection_nodes():
"""選択ノード + そのシェーダーネットワーク (ヒストリー) を返す。"""
sel = cmds.ls(sl=True, long=True) or []
if not sel:
return []
roots = set(sel)
shapes = cmds.listRelatives(sel, allDescendents=True, shapes=True,
fullPath=True) or []
roots.update(shapes)
sgs = cmds.listConnections(list(roots), type="shadingEngine") or []
roots.update(sgs)
hist = cmds.listHistory(list(roots)) or []
roots.update(hist)
return cmds.ls(list(roots), long=True) or []
def scan_scene(selection_only=False, texture_only=True, type_filter=""):
"""シーンからファイルパス属性を収集して PathEntry のリストを返す。"""
nodes = _selection_nodes() if selection_only else (cmds.ls(long=True) or [])
if not nodes:
return []
type_patterns = [t.strip() for t in re.split(r"[,\s]+", type_filter) if t.strip()]
type_re = None
if type_patterns:
type_re = re.compile("|".join(
"^" + re.escape(t).replace(r"\*", ".*") + "$" for t in type_patterns),
re.IGNORECASE)
# ノードタイプごとにグループ化
by_type = {}
for n in nodes:
try:
t = cmds.nodeType(n)
except RuntimeError:
continue
by_type.setdefault(t, []).append(n)
tex_cache = {}
entries = []
for t in sorted(by_type):
if type_re and not type_re.match(t):
continue
if not type_re:
if t in EXCLUDE_TYPES:
continue
if texture_only and not _is_texture_type(t, tex_cache):
continue
type_nodes = by_type[t]
# 静的属性はタイプ単位で同じなのでサンプル1つで判定 → 無ければスキップ
sample_attrs = cmds.listAttr(type_nodes[0], usedAsFilename=True) or []
if not sample_attrs:
continue
for n in type_nodes:
attrs = cmds.listAttr(n, usedAsFilename=True) or []
short = cmds.ls(n)[0] if cmds.ls(n) else n
for a in attrs:
plug = "{}.{}".format(n, a)
try:
val = cmds.getAttr(plug)
except (RuntimeError, ValueError):
continue
if not isinstance(val, str):
try:
val = val if isinstance(val, unicode) else None # noqa: F821 (py2)
except NameError:
val = None
if not val:
continue
try:
locked = bool(cmds.getAttr(plug, lock=True))
except RuntimeError:
locked = False
e = PathEntry(short, t, a, val, locked)
e.node = n # long name で保持
entries.append(e)
return entries
def apply_entries(entries):
"""チェックされた変更を適用。(成功数, 失敗リスト) を返す。1 回の Undo で戻せる。"""
ok, failed = 0, []
cmds.undoInfo(openChunk=True, chunkName="TexturePathReplace")
try:
for e in entries:
if not (e.enabled and e.changed):
continue
if e.locked:
failed.append((e.plug, "locked"))
continue
try:
cmds.setAttr(e.plug, e.new_path, type="string")
ok += 1
except RuntimeError as ex:
failed.append((e.plug, str(ex).strip()))
finally:
cmds.undoInfo(closeChunk=True)
return ok, failed
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
def _maya_main_window():
if omui is None or wrapInstance is None:
return None
ptr = omui.MQtUtil.mainWindow()
if ptr is None:
return None
try:
return wrapInstance(int(ptr), QtWidgets.QWidget)
except TypeError:
return wrapInstance(long(ptr), QtWidgets.QWidget) # noqa: F821 (py2)
if QtWidgets is not None:
COL_CHECK, COL_NODE, COL_TYPE, COL_ATTR, COL_OLD, COL_NEW, COL_EXISTS = range(7)
HEADERS = ["", "Node", "Type", "Attr", "現在のパス", "置換後のパス", "存在"]
class TexturePathReplaceWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super(TexturePathReplaceWindow, self).__init__(parent or _maya_main_window())
self.setObjectName(WINDOW_OBJECT_NAME)
self.setWindowTitle(TOOL_TITLE)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.resize(1100, 600)
self.entries = []
self._root_dir = ""
self._error = ""
self._preview_timer = QtCore.QTimer(self)
self._preview_timer.setSingleShot(True)
self._preview_timer.setInterval(200)
self._preview_timer.timeout.connect(self.update_preview)
self._build_ui()
self.rescan()
# ---- build -------------------------------------------------------
def _build_ui(self):
main = QtWidgets.QVBoxLayout(self)
# 検索 / 置換
form = QtWidgets.QGridLayout()
self.search_edit = QtWidgets.QLineEdit()
self.search_edit.setPlaceholderText("例) D:/old_project/sourceimages (正規表現: ^.*/textures/)")
self.replace_edit = QtWidgets.QLineEdit()
self.replace_edit.setPlaceholderText("例) ${PROJECT_ROOT}/sourceimages (正規表現: \\1 などのグループ参照可)")
swap_btn = QtWidgets.QPushButton(u"\u21c5")
swap_btn.setToolTip("検索と置換を入れ替え")
swap_btn.setFixedWidth(28)
swap_btn.clicked.connect(self._swap_fields)
form.addWidget(QtWidgets.QLabel("検索:"), 0, 0)
form.addWidget(self.search_edit, 0, 1)
form.addWidget(swap_btn, 0, 2, 2, 1)
form.addWidget(QtWidgets.QLabel("置換:"), 1, 0)
form.addWidget(self.replace_edit, 1, 1)
main.addLayout(form)
# オプション
opt = QtWidgets.QHBoxLayout()
self.regex_cb = QtWidgets.QCheckBox("正規表現")
self.case_cb = QtWidgets.QCheckBox("大文字小文字を区別")
self.slash_cb = QtWidgets.QCheckBox("/ と \\ を同一視")
self.slash_cb.setChecked(True)
self.slash_cb.setToolTip("通常検索時、/ と \\ のどちらでもマッチします")
self.normalize_cb = QtWidgets.QCheckBox("置換結果の区切りを / に統一")
self.normalize_cb.setChecked(True)
for w in (self.regex_cb, self.case_cb, self.slash_cb, self.normalize_cb):
opt.addWidget(w)
w.toggled.connect(self._schedule_preview)
opt.addStretch()
main.addLayout(opt)
# スキャン範囲
scope = QtWidgets.QHBoxLayout()
self.scope_all = QtWidgets.QRadioButton("シーン全体")
self.scope_sel = QtWidgets.QRadioButton("選択 (シェーダーネットワーク含む)")
self.scope_all.setChecked(True)
self.texture_only_cb = QtWidgets.QCheckBox("テクスチャ系ノードのみ")
self.texture_only_cb.setChecked(True)
self.texture_only_cb.setToolTip(
"OFF にすると gpuCache / Alembic / StandIn / imagePlane などのパスも対象になります")
self.type_filter_edit = QtWidgets.QLineEdit()
self.type_filter_edit.setPlaceholderText("ノードタイプ指定 (例: file, aiImage, Redshift*)")
self.type_filter_edit.setMaximumWidth(260)
rescan_btn = QtWidgets.QPushButton("再スキャン")
rescan_btn.clicked.connect(self.rescan)
for w in (self.scope_all, self.scope_sel, self.texture_only_cb):
scope.addWidget(w)
scope.addWidget(self.type_filter_edit)
scope.addStretch()
scope.addWidget(rescan_btn)
self.scope_all.toggled.connect(self.rescan)
self.texture_only_cb.toggled.connect(self.rescan)
self.type_filter_edit.returnPressed.connect(self.rescan)
main.addLayout(scope)
# 表示フィルタ
view = QtWidgets.QHBoxLayout()
self.changed_only_cb = QtWidgets.QCheckBox("変更される行のみ表示")
self.missing_only_cb = QtWidgets.QCheckBox("ファイルが見つからない行のみ表示")
self.changed_only_cb.toggled.connect(self._refresh_table)
self.missing_only_cb.toggled.connect(self._refresh_table)
check_all = QtWidgets.QPushButton("全チェック")
uncheck_all = QtWidgets.QPushButton("全解除")
check_all.clicked.connect(lambda: self._set_all_checked(True))
uncheck_all.clicked.connect(lambda: self._set_all_checked(False))
view.addWidget(self.changed_only_cb)
view.addWidget(self.missing_only_cb)
view.addStretch()
view.addWidget(check_all)
view.addWidget(uncheck_all)
main.addLayout(view)
# テーブル
self.table = QtWidgets.QTableWidget(0, len(HEADERS))
self.table.setHorizontalHeaderLabels(HEADERS)
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.table.setAlternatingRowColors(True)
self.table.setSortingEnabled(False)
self.table.verticalHeader().setVisible(False)
hdr = self.table.horizontalHeader()
hdr.setSectionResizeMode(QtWidgets.QHeaderView.Interactive)
hdr.setSectionResizeMode(COL_CHECK, QtWidgets.QHeaderView.ResizeToContents)
hdr.setSectionResizeMode(COL_EXISTS, QtWidgets.QHeaderView.ResizeToContents)
hdr.setStretchLastSection(False)
hdr.setSectionResizeMode(COL_OLD, QtWidgets.QHeaderView.Stretch)
hdr.setSectionResizeMode(COL_NEW, QtWidgets.QHeaderView.Stretch)
for col, w in ((COL_NODE, 130), (COL_TYPE, 80), (COL_ATTR, 110)):
self.table.setColumnWidth(col, w)
self.table.itemChanged.connect(self._on_item_changed)
self.table.cellDoubleClicked.connect(self._on_double_click)
self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._context_menu)
main.addWidget(self.table, 1)
# 下部
bottom = QtWidgets.QHBoxLayout()
self.status_label = QtWidgets.QLabel("")
apply_btn = QtWidgets.QPushButton("置換を実行")
apply_btn.setMinimumWidth(140)
apply_btn.setDefault(False)
apply_btn.setAutoDefault(False)
apply_btn.clicked.connect(self.apply)
bottom.addWidget(self.status_label, 1)
bottom.addWidget(apply_btn)
main.addLayout(bottom)
hint = QtWidgets.QLabel(
"ヒント: パス列をダブルクリックで検索欄へコピー / Node 列をダブルクリックでノード選択 / 右クリックでメニュー")
hint.setStyleSheet("color: gray;")
main.addWidget(hint)
self.search_edit.textChanged.connect(self._schedule_preview)
self.replace_edit.textChanged.connect(self._schedule_preview)
# ---- logic -------------------------------------------------------
def rescan(self, *args):
if cmds is None:
return
try:
self._root_dir = cmds.workspace(q=True, rootDirectory=True) or ""
except RuntimeError:
self._root_dir = ""
self.entries = scan_scene(
selection_only=self.scope_sel.isChecked(),
texture_only=self.texture_only_cb.isChecked(),
type_filter=self.type_filter_edit.text(),
)
for e in self.entries:
e.exists_old = path_exists(e.path, self._root_dir)
self.update_preview()
def _schedule_preview(self, *args):
self._preview_timer.start()
def update_preview(self):
self.search_edit.setStyleSheet("")
self._error = ""
try:
replacer = build_replacer(
self.search_edit.text(),
self.replace_edit.text(),
use_regex=self.regex_cb.isChecked(),
case_sensitive=self.case_cb.isChecked(),
ignore_slash=self.slash_cb.isChecked(),
)
except re.error as ex:
self.search_edit.setStyleSheet("background-color: #6b2b2b;")
self._error = u"正規表現エラー: {}".format(ex)
replacer = lambda p: (p, 0) # noqa: E731
normalize = self.normalize_cb.isChecked()
for e in self.entries:
try:
new, count = replacer(e.path)
except (re.error, IndexError) as ex: # 不正なグループ参照など
self._error = u"置換文字列エラー: {}".format(ex)
new, count = e.path, 0
if count and normalize:
new = normalize_slashes(new)
e.new_path, e.count = new, count
e.exists_new = path_exists(new, self._root_dir) if e.changed else e.exists_old
self._refresh_table()
def _visible_entries(self):
result = []
for e in self.entries:
if self.changed_only_cb.isChecked() and not e.changed:
continue
if self.missing_only_cb.isChecked() and e.exists_new:
continue
result.append(e)
return result
def _refresh_table(self, *args):
visible = self._visible_entries()
self.table.blockSignals(True)
self.table.setRowCount(len(visible))
changed_bg = QtGui.QColor(60, 90, 60)
missing_fg = QtGui.QColor(230, 110, 110)
ok_fg = QtGui.QColor(120, 200, 120)
for row, e in enumerate(visible):
chk = QtWidgets.QTableWidgetItem()
chk.setFlags(QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
chk.setCheckState(QtCore.Qt.Checked if e.enabled else QtCore.Qt.Unchecked)
chk.setData(QtCore.Qt.UserRole, self.entries.index(e))
self.table.setItem(row, COL_CHECK, chk)
node_item = QtWidgets.QTableWidgetItem(e.node.split("|")[-1])
node_item.setToolTip(e.node)
if e.locked:
node_item.setText(node_item.text() + " (locked)")
self.table.setItem(row, COL_NODE, node_item)
self.table.setItem(row, COL_TYPE, QtWidgets.QTableWidgetItem(e.node_type))
self.table.setItem(row, COL_ATTR, QtWidgets.QTableWidgetItem(e.attr))
old_item = QtWidgets.QTableWidgetItem(e.path)
old_item.setToolTip(e.path)
if not e.exists_old:
old_item.setForeground(missing_fg)
self.table.setItem(row, COL_OLD, old_item)
new_item = QtWidgets.QTableWidgetItem(e.new_path if e.changed else "")
new_item.setToolTip(e.new_path)
if e.changed:
new_item.setBackground(changed_bg)
self.table.setItem(row, COL_NEW, new_item)
ex_item = QtWidgets.QTableWidgetItem(u"\u2714" if e.exists_new else u"\u2718")
ex_item.setTextAlignment(QtCore.Qt.AlignCenter)
ex_item.setForeground(ok_fg if e.exists_new else missing_fg)
self.table.setItem(row, COL_EXISTS, ex_item)
self.table.blockSignals(False)
self._update_status()
def _update_status(self):
total = len(self.entries)
changed = sum(1 for e in self.entries if e.changed)
targets = sum(1 for e in self.entries if e.changed and e.enabled)
missing = sum(1 for e in self.entries if e.changed and e.enabled and not e.exists_new)
text = u"パス {} 件 / マッチ {} 件 / 適用対象 {} 件".format(total, changed, targets)
if missing:
text += u" (うち置換後にファイルが見つからない: {} 件)".format(missing)
if self._error:
text = u"<span style='color:#e66e6e'>{}</span> {}".format(self._error, text)
self.status_label.setText(text)
def _entry_at(self, row):
item = self.table.item(row, COL_CHECK)
if item is None:
return None
return self.entries[item.data(QtCore.Qt.UserRole)]
def _on_item_changed(self, item):
if item.column() != COL_CHECK:
return
e = self.entries[item.data(QtCore.Qt.UserRole)]
e.enabled = item.checkState() == QtCore.Qt.Checked
self._update_status()
def _set_all_checked(self, state):
for e in self._visible_entries():
e.enabled = state
self._refresh_table()
def _on_double_click(self, row, col):
e = self._entry_at(row)
if e is None:
return
if col in (COL_OLD, COL_NEW):
self.search_edit.setText(e.path if col == COL_OLD else e.new_path)
self.search_edit.setFocus()
else:
cmds.select(e.node, replace=True)
def _context_menu(self, pos):
rows = sorted({i.row() for i in self.table.selectedIndexes()})
entries = [x for x in (self._entry_at(r) for r in rows) if x]
if not entries:
return
menu = QtWidgets.QMenu(self)
menu.addAction("ノードを選択", lambda: cmds.select([x.node for x in entries], replace=True))
menu.addAction("チェックを入れる", lambda: self._set_checked(entries, True))
menu.addAction("チェックを外す", lambda: self._set_checked(entries, False))
menu.addSeparator()
menu.addAction("現在のパスをコピー",
lambda: QtWidgets.QApplication.clipboard().setText(
"\n".join(x.path for x in entries)))
menu.addAction("フォルダ部分を検索欄へ",
lambda: self.search_edit.setText(
os.path.dirname(entries[0].path.replace("\\", "/"))))
menu.exec_(self.table.viewport().mapToGlobal(pos))
def _set_checked(self, entries, state):
for e in entries:
e.enabled = state
self._refresh_table()
def _swap_fields(self):
s, r = self.search_edit.text(), self.replace_edit.text()
self.search_edit.setText(r)
self.replace_edit.setText(s)
def apply(self):
targets = [e for e in self.entries if e.enabled and e.changed]
if not targets:
QtWidgets.QMessageBox.information(self, TOOL_TITLE, "置換対象がありません。")
return
missing = sum(1 for e in targets if not e.exists_new)
msg = u"{} 件のパスを置換します。".format(len(targets))
if missing:
msg += u"\n\n置換後にファイルが見つからないパスが {} 件あります。".format(missing)
msg += u"\n\n実行しますか? (Ctrl+Z で一括 Undo 可能)"
if QtWidgets.QMessageBox.question(self, TOOL_TITLE, msg) != QtWidgets.QMessageBox.Yes:
return
ok, failed = apply_entries(targets)
print(u"[{}] {} 件置換しました。".format(TOOL_TITLE, ok))
for plug, reason in failed:
print(u"[{}] 失敗: {} ({})".format(TOOL_TITLE, plug, reason))
if failed:
QtWidgets.QMessageBox.warning(
self, TOOL_TITLE,
u"{} 件置換、{} 件失敗しました。詳細はスクリプトエディタを参照してください。".format(
ok, len(failed)))
self.rescan()
self.status_label.setText(u"{} 件置換しました。 ".format(ok) + self.status_label.text())
def show():
"""ウィンドウを表示 (既に開いていれば閉じて開き直す)。"""
if QtWidgets is None or cmds is None:
raise RuntimeError("Maya (PySide2/PySide6) 上で実行してください。")
app = QtWidgets.QApplication.instance()
for w in app.topLevelWidgets():
if w.objectName() == WINDOW_OBJECT_NAME:
w.close()
w.deleteLater()
win = TexturePathReplaceWindow()
win.show()
return win
if __name__ == "__main__" and cmds is not None:
show()










































































