:RenderTarget與SceneCapture的坑與解法)
很多做UE4開發(fā)的朋友應(yīng)該都有過這樣的經(jīng)歷辛辛苦苦搭好場景最后卡在了“怎么把畫面干凈地取出來”這一步。無論是做分屏、做小地圖、做屏幕后處理、做遠程像素流還是給外部程序提供視頻源本質(zhì)上都在做同一件事——管理RenderSource渲染源。這篇文章我不會講一堆空理論而是把我在實際項目里用UE4處理渲染源時踩過的坑、測過的參數(shù)、總結(jié)出來的方法原原本本寫出來。適合剛開始接觸渲染目標(biāo)RenderTarget和場景捕獲SceneCapture的開發(fā)者也適合那些已經(jīng)試過基本用法但總感覺“哪里不對”的朋友。1. 渲染源的本質(zhì)與常見分類先搞清楚你要抓什么叫1.1 我的理解渲染源其實是“像素的供給鏈路”很多人第一次接觸RenderSource容易把它理解成“一張圖片”或者“一個相機”但實際用下來你會發(fā)現(xiàn)它是一條完整的像素供給鏈路場景中的物體經(jīng)過相機視角、光照計算、后處理材質(zhì)最終輸出到一個目標(biāo)載體上。目標(biāo)載體可以是屏幕、一張RenderTarget貼圖、一份外部視頻流也可以是下一階段的輸入數(shù)據(jù)。我在做項目的時候習(xí)慣把渲染源拆成三段來看第一段是“誰來產(chǎn)生畫面”比如主場景相機或者獨立的SceneCapture相機第二段是“畫面的格式和精度”比如分辨率、像素格式、是否帶后期效果第三段是“像素往哪里去”比如寫入RenderTarget、上傳到網(wǎng)絡(luò)流、保存成文件。只要這三段想清楚了項目里80%的渲染源問題其實都能提前規(guī)避。1.2 UE4開發(fā)中常用的四種渲染源從實際開發(fā)看UE4里的渲染源基本可以分成下面四種我整理了一張對比表渲染源類型典型用法優(yōu)點常見坑主視口渲染Viewport正常的游戲畫面、像素流送主畫面直接可見、性能最優(yōu)難以獨立控制分辨率與相機參數(shù)場景捕獲Scene Capture小地圖、監(jiān)控畫面、后視鏡、多視角切換可自定義相機視角不影響主畫面每路捕獲都有額外Draw Call容易成倍增加開銷渲染目標(biāo)RenderTarget材質(zhì)輸入、UI圖標(biāo)、動態(tài)紋理、離屏渲染可被任意材質(zhì)采樣靈活性強尺寸和格式設(shè)置不當(dāng)畫質(zhì)發(fā)虛或內(nèi)存暴漲像素流送源Pixel Streaming Source遠程瀏覽器觀看、多路畫面分發(fā)不需要額外開發(fā)播放器UE4原生鏈路編碼延遲與畫面同步問題需要額外處理這四種并非互斥我身邊很多同事會把它們組合起來用。比如做“監(jiān)控墻”類的項目往往先用SceneCapture把多路視角渲染到多個RenderTarget上再把RenderTarget匯總到一個大畫布里最后用像素流推送出去。所以不要只看某一個技術(shù)點要把它當(dāng)成一條鏈路來設(shè)計。1.3 選型邏輯不是越高級越好很多新手在沒有明確需求時會直接上最高分辨率、最大尺寸、最全后處理效果結(jié)果項目跑到一半發(fā)現(xiàn)GPU扛不住幀率只有十幾再回頭一處處砍效果非常折騰。我做選型的時候一般只問三個問題這個畫面要給誰看能接受多大的延遲同一幀內(nèi)需要多少路渲染源同時更新如果只是給小地圖用256x256甚至128x128就夠了沒必要為了“清晰”上2048。如果是要給AI識別用那么優(yōu)先考慮SceneColor而不是FinalColor因為后處理會改變真實光照信息。如果是給大屏拼接墻做畫面同步分辨率要夠但同時必須控制捕獲組件的數(shù)量。選型這件事沒有萬能答案關(guān)鍵是讓每個渲染源都剛好滿足業(yè)務(wù)需求而不是追求參數(shù)上的好看。2. 核心細節(jié)拆解RenderTarget與SceneCapture的實操要點2.1 RenderTarget最常用也最容易翻車的渲染源RenderTarget在UE4里叫TextureRenderTarget2D藍圖和C都能創(chuàng)建。它的核心作用是把渲染結(jié)果先“?!痹谝粡圙PU貼圖上方便后續(xù)讀取、保存或者送進材質(zhì)節(jié)點。我見過太多的翻車現(xiàn)場十有八九都是因為尺寸和格式?jīng)]選對。以藍圖操作為例創(chuàng)建RenderTarget時默認(rèn)會給你一個256x256的尺寸格式是RTF_RGBA8。這個組合用來做臨時預(yù)覽沒問題但如果要拿去做UI背景或者視頻輸出就會明顯發(fā)虛。我的建議是使用RTF_RGBA8時像素密度至少保證屏幕上每UI英寸對應(yīng)2到3倍物理像素如果畫面里需要保留高動態(tài)范圍細節(jié)比如燈管、太陽光、爆炸光暈?zāi)蔷透某蒖TF_RGBA16F讓亮部信息不容易被削平。別小看格式這個選項我測試過同一場景用RGBA8能明顯看到高光一片死白換成RGBA16F之后層次感就出來了。另外RenderTarget的更新方式也特別容易踩坑。如果你把RT當(dāng)作“貼圖”直接用在材質(zhì)上那么它默認(rèn)是會隨場景實時更新的但如果你的RenderTarget只是作為某種離線輸出比如保存成全屏截圖那就沒必要每幀都刷新不然等于白白浪費GPU帶寬。我常用的做法是在藍圖里手動控制Update打開某個開關(guān)時才重新捕獲用完之后立即關(guān)掉性能能省一大截。2.2 SceneCaptureComponent2D的正確打開方式場景捕獲組件是UE4里最靈活的渲染源工具我經(jīng)常把它形容成“多養(yǎng)了一雙眼睛”。它在場景中放置一個相機把看到的畫面寫入指定RenderTarget。聽起來簡單但里面有幾個細節(jié)會直接影響最終效果必須單獨拆開說。第一個細節(jié)是CaptureSource的選擇。打開SceneCapture2D組件你會看到CaptureSource下拉框里面最常見的選項是SceneColor和FinalColor。SceneColor是色調(diào)映射之前的那層畫面保留了更多的原始光照信息適合做一些檢測、摳像和算法處理FinalColor是經(jīng)過后期處理和色調(diào)映射的最終畫面適合給人看或者做UI背景。我之前為某個模擬項目做儀表盤畫面時一開始用了FinalColor結(jié)果發(fā)現(xiàn)儀表盤上的光暈被Bloom效果擴大了數(shù)值讀數(shù)反而看不清后來切到SceneColor才恢復(fù)正常。記住這個原則給人看用FinalColor給機器用SceneColor。第二個細節(jié)是TextureTarget的綁定。如果創(chuàng)建了SceneCapture2D組件卻不綁定TextureTarget它的捕獲結(jié)果根本沒有地方可以落盤拍一百幀也是白拍。正確做法是先在內(nèi)容瀏覽器里創(chuàng)建RenderTarget2D指定好尺寸然后拖到組件的TextureTarget槽位上。這個綁定關(guān)系一旦建立組件每次捕獲都會覆蓋寫進這張RT。需要注意RT的尺寸和CaptureSource的格式?jīng)Q定了最終畫面的精度和控制開銷如果RT尺寸和最終顯示尺寸差太多會出現(xiàn)明顯的“拉伸感”或細節(jié)丟失。第三個細節(jié)是更新頻率。默認(rèn)情況下SceneCapture2D每一幀都會觸發(fā)一次場景捕獲如果只是做監(jiān)控畫面并且主畫面有30FPS那問題不大但如果場景里放了四五路捕獲每一幀都重復(fù)繪制GPU開銷會成倍增長。我在實際項目中總結(jié)出的經(jīng)驗是非實時性畫面設(shè)置成CaptureEveryFramefalse然后根據(jù)具體業(yè)務(wù)用定時器或者藍圖節(jié)點手動觸發(fā)捕獲比如每秒更新10次。這樣對性能友好得多而且肉眼幾乎分辨不出差別。2.3 從渲染源到材質(zhì)、UI和網(wǎng)絡(luò)流一條通用輸出路徑渲染源最終往往要“變現(xiàn)”也就是變成用戶看得到的內(nèi)容。UE4里最常見的輸出路徑大概有三條給材質(zhì)當(dāng)貼圖、給UI當(dāng)背景、給像素流當(dāng)畫面源。給材質(zhì)當(dāng)貼圖是最直接的方式把RT拖進材質(zhì)的Texture Sample節(jié)點選擇好過濾器一般用線性過濾然后連接到自發(fā)光或者基礎(chǔ)顏色。這條路徑要注意RT在材質(zhì)中被采樣時的UV坐標(biāo)建議用ScreenPosition或者自定義的Box UV節(jié)點方便控制畫面鋪滿區(qū)域。給UI當(dāng)背景則是把RT包裝成Slate Brush或者直接創(chuàng)建UImage控件動態(tài)設(shè)置Brush的資源為RT。這里有一個隱藏問題如果你在UI里顯示RT同時又在同一個畫面上疊加其他動態(tài)控件布局更新時會觸發(fā)重新合成性能容易波動。像素流這層要稍微復(fù)雜一點。像素流默認(rèn)捕獲的是主視口畫面如果你希望推送的是某一組渲染源的合成畫面而不是默認(rèn)游戲畫面就需要在UI層把RT繪制到畫布上再保證該畫布始終在主視口可見?;蛘吒鼜?fù)雜一點用像素流的多路信令機制把不同RenderTarget綁定到不同編碼器輸出通道。這部分我在第三節(jié)的實操項目里會詳細展開。3. 從零搭建一個多路渲染源系統(tǒng)某跨平臺仿真系統(tǒng)的實操記錄3.1 需求背景與總體方案之前我參與過一個模擬項目X目標(biāo)是做一套跨平臺的遠程仿真訓(xùn)練平臺用戶可以在瀏覽器端看到車輛艙內(nèi)視角、外部環(huán)視視角和一塊儀表盤信息顯示同時要求畫面延遲盡可能低。說白了這就是一個典型的多路渲染源需求。因為需要同時輸出多路畫面常規(guī)的單一主視口方案根本滿足不了所以我設(shè)計了以下渲染鏈路三路SceneCapture2D分別對應(yīng)左前方視角、正前方視角和儀表盤視角儀表盤視角單獨使用一個128x64的迷你RT用來推送關(guān)鍵讀數(shù)信息另外再用一個較大的RenderTarget1920x1080作為主畫面合并層把所有獨立畫面合成到一個綜合視圖中。如此設(shè)計的好處是各個視角互不干擾而且后期如果業(yè)務(wù)方要求增加視角不需要改動主畫面邏輯??傮w方案確定后我梳理了一版參數(shù)表渲染源用途相機組件RT分辨率RT格式CaptureSource更新頻率主畫面合成主相機1920x1080RTF_RGBA8FinalColor每幀左前方視角SceneCapture2D_A960x540RTF_RGBA8FinalColor每幀正前方視角SceneCapture2D_B960x540RTF_RGBA8FinalColor每幀儀表盤讀數(shù)SceneCapture2D_C512x256RTF_RGBA16FSceneColor10FPS參數(shù)表做好之后項目就變得清晰很多哪些渲染源貴、哪些渲染源便宜哪些可以低頻刷新一目了然。3.2 場景搭建與渲染源配置場景部分其實沒有太多懸念我用的是UE4默認(rèn)的第三人稱模板改的重點是渲染源配置的實操細節(jié)。我新建了三張RenderTargetRT_MainView、RT_LeftView、RT_DashView。在場景里拖了三個SceneCapture2D組件分別放在車廂前擋風(fēng)玻璃、車頭左前側(cè)和儀表盤正上方。每個組件的設(shè)置有一點必須強調(diào)如果你希望捕獲出來的畫面完全由這個相機決定必須把捕捉模式設(shè)為“無影響”Capture Source不繼承主相機任何效果同時注意后處理體積是否會把當(dāng)前場景的霧效、泛光帶進來。我測試的時候發(fā)現(xiàn)即使CaptureSource選擇的是SceneColor場景里如果有ExponentialHeightFog最終RT上仍然會有霧的干擾。后來我在捕獲相機附近放了一個忽略霧效的后處理體積并把體積的混合權(quán)重設(shè)為0問題才解決。組件對應(yīng)的TextureTarget綁定那張RT之后我還做了一層額外的“保險開關(guān)”用一個字節(jié)類型的布爾變量控制每組SceneCapture的啟用狀態(tài)。這樣程序運行期間如果需要臨時禁用某一路渲染源直接斷開布爾即可不用去藍圖里找組件引用。這個小設(shè)計在線上調(diào)試時給我省了大量時間。3.3 像素流鏈路與幀同步多路畫面必須考慮“同幀”問題像素流推送多路渲染源最惱人的問題不是畫質(zhì)而是“不同步”。我之前第一次上線測試時用戶反饋左側(cè)視角和主畫面的動作對不上畫面差了好幾幀。排查了很久最后發(fā)現(xiàn)是各路RT的更新時間不在同一幀內(nèi)導(dǎo)致的。UE4的渲染線程和游戲線程并不是完全同步的如果某一幀內(nèi)觸發(fā)了多次捕獲引擎內(nèi)部的執(zhí)行順序會受到物理、動畫、粒子等因素影響導(dǎo)致捕獲到的畫面時刻不一致。針對這個問題我的處理辦法是分幀更新主畫面保持每幀捕獲左前方視角和正前方視角相隔一幀交替捕獲儀表盤因為本身就是10FPS的低頻畫面單獨用Timer驅(qū)動。這樣每個渲染源捕獲時都能拿到一個相對“干凈”的幀上下文不會出現(xiàn)同一個物理時刻被兩路渲染源反復(fù)競爭的情況。實際運行下來三路畫面的時間偏差從接近100ms降到了20ms以內(nèi)體感上基本察覺不到。3.4 性能預(yù)算實測多路渲染源的GPU開銷規(guī)律為了把多路渲染源的性能賬算清楚我在項目里做了一個簡單的性能統(tǒng)計分別測試“只有主畫面”“主畫面一路SceneCapture”“主畫面三路SceneCapture”和“三路SceneCapture分別使用960x540與1920x1080”四種組合記錄GPU渲染線程耗時和幀率。結(jié)果如下表場景組合GPU線程耗時(ms)幀率(FPS)額外RT內(nèi)存(MB)僅主畫面6.2600主畫面1路SceneCapture(960x540)7.8608.2主畫面3路SceneCapture(960x540)10.95524.6主畫面3路SceneCapture(1920x1080)15.44288.4從數(shù)據(jù)可以很清楚地看到分辨率對開銷的影響比路數(shù)更夸張。960x540的RT只有1920x1080的四分之一像素但三路之后反而比三路1080p省了將近一半GPU時間。所以如果你的業(yè)務(wù)允許盡量降低非關(guān)鍵畫面的分辨率而不是無限壓縮路數(shù)。壓縮路數(shù)會影響功能完整性但降低分辨率通常人眼感知不明顯尤其是在小窗顯示時這個性價比非常高。4. 常見問題與排查技巧實錄4.1 渲染源黑屏先分清是“沒拍”還是“沒顯示”遇到黑屏我會先做一個劃分打開材質(zhì)預(yù)覽或者UI預(yù)覽看看RT里有沒有圖像。如果RT里是黑的說明捕獲環(huán)節(jié)出問題如果RT里有圖像但屏幕上看不到那說明是顯示鏈路的問題。捕獲環(huán)節(jié)的黑屏原因我遇到過幾種一是SceneCapture組件被誤刪或者Actor沒有激活二是CaptureSource選了FinalColor但場景里沒有正確的后處理體積三是RT被其他材質(zhì)完全覆蓋導(dǎo)致透明度失效。顯示環(huán)節(jié)的黑屏則多半是UI中Image的Brush資源沒有正確指向RT或者布局權(quán)重被其他控件壓住了。排查的時候不要一上來就懷疑引擎出Bug我第一步永遠是加一個“診斷預(yù)覽”UI層把所有RT臨時平鋪在屏幕角落這樣哪一路黑屏一眼就能看出來。4.2 畫面發(fā)虛和閃爍往往敗給一個采樣參數(shù)RT畫面發(fā)虛的原因大多是分辨率不足或者被拉伸但有時也和一個不起眼的參數(shù)有關(guān)過濾。在材質(zhì)中采樣RT時如果你的Filter設(shè)置成Nearest那么在分辨率不夠的情況下畫面會出現(xiàn)明顯鋸齒如果設(shè)成Linear畫面柔和但可能偏模糊如果設(shè)成Mips并開啟了自動生成MipMap那么RT更新時會觸發(fā)Mip重新計算產(chǎn)生肉眼可見的閃爍跳變。我的處理習(xí)慣是顯示類RT一律使用Linear過濾并關(guān)閉自動MipMap生成分析和摳像類RT使用Nearest或Point過濾保持邊緣銳利。閃爍問題如果出現(xiàn)在多路RT疊加場景還要檢查這幾張RT是不是在同一個幀內(nèi)被多次更新如果確實需要同步更新建議把更新邏輯放到同一Event里并手動調(diào)用一次FlushRenderingCommands確保所有寫入完成后再讀取。4.3 內(nèi)存暴漲小心每幀新建RenderTarget很多剛接觸渲染源的朋友會把“創(chuàng)建RT”這件事放在Tick里。每一幀創(chuàng)建一個新的TextureRenderTarget2D然后等用完就丟棄。這在編輯器環(huán)境里看著沒問題因為垃圾回收會兜底但在打包后的運行時環(huán)境里GPU資源并不會立刻釋放內(nèi)存會像滾雪球一樣越來越大。我見過一個案例某些項目在連續(xù)運行半個小時后直接從8G內(nèi)存漲到12G最后被系統(tǒng)強制殺掉罪魁禍?zhǔn)拙褪荰ick里不斷建RT。正確的做法是把RT的創(chuàng)建放在BeginPlay或者初始化函數(shù)里之后整個生命周期只復(fù)用這一張RT。如果確實需要不同分辨率的渲染源可以預(yù)先創(chuàng)建兩張固定規(guī)格的RT而不是動態(tài)生成。另外如果使用C開發(fā)CreateRenderTarget2D之后記得調(diào)用GameThread_GetRenderTargetResource()獲取資源引用不要每幀重復(fù)獲取。性能優(yōu)化這件事往往就是這些“看起來沒多大影響”的細節(jié)累積出來的。4.4 多相機并發(fā)捕獲注意渲染線程的同步點當(dāng)項目里同時存在多個SceneCapture2D時它們本質(zhì)上是多個“渲染命令”在渲染線程排隊執(zhí)行。由于UE4的渲染命令是異步的如果你在游戲線程中一個接一個地觸發(fā)更新然后立刻讀取RT數(shù)據(jù)可能會讀到“上一幀”的舊數(shù)據(jù)。我踩過這個坑某次做一個圖像識別Demo需要把識別結(jié)果覆蓋到畫面中我直接調(diào)用了CaptureScene()然后立刻讀取RT像素結(jié)果經(jīng)常拿到舊幀畫面導(dǎo)致識別結(jié)果和畫面內(nèi)容錯位。后來改為在捕獲完成事件里讀取像素并加上一個FrameIndex校驗變量只有確認(rèn)當(dāng)前幀的捕獲編號比上一幀大時才更新識別結(jié)果問題立刻消失。簡單說涉及多相機并發(fā)捕獲時一定要在自己代碼里制造“同步信號”不要依賴引擎默認(rèn)的幀序。5. 經(jīng)驗總結(jié)與擴展建議5.1 我踩過最深的三個坑第一個坑是無腦給所有場景加后處理。后處理效果在單個主相機上很出彩但到了SceneCapture里可能完全是災(zāi)難。Bloom會把儀表盤字符糊成光斑Vignette會讓RT邊緣變暗景深更會讓非焦點區(qū)域完全不清晰。凡是用于算法、識別、監(jiān)控類渲染源我現(xiàn)在的做法都是新建一個“純凈后處理體積”只保留基礎(chǔ)光照和色調(diào)映射。第二個坑是忽略RT的Gamma校正。有些開發(fā)者把RT輸出后直接存成PNG發(fā)現(xiàn)顏色泛灰、偏亮原因是沒有處理Gamma空間。UE4的Default彩色RT一般存儲在sRGB空間保存和處理時要多做一步轉(zhuǎn)Linear或者轉(zhuǎn)sRGB的操作。這個問題如果用藍圖處理很難察覺因為UI顯示會自動校正。第三個坑是給儀表盤這類低頻畫面設(shè)置了過高的捕獲頻率。人眼對儀表盤數(shù)字快速跳變并不敏感30FPS和10FPS的觀感差距極小但GPU開銷差距很大。后來我把儀表盤從30FPS降到10FPS整套系統(tǒng)的幀率反而更穩(wěn)定。5.2 后續(xù)可以擴展的方向渲染源管理這套思路除了傳統(tǒng)的小地圖、監(jiān)控墻、像素流尤其適合往“合成數(shù)據(jù)生成”和“AI輔助訓(xùn)練”兩個方向擴展。比如利用SceneCapture配合不同視角的RT快速生成幾千張帶標(biāo)記的合成圖像作為訓(xùn)練集或者把RT輸出接入神經(jīng)網(wǎng)絡(luò)推理實現(xiàn)基于真實場景的飛行器檢測。因為這些方向?qū)Ξ嬅娓袷胶筒东@時機的敏感度極高往往是渲染源管理能力發(fā)揮最大價值的地方。5.3 小技巧用后處理材質(zhì)做渲染源診斷最后分享一個調(diào)試?yán)鹘oRT專門掛一個“診斷后處理材質(zhì)”讓所有RT統(tǒng)一渲染成九宮格網(wǎng)格并在畫面四角打上不同顏色的標(biāo)記。這樣在多路渲染源項目里一旦某一塊畫面錯亂或延遲就能立刻判斷是哪個通道出了問題。這個辦法比查日志高效太多我已經(jīng)在新項目里把它做成默認(rèn)調(diào)試組件了。終究還是那句話渲染源這類底層的活兒掌握好原理和排查套路剩下就是不斷拿真實項目喂經(jīng)驗踩過一次就長記性了。