點(diǎn)清理:Python腳本安全優(yōu)化場(chǎng)景文件)
接手過(guò)別人做到一半的 Maya 工程后最讓人頭疼的往往不是模型布線也不是綁定邏輯而是打開(kāi)工程時(shí)漫長(zhǎng)的等待。一個(gè)只有幾十 MB 的場(chǎng)景看起來(lái)并不大可一旦在 Outliner 里操作Maya 像在逐幀思考按下保存時(shí)底部狀態(tài)欄一直轉(zhuǎn)圈導(dǎo)入幾張參考圖或者外部模型之后Maya 開(kāi)始頻繁失去響應(yīng)。很多人遇到這種狀況第一反應(yīng)是升級(jí)硬件但更常見(jiàn)的原因是場(chǎng)景文件里悄悄堆滿了無(wú)用節(jié)點(diǎn)也就是大家常說(shuō)的“垃圾節(jié)點(diǎn)”。垃圾節(jié)點(diǎn)問(wèn)題之所以難處理是因?yàn)?Maya 的場(chǎng)景結(jié)構(gòu)并不是簡(jiǎn)單的一張零件清單。模型、材質(zhì)、約束、控制器、變形器、表達(dá)式互相之間有父子關(guān)系、屬性連接和依賴關(guān)系直接刪除某個(gè)“看起來(lái)沒(méi)用”的節(jié)點(diǎn)很可能會(huì)連帶刪除仍然被引用的數(shù)據(jù)甚至讓整個(gè)場(chǎng)景崩壞。但垃圾節(jié)點(diǎn)又必須清理否則它會(huì)帶來(lái)文件體積膨脹、操作卡頓、Outliner 難以閱讀、協(xié)作交接時(shí)別人完全不知道哪些節(jié)點(diǎn)可以刪。這篇文章會(huì)圍繞“Maya 文件清理工具 / 垃圾節(jié)點(diǎn)清理”這個(gè)主題講清楚三件事垃圾節(jié)點(diǎn)到底是怎么產(chǎn)生的如何用 Maya 自帶的“優(yōu)化場(chǎng)景大小”功能和手動(dòng)檢查做第一輪清理以及如何用一段可以在 Script Editor 里直接運(yùn)行的 Python 腳本安全地找出并刪除確實(shí)無(wú)用的垃圾節(jié)點(diǎn)。文章還會(huì)給出常見(jiàn)坑的排查思路方便你在實(shí)際項(xiàng)目中復(fù)用。1. Maya 場(chǎng)景為什么會(huì)堆積垃圾節(jié)點(diǎn)要理解清理工具該怎么寫先得理解一條 Maya 場(chǎng)景里的數(shù)據(jù)是怎么被組織起來(lái)的。Maya 并不像普通的三維軟件那樣只管理“物體列表”它內(nèi)部有一套依賴圖。DAG 節(jié)點(diǎn)定義了對(duì)象之間的父子層級(jí)我們看到的 transform、shape、group 都在這個(gè)層級(jí)里DG 節(jié)點(diǎn)則管理著材質(zhì)、動(dòng)畫曲線、約束、變形器等數(shù)據(jù)之間的連接關(guān)系。一個(gè)模型最終能否被正確顯示和渲染取決于這張依賴圖是否完整。在模型、材質(zhì)和動(dòng)畫數(shù)據(jù)緊密連接的情況下場(chǎng)景里一旦出現(xiàn)“看起來(lái)沒(méi)用”的節(jié)點(diǎn)十有八九來(lái)自下面幾種情況第一反復(fù)導(dǎo)入外部資產(chǎn)。從其他 DCC 軟件導(dǎo)入模型、從網(wǎng)上下載資產(chǎn)、或者導(dǎo)入 glTF/GLB 文件時(shí)Maya 會(huì)把源文件里的分組結(jié)構(gòu)、變換節(jié)點(diǎn)、甚至一些源軟件特有的屬性一起帶進(jìn)來(lái)。很多外部文件會(huì)帶多層空 group、多余的 locator、無(wú)實(shí)際作用的 shape 節(jié)點(diǎn)這些就是最常見(jiàn)的垃圾節(jié)點(diǎn)來(lái)源。第二綁定和動(dòng)畫過(guò)程中產(chǎn)生的歷史數(shù)據(jù)。給角色刷權(quán)重會(huì)生成 skinCluster做混合變形會(huì)生成 blendShape給控制器添加約束會(huì)生成 constraint 節(jié)點(diǎn)。這些節(jié)點(diǎn)本身可能還有用但當(dāng)你刪掉被驅(qū)動(dòng)的模型、替換綁定結(jié)構(gòu)、或者把舊動(dòng)畫版本刪除后相關(guān)節(jié)點(diǎn)并不會(huì)自動(dòng)清理干凈還會(huì)殘留在場(chǎng)景里。第三引用文件和命名空間殘留。多人協(xié)作時(shí)場(chǎng)景里會(huì)加載 reference如果某個(gè) reference 文件被卸載或者被替換原來(lái)的 namespace 可能變成空殼大量帶前綴的孤立節(jié)點(diǎn)留在 Outliner 中。這時(shí)候文件不一定變大很多但 Outliner 會(huì)變得非常難用。第四缺少插件或插件版本變化導(dǎo)致的 unknown 節(jié)點(diǎn)。插件卸載后依賴該插件的節(jié)點(diǎn)會(huì)被標(biāo)記為 unknown甚至很多節(jié)點(diǎn)變成無(wú)法編輯的“僵尸節(jié)點(diǎn)”。如果只把這個(gè)場(chǎng)景發(fā)給別人對(duì)方?jīng)]有對(duì)應(yīng)插件unknown 節(jié)點(diǎn)會(huì)在打開(kāi)場(chǎng)景時(shí)繼續(xù)膨脹。所以清理垃圾節(jié)點(diǎn)并不是“打開(kāi)工具一鍵刪除”這么簡(jiǎn)單。你能看到哪些節(jié)點(diǎn)是垃圾取決于你對(duì)依賴圖的理解有多深你能不能安全地刪掉它們則取決于你是否考慮了父子關(guān)系、屬性連接、參考狀態(tài)和插件依賴。真正的清理工具只做一件事找出“確實(shí)沒(méi)有被引用且不會(huì)影響其他數(shù)據(jù)”的節(jié)點(diǎn)。2. 動(dòng)手之前先搞清楚哪些節(jié)點(diǎn)能刪、哪些不能刪在寫刪除腳本之前最忌諱的就是枚舉出一堆節(jié)點(diǎn)類型然后批量刪除。一個(gè)節(jié)點(diǎn)“類型可疑”不代表它“一定可以刪”。在資產(chǎn)制作流程里很多看似多余的空節(jié)點(diǎn)其實(shí)是綁定結(jié)構(gòu)、控制器分組或動(dòng)畫層級(jí)的骨架刪除后角色的一套控制邏輯就斷了。所以我建議先建立一個(gè)判斷框架把所有可疑節(jié)點(diǎn)分成三類。第一類是明確可以清理的節(jié)點(diǎn)。典型的例子包括沒(méi)有被引用的 unknown 節(jié)點(diǎn)沒(méi)有任何子對(duì)象、也沒(méi)有任何連接的空 transform 節(jié)點(diǎn)已經(jīng)被歷史記錄覆蓋但不再生效的無(wú)效表達(dá)式以及完全獨(dú)立的空命名空間。這些節(jié)點(diǎn)不參與渲染、不參與動(dòng)畫求值、不連接任何材質(zhì)或約束刪掉之后場(chǎng)景在功能上沒(méi)有變化。第二類是經(jīng)過(guò)確認(rèn)后才能清理的節(jié)點(diǎn)。這一類包括未使用的材質(zhì)、shadingEngine、貼圖節(jié)點(diǎn)、動(dòng)畫曲線、變形器歷史。比如一個(gè)材質(zhì)球可能和初始 shading group 連接Outliner 中看起來(lái)沒(méi)人用但實(shí)際它可能還被某些歷史節(jié)點(diǎn)引用直接刪會(huì)導(dǎo)致模型顏色異常。這類節(jié)點(diǎn)需要額外驗(yàn)證不能放進(jìn)自動(dòng)刪除腳本的主流程。第三類是絕對(duì)不能自動(dòng)處理的節(jié)點(diǎn)。比如正在被 reference 引用的節(jié)點(diǎn)、被約束和表達(dá)式驅(qū)動(dòng)的節(jié)點(diǎn)、控制器所屬的分組、默認(rèn)攝像機(jī)、默認(rèn)著色組、渲染全局節(jié)點(diǎn)等。這些節(jié)點(diǎn)哪怕看起來(lái)像是空節(jié)點(diǎn)也承擔(dān)著場(chǎng)景基礎(chǔ)功能。自動(dòng)刪除腳本里必須把這類節(jié)點(diǎn)加入保護(hù)名單。為了更直觀我把常見(jiàn)的可疑節(jié)點(diǎn)整理成了一張表節(jié)點(diǎn)類型典型例子是否可以自動(dòng)刪unknown 節(jié)點(diǎn)插件卸載后殘留節(jié)點(diǎn)未被引用時(shí)可以刪空 transform無(wú)子物體、無(wú)連接的 group可以刪但要排除綁定分組locator場(chǎng)景輔助定位點(diǎn)看是否有約束、父子關(guān)系未使用材質(zhì)lambert、blinn、shadingEngine需要核對(duì)連接不建議自動(dòng)刪動(dòng)畫曲線不再使用的 animCurve需要核對(duì)驅(qū)動(dòng)對(duì)象變形器歷史skinCluster、blendShape不建議手動(dòng)刪除容易破壞模型引用中的節(jié)點(diǎn)reference 內(nèi)部對(duì)象不能直接刪要從源頭文件處理默認(rèn)節(jié)點(diǎn)persp、top、defaultLightSet絕對(duì)不能刪這個(gè)判斷框架是所有清理腳本的地基。只要堅(jiān)持“先判斷引用關(guān)系再執(zhí)行刪除”就不會(huì)出現(xiàn)清理完之后模型少了一半的慘劇。3. 第一輪清理先用 Maya 自帶功能做安全排查在實(shí)際項(xiàng)目中我不建議一上來(lái)就寫腳本。Maya 本身就提供了兩個(gè)很實(shí)用的排查入口Outliner 和 File Optimize Scene Size。Outliner 的作用是幫你看清場(chǎng)景層級(jí)。默認(rèn)情況下 Outliner 顯示的是 DAG 節(jié)點(diǎn)也就是 transform 和 shape 層級(jí)。你可以通過(guò) Display 菜單調(diào)整顯示內(nèi)容把 DG 節(jié)點(diǎn)、未知節(jié)點(diǎn)也顯示出來(lái)。操作路徑一般是 Window Outliner然后在 Outliner 菜單中找到 Display勾選相關(guān)內(nèi)容。當(dāng)場(chǎng)景里出現(xiàn) unknown 節(jié)點(diǎn)時(shí)在 Outliner 中選中它再打開(kāi) Hypergraph 的 Connections 面板就能看到它有沒(méi)有連接到其他節(jié)點(diǎn)。如果它孤零零地掛在那里沒(méi)有 connections刪除它的風(fēng)險(xiǎn)就比較低。Outliner 適合做“點(diǎn)狀排查”真正適合做第一輪批量清理的是 File Optimize Scene Size。這個(gè)功能很多初學(xué)者知道但使用率不高。它會(huì)清除場(chǎng)景中未使用的節(jié)點(diǎn)類型比如無(wú)用節(jié)點(diǎn)、空分組、不使用的材質(zhì)球、無(wú)效動(dòng)畫曲線、多余表達(dá)式等。打開(kāi) Optimize Scene Size 后界面會(huì)列出可清理項(xiàng)你可以在選項(xiàng)中勾選需要清理的內(nèi)容。這里有兩種不同的使用方式。如果你處理的是自己剛導(dǎo)入的外部資產(chǎn)可以勾選“Remove unused nodes”“Remove empty groups”之類的選項(xiàng)然后執(zhí)行清理。如果你處理的是綁定或動(dòng)畫文件就要謹(jǐn)慎勾選和材質(zhì)、變形器、表達(dá)式相關(guān)的清理項(xiàng)因?yàn)榻壎ㄎ募锏暮芏噙B接不是通過(guò) Outliner 圖層直接看到的。運(yùn)行 Optimize Scene Size 之前還有一個(gè)容易被忽略的步驟把當(dāng)前場(chǎng)景另存為一個(gè)副本。這個(gè)功能本身不是破壞性工具但它清理的是“未使用”的數(shù)據(jù)而“未使用”的定義在不同項(xiàng)目里并不一樣。比如一個(gè)用于視覺(jué)開(kāi)發(fā)的材質(zhì)球在當(dāng)前場(chǎng)景中沒(méi)有被任何模型引用但你可能希望在后續(xù)建模時(shí)繼續(xù)沿用它的參數(shù)Optimize Scene Size 會(huì)把它當(dāng)成垃圾清理掉。所以在正式項(xiàng)目里先復(fù)制一份再優(yōu)化是最穩(wěn)妥的紀(jì)律。4. 編寫清理工具的必備思維先報(bào)告、后刪除用過(guò)很多第三方清理插件后你會(huì)發(fā)現(xiàn)好的清理工具和危險(xiǎn)清理工具之間有一個(gè)顯著區(qū)別前者會(huì)先告訴你“我打算刪哪些東西”后者則直接執(zhí)行刪除。這個(gè)區(qū)別在 Maya 清理場(chǎng)景中非常重要。我們自己編寫 Maya 清理工具時(shí)也應(yīng)該采用相同的設(shè)計(jì)思路。核心原則是清理工具應(yīng)該有兩個(gè)模式默認(rèn)進(jìn)入 dry_run 模式也就是只掃描、只報(bào)告不執(zhí)行任何刪除只有在人工確認(rèn)清單無(wú)誤后才切換 dry_runFalse 進(jìn)入真實(shí)刪除模式。掃描階段需要輸出的內(nèi)容包括當(dāng)前場(chǎng)景中所有 unknown 節(jié)點(diǎn)所有沒(méi)有被 reference 引用且沒(méi)有連接的空 transform 節(jié)點(diǎn)可疑空 namespace以及每個(gè)節(jié)點(diǎn)之所以被列入清理候選的原因。輸出越詳細(xì)使用工具的人越容易判斷下一步該怎么做。在 Maya 中實(shí)現(xiàn)這套邏輯并不難。Maya 提供了一組非常成熟的 Python 命令也就是maya.cmds。這個(gè)模塊不需要額外安裝Maya 自帶且兼容 Python 2 和 Python 3。你可能聽(tīng)說(shuō)過(guò) Maya 2022 這類版本正處于 Python 2/Python 3 交替期但只要我們不在代碼里使用某個(gè) Python 大版本獨(dú)有的語(yǔ)法這段清理腳本在兩個(gè)解釋器下都能運(yùn)行。下面是第一個(gè)腳本它只做掃描與報(bào)告不刪除任何節(jié)點(diǎn)# -*- coding: utf-8 -*- # 文件scene_health_report.py # 用途掃描當(dāng)前 Maya 場(chǎng)景輸出可能成為垃圾節(jié)點(diǎn)的可疑清單。 # 運(yùn)行方式在 Maya Script Editor 的 Python 標(biāo)簽中粘貼并執(zhí)行。 import maya.cmds as cmds def is_referenced_node(node): 判斷節(jié)點(diǎn)是否屬于 reference。 如果一個(gè)節(jié)點(diǎn)來(lái)自外部引用文件就不能在當(dāng)前場(chǎng)景中直接刪除。 try: return cmds.referenceQuery(node, isNodeReferencedTrue) except RuntimeError: return False def collect_empty_transforms(): 收集“真正為空”的 transform 節(jié)點(diǎn) 1. 沒(méi)有子對(duì)象 2. 沒(méi)有 shape 節(jié)點(diǎn) 3. 沒(méi)有任何連接 empty_transforms [] for tr in cmds.ls(typetransform) or []: # 默認(rèn)節(jié)點(diǎn)名直接跳過(guò) if tr in [persp, top, front, side]: continue # 有子對(duì)象說(shuō)明它不是空組 if cmds.listRelatives(tr, childrenTrue): continue # 有 shape說(shuō)明它可能承載幾何體或相機(jī) if cmds.listRelatives(tr, shapesTrue): continue # 有連接說(shuō)明它可能有約束、動(dòng)畫或表達(dá)式 if cmds.listConnections(tr): continue empty_transforms.append(tr) return empty_transforms def run_health_report(): print( Scene Health Report ) # 1. 查看 unknown 節(jié)點(diǎn) unknown_nodes cmds.ls(typeunknown) or [] local_unknown [] referenced_unknown [] for node in unknown_nodes: if is_referenced_node(node): referenced_unknown.append(node) else: local_unknown.append(node) print([unknown nodes]) print( referenced: {}.format(len(referenced_unknown))) for node in referenced_unknown: print( - {}.format(node)) print( local: {}.format(len(local_unknown))) for node in local_unknown: print( - {}.format(node)) # 2. 查看空 transform empty_transforms collect_empty_transforms() print([empty transforms]) for tr in empty_transforms: print( - {}.format(tr)) # 3. 查看當(dāng)前場(chǎng)景文件大小 scene_path cmds.file(queryTrue, sceneNameTrue) if scene_path: import os size_bytes os.path.getsize(scene_path) print([scene file]) print( path: {}.format(scene_path)) print( size: {:.2f} MB.format(size_bytes / 1024.0 / 1024.0)) print() if __name__ __main__: run_health_report()這個(gè)腳本運(yùn)行之后你會(huì)在 Script Editor 中看到類似下面的輸出 Scene Health Report [unknown nodes] referenced: 0 local: 2 - unknown1 - unknown2 [empty transforms] - group7 - group8 [scene file] path: D:/work/project/asset/base_mesh.mb size: 23.45 MB 看到輸出后不要急著手動(dòng)刪。你要做的是逐一確認(rèn)這些節(jié)點(diǎn)為什么會(huì)出現(xiàn)。比如group7可能是從外部導(dǎo)入 glTF 時(shí)帶入的多余空分組也可能是綁定師特意保留的控制器頂層分組。如果它沒(méi)有任何子物體也沒(méi)有連接那它大概率是真正的垃圾節(jié)點(diǎn)如果你不確定回到 Outliner 里選中它再查一次它的連接情況。5. 安全刪除工具清理流程完整代碼實(shí)現(xiàn)有了掃描報(bào)告下一步就是寫真正的刪除工具。為了讓這個(gè)工具項(xiàng)目具備實(shí)用價(jià)值還需要再加一層保護(hù)機(jī)制。我先說(shuō)清楚設(shè)計(jì)思路刪除腳本不追求“刪得越多越好”而是追求“刪得越安全越好”。腳本把所有節(jié)點(diǎn)分成三類受保護(hù)節(jié)點(diǎn)、跳過(guò)節(jié)點(diǎn)、候選節(jié)點(diǎn)。受保護(hù)節(jié)點(diǎn)是默認(rèn)生成的渲染、相機(jī)、著色節(jié)點(diǎn)跳過(guò)節(jié)點(diǎn)是被 reference 引用、被連接、包含子節(jié)點(diǎn)或 shape 的節(jié)點(diǎn)候選節(jié)點(diǎn)是 unknown 且未被引用或者 transform 且無(wú)任何連接內(nèi)容的節(jié)點(diǎn)。真實(shí)模式下只處理候選節(jié)點(diǎn)并且候選節(jié)點(diǎn)在進(jìn)入cmds.delete()之前還會(huì)先嘗試解鎖。下面是完整腳本# -*- coding: utf-8 -*- # 文件cleanup_scene_tool.py # 用途清理 Maya 場(chǎng)景中已確認(rèn)無(wú)用的垃圾節(jié)點(diǎn)。 # 默認(rèn) dry_runTrue只打印“準(zhǔn)備刪除”的內(nèi)容。 # 確認(rèn)無(wú)誤后將 main(dry_runTrue) 改成 main(dry_runFalse) 再執(zhí)行。 from __future__ import print_function import maya.cmds as cmds # 受保護(hù)節(jié)點(diǎn)這些是 Maya 默認(rèn)環(huán)境節(jié)點(diǎn)絕對(duì)不能出現(xiàn)在刪除清單里。 PROTECTED_NODES set([ persp, perspShape, top, topShape, front, frontShape, side, sideShape, defaultLightSet, defaultObjectSet, initialShadingGroup, initialParticleSE, lambert1, particleCloud1, time1, sequenceManager1, hardwareRenderingGlobals, renderGlobalsList, defaultRenderGlobals, defaultResolution, defaultColorMgtGlobals, colorManagementGlobals, ]) def is_referenced_node(node): 判斷節(jié)點(diǎn)是否位于外部 reference 中。 被引用的節(jié)點(diǎn)應(yīng)回到引用文件源頭處理不能在當(dāng)前場(chǎng)景刪除。 try: return cmds.referenceQuery(node, isNodeReferencedTrue) except RuntimeError: return False def unlock_node(node): 部分 unknown 節(jié)點(diǎn)默認(rèn)處于鎖定狀態(tài)。 刪除前先嘗試解除鎖定。 try: cmds.lockNode(node, lockFalse) except RuntimeError: pass def safe_delete_node(node): 嘗試刪除一個(gè)節(jié)點(diǎn)。如果刪除失敗只打印錯(cuò)誤不中斷整個(gè)清理流程。 try: cmds.delete(node) return True except RuntimeError as exc: print(delete failed: {} - {}.format(node, exc)) return False def check_node_safety(node): 對(duì)單個(gè)節(jié)點(diǎn)做安全檢查。 返回 (can_delete, reason) # 保護(hù)節(jié)點(diǎn)直接拒絕 if node in PROTECTED_NODES: return False, protected # 引用節(jié)點(diǎn)不能動(dòng) if is_referenced_node(node): return False, referenced node_type cmds.nodeType(node) # unknown 節(jié)點(diǎn)未被引用且節(jié)點(diǎn)類型為 unknown就可以加入候選 if node_type unknown: return True, unknown # transform 節(jié)點(diǎn)必須是真正的空 transform if node_type transform: if cmds.listRelatives(node, childrenTrue): return False, has_children if cmds.listRelatives(node, shapesTrue): return False, has_shape if cmds.listConnections(node): return False, has_connections return True, empty_transform return False, not_in_cleanup_scope def build_cleanup_candidates(dry_runTrue): 掃描全場(chǎng)景生成候選清理列表。 candidates [] # 先查 unknown 節(jié)點(diǎn) for node in cmds.ls(typeunknown) or []: can_delete, reason check_node_safety(node) if can_delete: candidates.append((node, reason)) # 再查所有 transform 中的空節(jié)點(diǎn) # 這里要遍歷全部 transform所以受保護(hù)節(jié)點(diǎn)會(huì)先被過(guò)濾一次 for tr in cmds.ls(typetransform) or []: can_delete, reason check_node_safety(tr) if can_delete: candidates.append((tr, reason)) # 名字去重避免同一個(gè)節(jié)點(diǎn)在不同查詢里出現(xiàn)多次 seen set() unique_candidates [] for node, reason in candidates: if node in seen: continue seen.add(node) unique_candidates.append((node, reason)) return unique_candidates def run_cleanup(dry_runTrue): 清理主流程。 dry_runTrue 時(shí)只打印候選清單不執(zhí)行刪除。 candidates build_cleanup_candidates(dry_rundry_run) if not candidates: print(No cleanup candidate found.) return 0 print(Cleanup candidate count: {}.format(len(candidates))) delete_success 0 for node, reason in candidates: if dry_run: print([dry-run] would delete: {} (reason{}).format(node, reason)) continue print([cleanup] deleting: {} (reason{}).format(node, reason)) unlock_node(node) if safe_delete_node(node): delete_success 1 if dry_run: print(Dry-run finished. Review the list above.) print(If it looks good, run run_cleanup(dry_runFalse).) else: print(Cleanup finished. Deleted count: {}.format(delete_success)) return delete_success # 默認(rèn)先執(zhí)行 dry_run。 # 當(dāng)你看完輸出并確認(rèn)安全后把下一行改成 run_cleanup(dry_runFalse) 再執(zhí)行。 run_cleanup(dry_runTrue)腳本執(zhí)行后會(huì)先進(jìn)入 dry_run 模式你會(huì)看到類似下面的輸出Cleanup candidate count: 3 [dry-run] would delete: unknown1 (reasonunknown) [dry-run] would delete: group7 (reasonempty_transform) [dry-run] would delete: group8 (reasonempty_transform) Dry-run finished. Review the list above. If it looks good, run run_cleanup(dry_runFalse).此時(shí)腳本還沒(méi)有刪除任何節(jié)點(diǎn)。你可以拿著候選清單回到場(chǎng)景再人工核對(duì)一次。確定安全后把腳本最后一行改成run_cleanup(dry_runFalse)再執(zhí)行腳本會(huì)進(jìn)入真實(shí)刪除模式。如果你需要經(jīng)常清理場(chǎng)景還可以把這套腳本做成一個(gè) Shelf 按鈕。在 Maya 的 Shelf Editor 中新建一個(gè)按鈕選擇 Python 命令將清理函數(shù)封裝為一行命令調(diào)用即可。這樣以后拿到一個(gè)外部資產(chǎn)場(chǎng)景可以先點(diǎn)擊“報(bào)告”按鈕看候選再確認(rèn)是否執(zhí)行刪除整個(gè)流程會(huì)和商業(yè)清理