# -*- coding: utf-8 -*-
"""
blendshape_io.py
Maya の blendShape ノードを「外部データ (JSON)」として書き出し / 読み戻す。
ターゲットメッシュ本体は不要。blendShape ノードのプラグ
inputTarget[g].inputTargetGroup[w].inputTargetItem[i].inputPointsTarget
inputTarget[g].inputTargetGroup[w].inputTargetItem[i].inputComponentsTarget
に入っている「疎な delta(動いた頂点だけの相対オフセット, ローカル空間)」を
そのまま JSON 化する。これは Maya が内部で持っている表現そのものなので、
往復(export→import)で完全一致する。
- alias(表情名)、envelope、origin、inbetween(中間ターゲット)も保存
- baseWeights / targetWeights(ペイントウェイト)は非デフォルト分だけ疎に保存
- import 時はターゲットメッシュに新しい blendShape ノードを作り、プラグへ直接流し込む
→ CLO_*_bs のような「焼き込み済み」ノードを別シーン / 別リグへ移植できる
使い方 (Maya Script Editor / Python):
# ★再現性のため import + reload ではなく exec 推奨(reload は編集中だと壊れる)
p = r"D:/Github/ENRAI_CLO3D_UnityGit/claudeMayaBlendShapeCode/blendshape_io.py"
g = {}; exec(open(p, encoding="utf-8").read(), g); bio = type("m", (), g)
# --- 書き出し(元シーンで)---
bio.export_all(r"D:/tmp/bs_export") # シーン内の全 blendShape を個別 JSON に
# --- 読み戻し(移植先シーンで): フォルダ一括・冪等・結果表つき ---
bio.import_dir(r"D:/tmp/bs_export",
root="Tsubame_v1_0_VRChat4_miya7_openFinger3|geo_gp|head_gp",
suffix="_miya73")
# root 直下の同名メッシュへ全 JSON を import。既存は置換。
# メッシュが無い JSON は SKIP(エラーで止まらない)。最後に結果表。
# --- 単発でやりたいとき ---
bio.export_node("CLO_Face_Mesh_bs", r"D:/tmp/CLO_Face_Mesh_bs.json")
bio.import_node(r"D:/tmp/CLO_Face_Mesh_bs.json",
mesh="...|head_gp|Face_Mesh", replace=True)
# --- 検証(ライブノード vs JSON の delta 差分)---
bio.verify(r"D:/tmp/CLO_Face_Mesh_bs.json", "CLO_Face_Mesh_bs")
# --- import の掃除(import_node で作ったノードだけ消す)---
bio.list_imported() # 一覧
bio.remove_imported(dry_run=True) # 消える対象を確認
bio.remove_imported() # 全部削除
bio.remove_imported(mesh="...|Face_Mesh") # そのメッシュの分だけ
bio.remove_imported(name="CLO_*_miya7") # 名前で絞り込み
bio.remove_by_name("CLO_Face_Mesh_bs") # 印の無い旧 import 分を強制削除
コマンドポート経由 (4700 / mel) の場合:
python("import blendshape_io as bio; bio.export_all(r'D:/tmp/bs_export')")
進捗は out ディレクトリ(export)またはシーン同階層(import)の
blendshape_io_log.txt に flush 都度書かれるので tail で追える。
FORMAT: "maya-blendshape-io/1"
"""
import os
import json
import time
import fnmatch
import maya.cmds as cmds
import maya.api.OpenMaya as om
FORMAT = "maya-blendshape-io/1"
# import_node が作ったノードに付ける印(掃除で使う)
_TAG = "blendShapeIoImported"
# blendShape プラグの短縮名
_IT = "inputTarget"
_ITG = "inputTargetGroup"
_ITI = "inputTargetItem"
_IPT = "inputPointsTarget"
_ICT = "inputComponentsTarget"
# --------------------------------------------------------------------------- log
class _Logger(object):
def __init__(self, path):
self.path = path
self.t0 = time.time()
try:
self.f = open(path, "w", encoding="utf-8")
except Exception:
self.f = None
self.write(u"blendshape_io 開始 (%s)" % FORMAT)
def write(self, msg):
line = u"[%6.1fs] %s" % (time.time() - self.t0, msg)
print(line)
if self.f:
try:
self.f.write(line + u"\n")
self.f.flush()
os.fsync(self.f.fileno())
except Exception:
pass
def close(self, ok=True):
self.write(u"完了" if ok else u"中断(エラー)")
if self.f:
try:
self.f.close()
except Exception:
pass
def _log_path(hint_dir=None):
d = hint_dir
if not d:
sp = cmds.file(q=True, sn=True)
d = os.path.dirname(sp) if sp else cmds.workspace(q=True, directory=True)
return os.path.join(d, "blendshape_io_log.txt")
# ----------------------------------------------------------------------- helpers
def _resolve(name):
hits = cmds.ls(name, long=True) or []
if not hits:
raise ValueError(u"見つかりません: %s" % name)
return sorted(hits, key=lambda p: ("OLD" in p, p.count("|")))[0]
def _shape_of(node):
"""transform ならデフォームされる可視シェイプ、shape ならそのまま返す。"""
node = _resolve(node)
if cmds.nodeType(node) == "mesh":
return node
shapes = cmds.listRelatives(node, s=True, f=True, ni=False) or []
vis = [s for s in shapes if not cmds.getAttr(s + ".intermediateObject")]
if not vis:
raise ValueError(u"可視メッシュシェイプが見つかりません: %s" % node)
return vis[0]
def _num_vertices(shape):
try:
return cmds.polyEvaluate(shape, v=True)
except Exception:
return None
def _weight_indices(bs):
return sorted(cmds.getAttr(bs + ".weight", mi=True) or [])
def _geo_indices(bs):
idx = cmds.getAttr("%s.%s" % (bs, _IT), mi=True)
return sorted(idx) if idx else [0]
def _alias_of(bs, wi):
a = cmds.aliasAttr("%s.weight[%d]" % (bs, wi), q=True)
return a if a else None
def _item_indices(bs, gi, wi):
plug = "%s.%s[%d].%s[%d].%s" % (bs, _IT, gi, _ITG, wi, _ITI)
idx = cmds.getAttr(plug, mi=True)
return sorted(idx) if idx else []
def _item_plug(bs, gi, wi, ii):
return "%s.%s[%d].%s[%d].%s[%d]" % (bs, _IT, gi, _ITG, wi, _ITI, ii)
def _iti_to_weight(ii):
# 6000 -> 1.0, 5000 -> 0.0, 5750 -> 0.75, 4000 -> -1.0
return (ii - 5000) / 1000.0
def _weight_to_iti(w):
return int(round(w * 1000.0)) + 5000
def _read_target_item(bs, gi, wi, ii):
plug = _item_plug(bs, gi, wi, ii)
pts = cmds.getAttr(plug + "." + _IPT) or []
comps = cmds.getAttr(plug + "." + _ICT) or []
pts = [[round(float(p[0]), 7), round(float(p[1]), 7), round(float(p[2]), 7)]
for p in pts]
comps = [str(c) for c in comps]
return comps, pts
def _read_paint_weights(bs, gi, wi=None):
"""baseWeights (wi=None) / targetWeights を疎な {index: value} で返す。
デフォルト (1.0) の要素は落とす。全てデフォルトなら None。"""
if wi is None:
plug = "%s.%s[%d].baseWeights" % (bs, _IT, gi)
else:
plug = "%s.%s[%d].%s[%d].targetWeights" % (bs, _IT, gi, _ITG, wi)
idx = cmds.getAttr(plug, mi=True)
if not idx:
return None
out = {}
for i in idx:
try:
v = cmds.getAttr("%s[%d]" % (plug, i))
except Exception:
continue
if abs(v - 1.0) > 1e-6:
out[str(i)] = round(float(v), 6)
return out or None
def _inbetween_name(bs, wi, ii):
"""named inbetween があれば名前を返す(無ければ None)。"""
for attr in ("%s.inbetweenInfoGroup[%d].inbetweenInfo[%d].inbetweenTargetName" % (bs, wi, ii),):
try:
if cmds.objExists(bs) and cmds.attributeQuery("inbetweenInfoGroup", node=bs, exists=True):
v = cmds.getAttr(attr)
if v:
return v
except Exception:
pass
return None
# ----------------------------------------------------------------------- export
def export_node(node, out_path, log=None, include_paint_weights=True):
own_log = log is None
if own_log:
log = _Logger(_log_path(os.path.dirname(os.path.abspath(out_path))))
try:
bs = _resolve(node)
if cmds.nodeType(bs) != "blendShape":
# transform / mesh を渡された場合、履歴から blendShape を拾う
hist = cmds.listHistory(bs) or []
cand = [h for h in hist if cmds.nodeType(h) == "blendShape"]
if not cand:
raise ValueError(u"blendShape ノードが見つかりません: %s" % node)
bs = cand[0]
log.write(u"export: %s" % bs)
geo = cmds.blendShape(bs, q=True, geometry=True) or []
geo_long = [_resolve(g) for g in geo]
data = {
"format": FORMAT,
"node": bs.split("|")[-1],
"envelope": round(cmds.getAttr(bs + ".envelope"), 6),
"origin": cmds.getAttr(bs + ".origin", asString=True),
"supportNegativeWeights": _safe_getattr(bs, "supportNegativeWeights"),
"maya": cmds.about(version=True),
"scene": cmds.file(q=True, sn=True) or "",
"geometry": [],
}
for gi in _geo_indices(bs):
shp = geo_long[gi] if gi < len(geo_long) else None
tr = None
if shp:
p = cmds.listRelatives(shp, p=True, f=True)
tr = p[0] if p else None
nv = _num_vertices(shp) if shp else None
g = {
"index": gi,
"shape": shp.split("|")[-1] if shp else None,
"transform": tr.split("|")[-1] if tr else None,
"transformPath": tr if tr else None,
"numVertices": nv,
"baseWeights": _read_paint_weights(bs, gi) if include_paint_weights else None,
"targets": [],
}
for wi in _weight_indices(bs):
items = []
for ii in _item_indices(bs, gi, wi):
comps, pts = _read_target_item(bs, gi, wi, ii)
if not comps and not pts:
continue
items.append({
"itemIndex": ii,
"inbetweenWeight": round(_iti_to_weight(ii), 4),
"inbetweenName": _inbetween_name(bs, wi, ii),
"components": comps,
"points": pts,
})
if not items:
continue
g["targets"].append({
"weightIndex": wi,
"alias": _alias_of(bs, wi),
"weight": round(_safe_getattr(bs, "weight[%d]" % wi, 0.0), 6),
"targetWeights": _read_paint_weights(bs, gi, wi) if include_paint_weights else None,
"items": items,
})
log.write(u" geo[%d] %s : %d ターゲット, %s 頂点"
% (gi, g["shape"], len(g["targets"]), nv))
data["geometry"].append(g)
_write_json(out_path, data)
n_t = sum(len(g["targets"]) for g in data["geometry"])
sz = os.path.getsize(out_path)
log.write(u"書き出し完了: %s (%d ターゲット, %.1f KB)"
% (out_path, n_t, sz / 1024.0))
return out_path
finally:
if own_log:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
def export_all(out_dir, include_paint_weights=True, include_referenced=False):
if not os.path.isdir(out_dir):
os.makedirs(out_dir)
log = _Logger(_log_path(out_dir))
try:
nodes = cmds.ls(type="blendShape") or []
if not include_referenced:
keep = []
for bs in nodes:
try:
ref = cmds.referenceQuery(bs, isNodeReferenced=True)
except Exception:
ref = False
if ref:
log.write(u" スキップ(参照ノード): %s" % bs)
else:
keep.append(bs)
nodes = keep
log.write(u"blendShape ノード %d 件: %s" % (len(nodes), nodes))
paths = []
for bs in nodes:
safe = bs.split("|")[-1].replace(":", "_")
out = os.path.join(out_dir, safe + ".json")
try:
export_node(bs, out, log=log,
include_paint_weights=include_paint_weights)
paths.append(out)
except Exception as e:
log.write(u" 失敗 %s: %s" % (bs, e))
log.write(u"全 %d 件書き出し -> %s" % (len(paths), out_dir))
return paths
finally:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
# ----------------------------------------------------------------------- import
def import_node(in_path, mesh=None, node_name=None, front_of_chain=True,
geo_index=0, apply_paint_weights=True, replace=False):
"""
JSON から blendShape ノードを再構築して mesh に適用する。
mesh : ターゲット transform / shape。省略時は JSON の transform 名で検索。
node_name : 生成する blendShape 名。省略時は JSON の "node"。
front_of_chain : True で skinCluster 等より上流に挿入(CLO_*_bs と同じ)。
geo_index : JSON 内のどの geometry ブロックを使うか(通常 0)。
replace : 同名ノードが既にあれば削除してから作り直す。
"""
data = _read_json(in_path)
if data.get("format") != FORMAT:
raise ValueError(u"未知のフォーマット: %s" % data.get("format"))
geos = data.get("geometry") or []
g = next((x for x in geos if x.get("index") == geo_index), geos[0] if geos else None)
if g is None:
raise ValueError(u"geometry ブロックがありません")
node_name = node_name or data.get("node") or "imported_bs"
log = _Logger(_log_path())
try:
# --- ターゲットメッシュ解決 ---
if mesh is None:
for key in ("transformPath", "transform", "shape"):
nm = g.get(key)
if nm and (cmds.ls(nm) or cmds.ls("*|" + nm)):
mesh = nm
break
if mesh is None:
raise ValueError(
u"mesh 未指定で、JSON の '%s' も見つかりません。mesh= で指定してください。"
% g.get("transform"))
shp = _shape_of(mesh)
tr = cmds.listRelatives(shp, p=True, f=True)[0]
log.write(u"import 先: %s" % tr)
nv_json = g.get("numVertices")
nv_live = _num_vertices(shp)
if nv_json and nv_live and nv_json != nv_live:
raise ValueError(u"頂点数不一致: JSON %d ≠ メッシュ %d(トポロジ違い)"
% (nv_json, nv_live))
log.write(u"頂点数 OK: %s" % nv_live)
# --- 既存ノード処理 ---
if cmds.objExists(node_name):
if replace:
log.write(u"既存 %s を削除" % node_name)
cmds.delete(node_name)
else:
raise ValueError(u"%s は既に存在します。replace=True か node_name= を指定。"
% node_name)
# --- blendShape 生成(0 ターゲット)---
kw = {"name": node_name, "frontOfChain": front_of_chain}
origin = data.get("origin")
if origin in ("local", "world"):
kw["origin"] = origin
bs = cmds.blendShape(tr, **kw)[0]
_tag_imported(bs, in_path, tr)
log.write(u"blendShape 生成: %s (frontOfChain=%s, origin=%s)"
% (bs, front_of_chain, origin))
gi = geo_index
targets = g.get("targets") or []
for n, t in enumerate(targets):
wi = t["weightIndex"]
grp = "%s.%s[%d].%s[%d]" % (bs, _IT, gi, _ITG, wi)
for item in t.get("items") or []:
ii = item["itemIndex"]
comps = item.get("components") or []
pts = [tuple(map(float, p)) for p in (item.get("points") or [])]
plug = "%s.%s[%d]" % (grp, _ITI, ii)
if comps:
cmds.setAttr(plug + "." + _ICT, len(comps), *comps,
type="componentList")
if pts:
cmds.setAttr(plug + "." + _IPT, len(pts), *pts,
type="pointArray")
# weight プラグ + alias
wplug = "%s.weight[%d]" % (bs, wi)
try:
cmds.setAttr(wplug, float(t.get("weight") or 0.0))
except Exception:
pass
alias = t.get("alias")
if alias:
try:
cmds.aliasAttr(alias, wplug)
except Exception as e:
log.write(u" alias 設定失敗 %s: %s" % (alias, e))
# targetWeights(ペイント)
if apply_paint_weights and t.get("targetWeights"):
for si, sv in t["targetWeights"].items():
try:
cmds.setAttr("%s.targetWeights[%s]" % (grp, si), float(sv))
except Exception:
pass
if (n + 1) % 20 == 0:
log.write(u" %d / %d ターゲット" % (n + 1, len(targets)))
# baseWeights(ペイント)
if apply_paint_weights and g.get("baseWeights"):
bplug = "%s.%s[%d].baseWeights" % (bs, _IT, gi)
for si, sv in g["baseWeights"].items():
try:
cmds.setAttr("%s[%s]" % (bplug, si), float(sv))
except Exception:
pass
if "envelope" in data:
cmds.setAttr(bs + ".envelope", float(data["envelope"]))
got = cmds.listAttr(bs + ".weight", multi=True) or []
log.write(u"完了: %s ターゲット %d 件 (alias: %s ...)"
% (bs, len(got), ", ".join(got[:6])))
return bs
finally:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
# ----------------------------------------------------------------------- verify
def verify(in_path, node, tol=1e-5, geo_index=0):
"""JSON の delta とライブ blendShape ノードの delta を突き合わせる。"""
data = _read_json(in_path)
bs = _resolve(node)
g = next((x for x in (data.get("geometry") or []) if x.get("index") == geo_index), None)
if g is None:
print(u"geometry[%d] が JSON にありません" % geo_index)
return False
ok = True
for t in g.get("targets") or []:
wi = t["weightIndex"]
for item in t.get("items") or []:
ii = item["itemIndex"]
comps, pts = _read_target_item(bs, geo_index, wi, ii)
jp = item.get("points") or []
jc = item.get("components") or []
if comps != jc:
print(u"[%s iti=%d] components 不一致 (live %d / json %d)"
% (t.get("alias") or wi, ii, len(comps), len(jc)))
ok = False
continue
md = 0.0
for a, b in zip(pts, jp):
d = max(abs(a[0] - b[0]), abs(a[1] - b[1]), abs(a[2] - b[2]))
md = max(md, d)
flag = u"OK" if md <= tol else u"差分あり"
if md > tol:
ok = False
print(u"[%-24s iti=%d] 点 %4d 最大差 %.2e %s"
% (t.get("alias") or wi, ii, len(pts), md, flag))
print(u"=== %s ===" % (u"一致" if ok else u"不一致あり"))
return ok
def _verify_quiet(in_path, node, tol=1e-4, geo_index=0):
"""verify の静か版。(ok, 最大差, ターゲット数) を返す。表示なし。"""
try:
data = _read_json(in_path)
bs = _resolve(node)
except Exception:
return (False, None, 0)
g = next((x for x in (data.get("geometry") or [])
if x.get("index") == geo_index), None)
if g is None:
return (False, None, 0)
ok, gmax, n = True, 0.0, 0
for t in g.get("targets") or []:
wi = t["weightIndex"]
for item in t.get("items") or []:
ii = item["itemIndex"]
comps, pts = _read_target_item(bs, geo_index, wi, ii)
jc = item.get("components") or []
jp = item.get("points") or []
n += 1
if comps != jc:
ok = False
continue
for a, b in zip(pts, jp):
d = max(abs(a[0] - b[0]), abs(a[1] - b[1]), abs(a[2] - b[2]))
gmax = max(gmax, d)
if gmax > tol:
ok = False
return (ok, gmax, n)
# ------------------------------------------------------------- batch import (1発)
def import_dir(json_dir, root, suffix="", front_of_chain=True,
replace=True, do_verify=True):
"""
フォルダ内の *.json を、root 直下の同名メッシュへ順に import する冪等バッチ。
json_dir : export_all で作った JSON フォルダ
root : ターゲット階層のルート(例
"Tsubame_v1_0_VRChat4_miya7_openFinger3|geo_gp|head_gp")
JSON の transform 名を root 直下から探す。見つからなければ
root 配下を再帰検索、それでも無ければ SKIP(エラーにしない)。
suffix : 生成ノード名の接尾辞(例 "_miya73" → CLO_Face_Mesh_bs_miya73)
replace : True で既存同名ノードを削除して作り直す(冪等)
最後に 1 行 = 1 メッシュの結果表を出す。途中で 1 件失敗しても続行する。
"""
log = _Logger(_log_path(json_dir))
rows = []
try:
files = sorted(f for f in os.listdir(json_dir)
if f.lower().endswith(".json"))
log.write(u"root = %s" % root)
log.write(u"JSON %d 件: %s" % (len(files), files))
for fn in files:
path = os.path.join(json_dir, fn)
try:
data = _read_json(path)
except Exception as e:
rows.append((fn, "-", "-", u"SKIP 壊れたJSON (%s)" % e))
continue
if data.get("format") != FORMAT:
rows.append((fn, "-", "-", u"SKIP フォーマット違い"))
continue
geos = data.get("geometry") or []
g = geos[0] if geos else {}
mesh_name = g.get("transform")
node_name = (data.get("node") or fn[:-5]) + suffix
# --- メッシュ解決 ---
mesh = None
for cand in (root + "|" + (mesh_name or ""),):
if mesh_name and cmds.ls(cand):
mesh = cand
break
if mesh is None and mesh_name:
deep = [m for m in (cmds.ls(root + "|*|" + mesh_name, long=True)
or []) + (cmds.ls("*|" + mesh_name, long=True)
or []) if m.startswith("|" + root.split("|")[0])
or root.split("|")[0] in m]
deep = deep or (cmds.ls("*|" + mesh_name, long=True) or [])
if len(deep) == 1:
mesh = deep[0]
elif len(deep) > 1:
rows.append((fn, mesh_name, node_name,
u"SKIP 候補が%d件 曖昧: %s" % (len(deep), deep)))
continue
if mesh is None:
rows.append((fn, mesh_name, node_name, u"SKIP メッシュ無し"))
continue
# --- 頂点数プリチェック ---
shp = _shape_of(mesh)
nvj, nvl = g.get("numVertices"), _num_vertices(shp)
if nvj and nvl and nvj != nvl:
rows.append((fn, mesh_name, node_name,
u"SKIP 頂点数 %d≠%d" % (nvj, nvl)))
continue
# --- import ---
try:
bs = import_node(path, mesh=mesh, node_name=node_name,
front_of_chain=front_of_chain, replace=replace)
except Exception as e:
rows.append((fn, mesh_name, node_name, u"FAIL %s" % e))
continue
ntg = len(cmds.listAttr(bs + ".weight", multi=True) or [])
note = u"OK (%d target)" % ntg
if do_verify:
vok, vmax, _ = _verify_quiet(path, bs)
note = u"%s verify=%s (最大差 %.1e)" % (
u"OK" if vok else u"要確認", u"一致" if vok else u"NG",
vmax if vmax is not None else -1)
rows.append((fn, mesh_name, bs, note))
log.write(u" %s -> %s : %s" % (fn, bs, note))
# --- 結果表 ---
log.write(u"---- 結果 ----")
for fn, mesh_name, node, note in rows:
log.write(u" %-30s %-16s %-28s %s"
% (fn, mesh_name or "-", node, note))
bad = [r for r in rows if not r[3].startswith("OK")]
log.write(u"完了: %d 件中 OK %d / 要対応 %d"
% (len(rows), len(rows) - len(bad), len(bad)))
return rows
finally:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
# --------------------------------------------------------------------- cleanup
def _tag_imported(bs, src, tr):
"""import_node が作ったノードに印を付ける(掃除で識別するため)。"""
try:
if not cmds.attributeQuery(_TAG, node=bs, exists=True):
cmds.addAttr(bs, ln=_TAG, dt="string")
cmds.setAttr(bs + "." + _TAG, json.dumps({
"source": src,
"mesh": tr.split("|")[-1],
"when": time.strftime("%Y-%m-%d %H:%M:%S"),
"format": FORMAT,
}), type="string")
except Exception:
pass
def list_imported(verbose=True):
"""blendshape_io.import_node で作った blendShape ノードを列挙。"""
out = []
for bs in cmds.ls(type="blendShape", long=True) or []:
if not cmds.attributeQuery(_TAG, node=bs, exists=True):
continue
try:
info = json.loads(cmds.getAttr(bs + "." + _TAG) or "{}")
except Exception:
info = {}
geo = cmds.blendShape(bs, q=True, geometry=True) or []
out.append({"node": bs, "geometry": geo, "info": info})
if verbose:
print(u"%-40s mesh=%s when=%s"
% (bs.split("|")[-1], info.get("mesh"), info.get("when")))
if verbose and not out:
print(u"blendshape_io で作った blendShape ノードはありません")
return out
def remove_imported(mesh=None, name=None, dry_run=False):
"""
import_node で作った blendShape ノードだけを削除する。
mesh : この transform / shape に付いているものだけ(省略で全部)
name : ノード名のワイルドカード絞り込み(例 "CLO_*_miya7")
dry_run : True で対象一覧だけ表示して削除しない
印 (_TAG 属性) が付いていないノードは絶対に触らない。
印の無い旧 import 分は name= で個別指定して cmds.delete するか、
force_names=[...] を使うこと。
"""
log = _Logger(_log_path())
try:
want_shape = _shape_of(mesh) if mesh else None
targets = []
for bs in cmds.ls(type="blendShape", long=True) or []:
if not cmds.attributeQuery(_TAG, node=bs, exists=True):
continue
if name and not fnmatch.fnmatch(bs.split("|")[-1], name):
continue
if want_shape:
geo = [_resolve(g) for g in
(cmds.blendShape(bs, q=True, geometry=True) or [])]
if want_shape not in geo:
continue
targets.append(bs)
log.write(u"削除対象 %d 件%s: %s"
% (len(targets), u"(dry run)" if dry_run else u"",
[t.split("|")[-1] for t in targets]))
if dry_run or not targets:
return targets
for bs in targets:
try:
cmds.lockNode(bs, lock=False)
except Exception:
pass
try:
cmds.delete(bs)
log.write(u" 削除: %s" % bs.split("|")[-1])
except Exception as e:
log.write(u" 削除失敗 %s: %s" % (bs.split("|")[-1], e))
log.write(u"削除完了")
return targets
finally:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
def remove_by_name(names, dry_run=False):
"""印の有無に関係なく、指定した名前の blendShape ノードを削除する。
印の無い旧 import 分を消したいとき用(names は文字列 or リスト、ワイルドカード可)。"""
if isinstance(names, str):
names = [names]
log = _Logger(_log_path())
try:
hit = []
for pat in names:
for bs in cmds.ls(pat, type="blendShape", long=True) or []:
if bs not in hit:
hit.append(bs)
log.write(u"対象 %d 件%s: %s"
% (len(hit), u"(dry run)" if dry_run else u"",
[h.split("|")[-1] for h in hit]))
if dry_run:
return hit
for bs in hit:
try:
cmds.lockNode(bs, lock=False)
except Exception:
pass
try:
cmds.delete(bs)
log.write(u" 削除: %s" % bs.split("|")[-1])
except Exception as e:
log.write(u" 削除失敗 %s: %s" % (bs.split("|")[-1], e))
return hit
finally:
import sys as _sys
log.close(ok=_sys.exc_info()[0] is None)
# -------------------------------------------------------------------------- misc
def _safe_getattr(node, attr, default=None):
try:
return cmds.getAttr("%s.%s" % (node, attr))
except Exception:
return default
def _write_json(path, data):
d = os.path.dirname(os.path.abspath(path))
if d and not os.path.isdir(d):
os.makedirs(d)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
def _read_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
if __name__ == "__main__":
sel = cmds.ls(sl=True) or []
if sel and cmds.nodeType(sel[0]) == "blendShape":
out = _resolve(sel[0]).split("|")[-1] + ".json"
export_node(sel[0], os.path.join(cmds.workspace(q=True, rd=True), out))
else:
print(u"blendShape ノードを選択して実行するか、bio.export_all(dir) を使ってください")