出紋理:Python腳本自動(dòng)化渲染調(diào)試)
簡(jiǎn)介RenderDocV1.x批量導(dǎo)出紋理版是基于RenderDoc二次開發(fā)的圖形調(diào)試增強(qiáng)工具面向游戲研發(fā)、圖形編程及渲染分析人員解決原生版本只能逐個(gè)導(dǎo)出紋理、流程繁瑣、耗時(shí)明顯的問題。修改版通過擴(kuò)展RenderDoc接口、遍歷幀內(nèi)紋理資源并引入多線程與多種常見格式支持把大量紋理的導(dǎo)出工作壓縮為批量操作尤其適合貼圖量大或需要周期性做資源審計(jì)的項(xiàng)目。壓縮包共51個(gè)文件約51.6MB核心由29個(gè)dll動(dòng)態(tài)庫、6個(gè)exe執(zhí)行程序、6個(gè)pyd擴(kuò)展模塊組成同時(shí)帶有多份頭文件與json配置并內(nèi)置Qt/Python運(yùn)行組件和x86/x64兩套R(shí)elease版本。包內(nèi)同時(shí)提供圖形界面與命令行可執(zhí)行程序解壓后可按環(huán)境選用直接用于幀紋理的批量提取和保存。已有1186人學(xué)習(xí)下載適合需要快速獲取幀紋理、整理貼圖資源或排查渲染問題的開發(fā)者收藏參考。1. 批量導(dǎo)出紋理這回事先把場(chǎng)景說清楚做幀調(diào)試的人都有這種經(jīng)歷RenderDoc V1.x里打開一個(gè)捕獲場(chǎng)景里二三十張紋理等著逐個(gè)過目。右鍵一張張Export點(diǎn)到后面手酸還經(jīng)常漏點(diǎn)。RenderDoc V1.x批量導(dǎo)出紋理版解決的就是這個(gè)寫一段腳本把捕獲里所有紋理按你定的命名規(guī)則、格式和層級(jí)一次性落到磁盤。這套做法適合三種人一是做渲染調(diào)試時(shí)要把幀里所有資源歸檔的開發(fā)者二是技術(shù)美術(shù)需要把美術(shù)資源在實(shí)時(shí)渲染里的終版效果導(dǎo)出來核對(duì)三是做自動(dòng)化比對(duì)的人想拿不同幀的紋理做像素級(jí)diff。前提是你對(duì)RenderDoc的基本操作已經(jīng)順手但不想繼續(xù)忍受機(jī)械重復(fù)。目標(biāo)是讓腳本幫你把“看完一張導(dǎo)出一次”變成“跑一條命令全部拿到”。2. 導(dǎo)出前必須搞清的三個(gè)概念資源、紋理與保存格式批量導(dǎo)出腳本的核心邏輯是“遍歷描述 → 選格式 → 落盤”。如果不知道RenderDoc用什么結(jié)構(gòu)描述一張紋理參數(shù)就只能靠猜導(dǎo)出的結(jié)果就會(huì)各種不對(duì)勁。這章先講概念再給選型。2.1 RenderDoc眼里的紋理不只是一張圖片在RenderDoc的回放環(huán)境里每個(gè)紋理資源對(duì)應(yīng)一個(gè)唯一的ResourceId同時(shí)附帶一份TextureDescription結(jié)構(gòu)體。腳本里調(diào)用GetTextures()返回的就是這些描述對(duì)象的列表。字段大概包括width、height、depth是基礎(chǔ)尺寸mips是mip層級(jí)數(shù)arraySize是數(shù)組層數(shù)type區(qū)分Texture1D、Texture2D、Texture2DArray、Texture3D、Cubeformat是ResourceFormat里面有compTypeFloat、UNorm、UInt、Depth等和compCount。批量導(dǎo)出最容易漏掉的就是arraySize和mips。UI右鍵導(dǎo)出時(shí)RenderDoc默認(rèn)幫你導(dǎo)了“當(dāng)前看得見的那一層”腳本沒有這個(gè)默認(rèn)值。你不處理CubeMap只拿到第一個(gè)面數(shù)組紋理只拿到第一層。這也是后面避坑章節(jié)的伏筆。format字段決定了你用什么格式落盤。比如format是BC7的壓縮紋理RenderDoc在導(dǎo)出PNG時(shí)會(huì)自動(dòng)解壓如果你走原始數(shù)據(jù)讀取再自己編碼就得先解壓BC7塊這活兒自己做起來相當(dāng)痛苦。所以腳本導(dǎo)出時(shí)盡量別自己碰原始字節(jié)交給RenderDoc的保存接口。2.2 三種導(dǎo)出姿勢(shì)UI右鍵、離線腳本、內(nèi)嵌控制臺(tái)常見的導(dǎo)出姿勢(shì)有三種。UI右鍵導(dǎo)出適合臨時(shí)看一兩張它把當(dāng)前資源通道、當(dāng)前mip、當(dāng)前面的那一份存下來優(yōu)點(diǎn)是直觀缺點(diǎn)是沒法批量。renderdoccmd是RenderDoc自帶命令行工具可以回放捕獲但它的職責(zé)更多在捕獲、回放和自動(dòng)化測(cè)試拿來做靈活導(dǎo)出不如腳本順手。最可控的是Python接口能拿到完整描述、能做過濾、能循環(huán)、能重試出了錯(cuò)還能打日志。方式適用場(chǎng)景批量能力可定制性UI右鍵導(dǎo)出臨時(shí)看一兩張紋理弱低renderdoccmd捕獲、回放、回歸測(cè)試中中Python腳本批量導(dǎo)出、歸檔、像素比對(duì)強(qiáng)高我一般會(huì)直接用Python接口。RenderDoc安裝目錄里自帶Python接口模塊夠用腳本跑在哪個(gè)環(huán)境不重要重要的是先把接口跑通。V1.x的接口整體穩(wěn)定但小版本之間偶爾有枚舉名和函數(shù)簽名的漂移遇到問題先查本機(jī)版本的接口簽名別上來就懷疑邏輯。2.3 導(dǎo)出格式怎么選PNG、DDS、KTX、EXR腳本里最難定的不是邏輯是格式。批量導(dǎo)出前先想清楚這批紋理給誰看。導(dǎo)出一份用于核對(duì)的紋理我習(xí)慣PNG導(dǎo)出給引擎?zhèn)扔玫腄DS或KTX發(fā)現(xiàn)紋理里存了float數(shù)據(jù)比如GBuffer里的深度/法線用EXR才能保證不丟精度。格式典型場(chǎng)景是否保留mip備注PNG文檔、驗(yàn)收、像素比對(duì)否無損適合RGBA8DDS回拷引擎、工具鏈?zhǔn)悄鼙A魤嚎s塊信息KTX移動(dòng)端、運(yùn)行時(shí)加載是需要配套解析庫EXRHDR、float紋理否保存高動(dòng)態(tài)范圍TextureSave里通過format字段指定目標(biāo)格式比如FileType.PNG、FileType.DDS、FileType.KTX、FileType.EXR。不同格式會(huì)直接影響保存接口內(nèi)部走的編碼路徑所以“先定格式再寫循環(huán)”是對(duì)的。另外還要理解TextureSave這個(gè)配置對(duì)象它管的不只是格式還有mip、slice、目的位置磁盤還是內(nèi)存緩沖。批量腳本里常用的是把destination設(shè)為Disk直接落盤。3. 用Python腳本把批量導(dǎo)出跑通最小可用版現(xiàn)在開始動(dòng)手。這章給一個(gè)能直接用的最小腳本然后拆開講參數(shù)含義。跑通之后再做命名、過濾和重試這些工程化的事。3.1 最小腳本遍歷紋理、逐個(gè)落盤import renderdoc as rd import os def export_all_textures(capture_path: str, output_dir: str) - int: # 初始化RenderDoc運(yùn)行時(shí) rd.Initialise() # 打開捕獲文件并加載 controller rd.ReplayController.OpenCaptureFile(capture_path) if controller is None: raise RuntimeError(無法打開捕獲文件: %s % capture_path) err controller.LoadCapture() if err ! 0: raise RuntimeError(加載捕獲失敗錯(cuò)誤碼: %d % err) # 遍歷所有紋理資源 textures controller.GetTextures() os.makedirs(output_dir, exist_okTrue) for tex in textures: # tex 是 TextureDescriptionresourceId 唯一標(biāo)識(shí)資源 out_path os.path.join(output_dir, f{tex.resourceId}.png) result controller.SaveTexture(tex.resourceId, out_path) print(f{tex.resourceId} {tex.width}x{tex.height} - {out_path} ({result})) controller.Shutdown() rd.Shutdown() return 0 if __name__ __main__: export_all_textures(frame_0123.rdc, exported_textures)這個(gè)腳本的邏輯很直白初始化RenderDoc打開捕獲文件加載回放拿全部紋理描述循環(huán)保存成PNG。SaveTexture第一個(gè)參數(shù)是ResourceId第二個(gè)參數(shù)是落盤路徑。這里沒有指定mip和slice默認(rèn)保存的是“可用視圖”對(duì)應(yīng)的那一層對(duì)于普通Texture2D夠用了。腳本末尾的Shutdown不能省批量跑多個(gè)捕獲文件時(shí)不釋放回放控制器會(huì)累積顯存和內(nèi)存。跑之前有兩件事要確認(rèn)一是確保Python能找到renderdoc模塊常見做法是在RenderDoc安裝目錄里找到Python接口模塊路徑或者直接用RenderDoc UI自帶的內(nèi)嵌控制臺(tái)跑省掉sys.path的折騰二是確認(rèn)捕獲文件是用匹配的V1.x版本生成的版本跨太大的文件打不開或者加載報(bào)錯(cuò)。3.2 參數(shù)怎么調(diào)整mip層、數(shù)組切片、目標(biāo)目錄最小腳本導(dǎo)出的是“默認(rèn)層”但真實(shí)場(chǎng)景里你得控制導(dǎo)出哪一層mip、哪一層數(shù)組切片、命名帶什么后綴。這就需要TextureSave對(duì)象def export_with_options(controller, tex, output_dir): # 構(gòu)造保存選項(xiàng) opts rd.TextureSave() opts.format rd.FileType.PNG opts.mip 0 # 只導(dǎo)出第0層mip opts.slice 0 # 數(shù)組/立方體貼圖的第0層 opts.destination rd.TextureSaveDestination.Disk out_path os.path.join(output_dir, f{tex.resourceId}_m0.png) # 帶選項(xiàng)的保存不同小版本對(duì)tex參數(shù)的傳法略有差異 controller.SaveTexture(tex.resourceId, out_path, opts)mip參數(shù)控制mip層級(jí)0是最高分辨率那一層tex.mips - 1是最小的一層。slice參數(shù)在普通Texture2D上無效但在Texture2DArray和Cube上就是決定生死的關(guān)鍵。Cube紋理的arraySize通常是6每個(gè)面一個(gè)slice遍歷時(shí)要把slice從0循環(huán)到5才能導(dǎo)出完整天空盒。destination設(shè)為Disk表示直接寫文件如果后續(xù)要對(duì)紋理數(shù)據(jù)做二次處理可以改成MemoryBuffer從返回的字節(jié)數(shù)組自己處理。這里要特別提一下SaveTexture在不同小版本里的簽名差異。V1.x早期版本有的要求傳(resourceId, path)有的版本傳(resourceId, path, opts)還有的版本把path放進(jìn)了opts.destination。寫通用腳本時(shí)用hasattr或者try/except包一層別把簽名寫死。用dir(controller.SaveTexture)看本機(jī)版本的簽名比翻文檔快。3.3 從UI里驗(yàn)證腳本結(jié)果和手點(diǎn)導(dǎo)出一致腳本跑完先別急著批量使用選一張紋理做一致性校驗(yàn)。在RenderDoc UI里找到同一個(gè)ResourceId的紋理右鍵Export成PNG然后用文件比對(duì)工具對(duì)比UI導(dǎo)出文件和腳本導(dǎo)出文件。理論上同一版本、同一導(dǎo)出格式、同一mip和slice兩份文件的像素?cái)?shù)據(jù)應(yīng)該完全一致。我習(xí)慣用md5做快速校驗(yàn)md5sum ui_export.png script_export.png。如果md5一致說明腳本走的路子和UI一致可以信任批量結(jié)果如果不一致先檢查導(dǎo)出的mip和slice是否一致再看是不是文件名里帶了不同的后綴。UI右鍵導(dǎo)出時(shí)RenderDoc也遵循同樣的TextureSave邏輯出現(xiàn)不一致大概率是你腳本里設(shè)置了UI沒有的設(shè)置項(xiàng)比如mip不是0。4. 把腳本改成能每天用的工具命名、過濾和錯(cuò)誤處理最小腳本能跑通但要天天用還差得遠(yuǎn)。原始文件名是resourceId數(shù)字串看不出哪張是哪張捕獲里還混著一堆1x1像素的占位紋理和buffer導(dǎo)出來純屬噪音。這章把腳本往工具方向推。4.1 命名規(guī)則加尺寸、格式、層級(jí)別用resourceId裸奔resourceId做文件名有一個(gè)好處是唯一但可讀性為零。你在項(xiàng)目里看到一個(gè)1234567890.png根本不知道它對(duì)應(yīng)的是漫反射還是法線。我常用的命名規(guī)則是把紋理名、尺寸、格式和層級(jí)拼在一起def build_name(tex, mip_idx0, slice_idx0): fmt_name tex.format.shortName() if hasattr(tex.format, shortName) else str(tex.format) base tex.name if tex.name else str(tex.resourceId) return f{base}_{tex.width}x{tex.height}_{fmt_name}_m{mip_idx}_s{slice_idx}.png紋理的name字段在捕獲里不一定有很多內(nèi)部資源名是空的。這時(shí)用resourceId兜底至少保證不重名。shortName方法把R8G8B8A8_UNORM壓縮成可讀性好的短名比打一串全稱好。帶上mip和slice后綴是為了避免同一紋理不同層導(dǎo)出時(shí)互相覆蓋。最后清理文件名里的非法字符把空格換成下劃線去掉路徑分隔符否則在部分平臺(tái)上寫文件會(huì)報(bào)錯(cuò)。4.2 過濾邏輯只導(dǎo)關(guān)心的事件期間的紋理一幀捕獲里的紋理資源數(shù)量比你想的多。RenderDoc的GetTextures()返回所有分配過的紋理包括1x1的占位紋理、深度緩沖、staging紋理。你真正關(guān)心的可能是幾張主場(chǎng)景紋理所以過濾邏輯要加for tex in textures: # 跳過buffer類型 if tex.type rd.TextureType.Buffer: continue # 跳過過小的紋理 if tex.width 16 and tex.height 16: continue # 跳過明顯是占位符的無名資源 if not tex.name and tex.width 4: continue # 導(dǎo)出 opts rd.TextureSave() opts.format rd.FileType.PNG opts.mip 0 opts.slice 0 opts.destination rd.TextureSaveDestination.Disk out_path os.path.join(output_dir, build_name(tex)) controller.SaveTexture(tex.resourceId, out_path, opts)過濾條件按需增減。比如調(diào)試GBuffer時(shí)反而要導(dǎo)深度紋理這時(shí)就別按維度過濾而是按compType過濾把Depth類型單獨(dú)挑出來導(dǎo)EXR。常見做法是先跑一次全量導(dǎo)出py文件里打印每個(gè)紋理的name、type、format、尺寸看一遍再定過濾規(guī)則。第一次別太自信先看全貌再砍數(shù)據(jù)。4.3 斷點(diǎn)續(xù)導(dǎo)與失敗重試批量導(dǎo)出幾十張紋理時(shí)中途可能因?yàn)榇疟P空間不足、單個(gè)紋理過大、驅(qū)動(dòng)超時(shí)等問題中斷。重新從頭跑一遍浪費(fèi)時(shí)間所以斷點(diǎn)續(xù)導(dǎo)是剛需。我通常維護(hù)一個(gè)文本清單記錄已完成導(dǎo)出的文件路徑manifest_path os.path.join(output_dir, exported.txt) done set() if os.path.exists(manifest_path): with open(manifest_path, r, encodingutf-8) as fp: done.update(line.strip() for line in fp if line.strip()) for tex in textures: out_path os.path.join(output_dir, build_name(tex)) if out_path in done: continue for attempt in range(2): try: controller.SaveTexture(tex.resourceId, out_path, opts) with open(manifest_path, a, encodingutf-8) as fp: fp.write(out_path \n) break except Exception as e: print(f第{attempt 1}次導(dǎo)出失敗: {tex.resourceId} - {e})這個(gè)做法的關(guān)鍵是“先寫清單再繼續(xù)下一個(gè)”確保導(dǎo)出成功的文件一定被記錄。失敗重試兩次就放棄避免死循環(huán)。清單文件本身也方便你事后統(tǒng)計(jì)哪些紋理成功、哪些失敗、失敗原因是什么。配合上一條的打印日志基本能定位所有問題。5. 批量導(dǎo)出紋理避坑實(shí)錄現(xiàn)象、原因與解決腳本跑多了總會(huì)遇到玄學(xué)問題。這章寫五個(gè)我實(shí)際踩過的坑按“現(xiàn)象 → 原因 → 解決”記錄給后來者少燒點(diǎn)時(shí)間。5.1 黑圖翻車壓縮紋理沒有正確解壓就落盤現(xiàn)象導(dǎo)出的PNG完全黑或者大面積花屏但RenderDoc UI里預(yù)覽卻正常。第一次遇到以為是顯存損壞嚇一跳。原因部分紋理在驅(qū)動(dòng)側(cè)是塊壓縮格式BC1/BC3/BC7RenderDoc的預(yù)覽視圖會(huì)做解壓顯示但腳本直接讀取原始數(shù)據(jù)時(shí)拿到的是壓縮塊。后續(xù)保存為PNG時(shí)如果代碼路徑里沒有觸發(fā)解壓就會(huì)把壓縮數(shù)據(jù)當(dāng)像素?cái)?shù)據(jù)編碼得到黑圖和花屏。解決把導(dǎo)出交給SaveTexture并明確指定目標(biāo)格式。RenderDoc在保存為PNG/DDS/EXR時(shí)會(huì)按目標(biāo)格式自動(dòng)處理源紋理的壓縮狀態(tài)。千萬不要自己用GetTextureData拿字節(jié)緩存再交給PIL之類的庫編碼除非你明確知道自己在解壓BC塊。另外導(dǎo)出時(shí)留意tex.format.blockSize字段看到blockSize大于0的格式優(yōu)先用SaveTexture而不是手寫編碼路徑。5.2 數(shù)組紋理和CubeMap只導(dǎo)出第一層現(xiàn)象CubeMap紋理導(dǎo)出的PNG只有第一個(gè)面的內(nèi)容另外五個(gè)面憑空消失數(shù)組紋理導(dǎo)出的結(jié)果只有slice 0。原因SaveTexture默認(rèn)的slice參數(shù)是0UI右鍵導(dǎo)出時(shí)會(huì)自動(dòng)按當(dāng)前選中的面處理腳本沒有這個(gè)默認(rèn)必須顯式循環(huán)。解決判斷tex.arraySize或tex.type。Cube類型的arraySize在RenderDoc里通常表現(xiàn)為6寫循環(huán)時(shí)不要用arraySize硬編碼按type判斷更穩(wěn)妥if tex.type rd.TextureType.Cube: slice_count 6 elif tex.type rd.TextureType.Texture2DArray: slice_count tex.arraySize else: slice_count 1 for slice_idx in range(slice_count): opts.slice slice_idx out_path ... # 帶slice后綴 controller.SaveTexture(tex.resourceId, out_path, opts)5.3 Windows下中文路徑導(dǎo)出0字節(jié)現(xiàn)象腳本沒報(bào)錯(cuò)輸出目錄也生成了文件但文件大小是0。單獨(dú)查文件名發(fā)現(xiàn)路徑里帶中文或者空格比如D:\測(cè)試輸出\water map.png。原因V1.x部分版本的底層文件寫入用了窄字符路徑在Windows上遇到非ASCII字符時(shí)寫文件失敗但上層接口沒有把錯(cuò)誤透?jìng)鞒鰜盱o默產(chǎn)出0字節(jié)文件。解決全ASCII輸出目錄文件名里不要留中文。路徑中的空格盡量換成下劃線。這是一個(gè)干脆的規(guī)避方案——為驗(yàn)證工具折騰寬字符路徑編碼不值得。同時(shí)加一道防線保存后立即檢查文件大小等于0就視為導(dǎo)出失敗打印警告并計(jì)入失敗清單。這樣即使遇到其他路徑編碼問題也不會(huì)靜默通過。5.4 在action循環(huán)里到處調(diào)SaveTexture導(dǎo)致重復(fù)導(dǎo)出幾十次現(xiàn)象導(dǎo)出的文件數(shù)量比預(yù)期多一個(gè)數(shù)量級(jí)同一張紋理出現(xiàn)幾十次只是名字后綴不同。原因遍歷渲染動(dòng)作列表action tree時(shí)每個(gè)draw call都引用了各自的綁定資源同一張紋理被多個(gè)action綁定循環(huán)內(nèi)直接導(dǎo)出自然重復(fù)。解決不要再action循環(huán)里做導(dǎo)出改為先GetTextures()拿到資源全集再按資源的ResourceId去重后導(dǎo)出。如果目標(biāo)本來就是“導(dǎo)出某幾個(gè)pass用到的紋理”也要用一個(gè)set先收集ResourceId再統(tǒng)一走導(dǎo)出循環(huán)。這個(gè)坑的根源是“資源”和“資源綁定實(shí)例”不是一回事前者是集合后者是引用。5.5 V1.x小版本API漂移AttributeError和簽名變化現(xiàn)象腳本在A機(jī)器上正常拿到B機(jī)器上跑報(bào)AttributeErrormodule renderdoc has no attribute TextureSaveDestination或者SaveTexture調(diào)用報(bào)參數(shù)個(gè)數(shù)不對(duì)。原因RenderDoc V1.x不同小版本的Python接口有過調(diào)整TextureSaveDest枚舉名、SaveTexture簽名都有變化直接從網(wǎng)上復(fù)制的腳本很難跨版本通用。解決腳本開頭做一個(gè)自適應(yīng)層。用一個(gè)helper函數(shù)統(tǒng)一封裝TextureSave的創(chuàng)建和保存調(diào)用內(nèi)部用getattr做兜底def make_texture_save(): if hasattr(rd, TextureSave): return rd.TextureSave() # 某些舊版本里的保存參數(shù)結(jié)構(gòu)不同這里做兜底 return rd.TextureSave() def save_texture(controller, resource_id, out_path, opts): try: return controller.SaveTexture(resource_id, out_path, opts) except TypeError: # 舊版本簽名SaveTexture(resource_id, opts) return controller.SaveTexture(resource_id, opts)寫完之后在目標(biāo)機(jī)器上跑一條導(dǎo)出觀察日志確認(rèn)走的是哪條分支不要裸調(diào)官方文檔API。6. 讓批量導(dǎo)出融入日常流程的一個(gè)驗(yàn)證技巧6.1 用像素統(tǒng)計(jì)把“導(dǎo)錯(cuò)了”變成“可檢查”批量導(dǎo)出之后最怕的是文件數(shù)量對(duì)了、文件也能打開但內(nèi)容不對(duì)。肉眼一張張看幾十張圖看到第三張就開始走神。我習(xí)慣在導(dǎo)出完跑一個(gè)像素統(tǒng)計(jì)腳本把每張PNG的均值、最大透明值、顏色通道比例打印出來。數(shù)值異常的紋理一眼就能看出來比如法線紋理的藍(lán)色通道均值通常接近0.5如果統(tǒng)計(jì)出來是0.98說明通道順序錯(cuò)了。from PIL import Image import numpy as np def analyze_png(path: str): img Image.open(path).convert(RGBA) arr np.asarray(img, dtypenp.float32) # 輸出RGB均值與Alpha極值用于快速判斷通道異常 rgb_mean arr[..., :3].mean(axis(0, 1)) alpha_max arr[..., 3].max() print(f{path} RGB均值: {rgb_mean.round(3)} Alpha最大: {alpha_max})跑一遍分析輸出的數(shù)值是否合理比肉眼掃描靠譜得多。RGB均值里R、G、B三者比例異常往往就是通道順序翻車或者格式轉(zhuǎn)換出了問題。Alpha最大值為0的紋理如果它本該是半透明貼圖就得回頭查導(dǎo)出設(shè)置。6.2 用幀間diff做回歸驗(yàn)證批量導(dǎo)出的另一個(gè)高頻用途是回歸改了一個(gè)渲染參數(shù)把修改前后的兩幀分別導(dǎo)出紋理然后做diff確認(rèn)只有預(yù)期的紋理變化其他紋理像素不應(yīng)該有絲毫差異。全圖逐一對(duì)比是體力活但像素diff只要幾行def compare_frames(png_a: str, png_b: str) - float: img_a np.asarray(Image.open(png_a).convert(RGBA), dtypenp.float32) img_b np.asarray(Image.open(png_b).convert(RGBA), dtypenp.float32) if img_a.shape ! img_b.shape: return -1.0 # 尺寸不一致直接標(biāo)記為異常 diff np.abs(img_a - img_b) return diff.mean() # 平均差異0.0表示完全一致平均差異為0基本可以判定兩張圖完全一致大于0就打印出對(duì)應(yīng)的紋理名和diff值。把48張紋理的diff結(jié)果按數(shù)值排序先處理差異大的差異為0的直接跳過這個(gè)習(xí)慣幫我省了大量核對(duì)時(shí)間。我現(xiàn)在每調(diào)完一個(gè)渲染參數(shù)都會(huì)順手跑一次批量導(dǎo)出加diff確認(rèn)沒有改壞相鄰效果。這套流程用了很久出問題的次數(shù)明顯少了希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取