
1. 項(xiàng)目概述在Unity中處理高質(zhì)量文本渲染TextMesh ProTMP是繞不開的核心工具而它的靈魂在于其基于Signed Distance FieldSDF有向距離場(chǎng)的著色器。這個(gè)系列文章已經(jīng)來到了第七篇我們繼續(xù)深挖TMP_SDF著色器中那些看似不起眼實(shí)則至關(guān)重要的宏定義。上一期我們分析了OUTLINE_ON等宏今天我們把目光聚焦在UNDERLAY_ON、UNDERLAY_INNER以及Unity版本相關(guān)的宏上。這些宏控制著文本的“底影”效果和不同Unity版本下的裁剪兼容性是讓文字在不同背景上清晰可讀、保證項(xiàng)目跨版本穩(wěn)定的關(guān)鍵。無論你是想定制自己的TMP材質(zhì)還是遇到了“升級(jí)Unity后文字顯示異常”的坑理解這部分內(nèi)容都能讓你從“知其然”進(jìn)階到“知其所以然”。2. 核心宏功能解析與設(shè)計(jì)思路2.1 UNDERLAY宏為文字添加“靈魂底影”在TMP_SDF著色器中UNDERLAY_ON和UNDERLAY_INNER這兩個(gè)宏共同實(shí)現(xiàn)了一種我稱之為“文字底影”的效果。這可不是簡(jiǎn)單的投影Drop Shadow而是一種更靈活、常用于增強(qiáng)文字可讀性的技術(shù)。為什么需要UNDERLAY想象一下把白色文字放在一張復(fù)雜、顏色多變的圖片上文字很容易被背景“吃掉”導(dǎo)致難以辨認(rèn)。直接給文字加粗或加描邊有時(shí)會(huì)顯得笨重。這時(shí)一個(gè)柔和、顏色可調(diào)的底影比如半透明的深色就能在文字和背景之間建立一個(gè)視覺緩沖層顯著提升文字的可讀性同時(shí)保持設(shè)計(jì)上的輕盈感。這在游戲UI、字幕、或者任何文字需要疊加在動(dòng)態(tài)背景上的場(chǎng)景中極其有用。兩種模式的設(shè)計(jì)考量UNDERLAY_ON外部底影這是最常用的模式。底影渲染在文字主體包括描邊的外部。你可以把它理解為給文字加了一個(gè)向外擴(kuò)散的、柔和的彩色光暈。它不改變文字本身的形狀和大小只是在外圍增加了一層視覺襯托。UNDERLAY_INNER內(nèi)部底影這個(gè)模式相對(duì)特殊底影渲染在文字主體內(nèi)部。它產(chǎn)生的效果更像是讓文字內(nèi)部帶有一種“內(nèi)發(fā)光”或者“內(nèi)部襯底”的感覺。這種效果通常用于創(chuàng)造一些風(fēng)格化的文字比如霓虹燈管效果亮邊內(nèi)部有暗色襯底或者讓文字看起來有凹陷感。Shader中通過#pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER來聲明這兩個(gè)互斥的特性開關(guān)。這意味著在同一個(gè)材質(zhì)實(shí)例上你只能啟用其中一種底影模式或者都不啟用。這種設(shè)計(jì)保證了Shader變體Variant的可管理性避免生成不必要的Shader組合。2.2 版本兼容宏應(yīng)對(duì)Unity的API變遷另一個(gè)至關(guān)重要的部分是Unity版本相關(guān)的條件編譯主要體現(xiàn)在像素著色器PixShader末尾的矩形遮罩ClipRect處理上。代碼中清晰地出現(xiàn)了#if UNITY_VERSION 530這樣的判斷。背后的痛點(diǎn)Unity 5.3版本是一個(gè)重要的分水嶺。在此版本之前Unity UI系統(tǒng)uGUI的矩形遮罩功能在Shader中的實(shí)現(xiàn)方式與之后不同。具體來說在5.2及更早版本中裁剪是通過一個(gè)名為_UseClipRect的屬性和一套特定的計(jì)算來完成的。而從Unity 5.3開始Unity引入了一個(gè)更通用、更強(qiáng)大的內(nèi)置函數(shù)UnityGet2DClipping或其相關(guān)機(jī)制來處理UI元素的裁剪并且支持了軟邊Softness遮罩。TMP的應(yīng)對(duì)策略TMP作為一個(gè)需要兼容廣泛Unity版本從5.x到最新的LTS版本的資產(chǎn)包其著色器必須能適應(yīng)這種底層API的變化。它不能簡(jiǎn)單地只使用新API否則在舊項(xiàng)目中會(huì)報(bào)錯(cuò)或功能失效也不能只使用舊方法否則無法享受新版本帶來的功能如軟遮罩。因此條件編譯成為了唯一的選擇。注意這里使用的UNITY_VERSION是一個(gè)由Unity Shader編譯器預(yù)定義的宏其值對(duì)應(yīng)著Unity的主版本號(hào)如530代表5.3。通過判斷這個(gè)值Shader可以在編譯時(shí)就決定為當(dāng)前Unity版本生成哪一段代碼從而確保運(yùn)行時(shí)不會(huì)調(diào)用不存在的函數(shù)或訪問不存在的屬性。這是保持Shader跨版本兼容性的標(biāo)準(zhǔn)做法。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)3.1 UNDERLAY效果的參數(shù)驅(qū)動(dòng)原理底影效果并非魔法它完全由一系列材質(zhì)參數(shù)控制并在頂點(diǎn)著色器VertShader和像素著色器PixShader中分步計(jì)算。理解這個(gè)數(shù)據(jù)流是自定義效果的基礎(chǔ)。控制參數(shù)Properties在Shader的Properties塊中與底影相關(guān)的參數(shù)有_UnderlayColor底影的顏色和透明度。_UnderlayOffsetX/Y底影在X和Y方向上的偏移量。這是實(shí)現(xiàn)“投影”方向感的關(guān)鍵。_UnderlayDilate底影的擴(kuò)張或收縮。正值向外擴(kuò)張負(fù)值向內(nèi)收縮。_UnderlaySoftness底影邊緣的柔和度。_ScaleRatioC一個(gè)縮放比率通常用于自動(dòng)調(diào)整底影參數(shù)與文字大小的比例關(guān)系保證在不同字體大小下效果一致。頂點(diǎn)著色器中的預(yù)處理在VertShader函數(shù)中當(dāng)檢測(cè)到啟用了任一UNDERLAY宏時(shí)會(huì)進(jìn)行以下關(guān)鍵計(jì)算獨(dú)立縮放計(jì)算底影的layerScale是基于文字主體的scale獨(dú)立計(jì)算的但會(huì)額外考慮_UnderlaySoftness的影響。這確保了底影的柔和度不會(huì)干擾文字本體的銳利度。layerScale / 1 ((_UnderlaySoftness * _ScaleRatioC) * layerScale);偏移量轉(zhuǎn)換_UnderlayOffsetX/Y是用戶直觀理解的“像素偏移”或“相對(duì)偏移”但在SDF空間中需要結(jié)合_GradientScale和字體紋理的_TextureWidth/Height將其轉(zhuǎn)換為正確的紋理坐標(biāo)偏移量layerOffset。這一步保證了無論屏幕分辨率如何底影的偏移視覺比例是穩(wěn)定的。數(shù)據(jù)傳遞計(jì)算好的layerScale和layerBias經(jīng)過UnderlayDilate調(diào)整后的閾值偏差被封裝進(jìn)output.underlayParam紋理坐標(biāo)偏移和原始透明度被封裝進(jìn)output.texcoord1傳遞給像素著色器。像素著色器中的合成在PixShader中根據(jù)不同的宏進(jìn)行最終的顏色混合UNDERLAY_ON這是一個(gè)標(biāo)準(zhǔn)的“外部疊加”操作。先采樣底影區(qū)域的SDF值d計(jì)算其可見性saturate(d - input.underlayParam.y)然后以(1 - c.a)為系數(shù)疊加到當(dāng)前顏色c上。(1 - c.a)是關(guān)鍵它意味著底影只會(huì)出現(xiàn)在文字本體和描邊透明的區(qū)域從而實(shí)現(xiàn)“底影在文字后面”的效果?;旌戏绞绞羌臃ú⑶翌A(yù)乘了底影顏色的Alpha_UnderlayColor.rgb * _UnderlayColor.a這是正確的透明顏色混合方式。UNDERLAY_INNER計(jì)算更巧妙一些。它先計(jì)算了文字本體包括描邊的可見性sd。然后采樣底影但這次用(1 - saturate(d - input.underlayParam.y))這意味著它取的是SDF圖形的內(nèi)部區(qū)域。最后將底影顏色與sd * (1 - c.a)相乘再疊加。這樣底影只出現(xiàn)在文字內(nèi)部且當(dāng)前像素未被填充的區(qū)域形成了“內(nèi)襯”效果。3.2 版本分支下的遮罩處理差異讓我們仔細(xì)對(duì)比一下Shader中矩形遮罩處理的兩套代碼Unity 5.2及之前舊路徑#if UNITY_VERSION 530 if (_UseClipRect) { half2 m saturate((_ClipRect.zw - _ClipRect.xy - abs(input.mask.xy)) * input.mask.zw); c * m.x * m.y; } #endif條件執(zhí)行它被包裹在一個(gè)if (_UseClipRect)判斷中。這意味著在舊版本中矩形遮罩可能是一個(gè)可選的特性需要顯式開啟。計(jì)算邏輯計(jì)算方式與新版類似都是通過abs(input.mask.xy)獲取當(dāng)前像素到裁剪矩形四邊的距離然后利用saturate和乘法得到一個(gè)0到1的遮罩系數(shù)m。input.mask.zw存儲(chǔ)了基于_MaskSoftnessX/Y和像素大小的軟化因子。Unity 5.3及之后新路徑#else half2 m saturate((_ClipRect.zw - _ClipRect.xy - abs(input.mask.xy)) * input.mask.zw); c * m.x * m.y; #endif無條件執(zhí)行去掉了if (_UseClipRect)判斷。這表明在新版本的UI系統(tǒng)中矩形裁剪是默認(rèn)且始終生效的機(jī)制對(duì)于UI元素_ClipRect可能由CanvasRenderer自動(dòng)設(shè)置。功能一致性盡管執(zhí)行條件不同但核心數(shù)學(xué)計(jì)算是完全一致的。這保證了視覺效果在版本遷移前后的一致性。實(shí)操心得當(dāng)你從Unity 5.2或更早版本升級(jí)一個(gè)使用了TMP的項(xiàng)目到新版本時(shí)如果發(fā)現(xiàn)UI文字裁剪出現(xiàn)問題檢查Shader中這段代碼的編譯路徑是否正確至關(guān)重要。有時(shí)Shader的編譯緩存可能導(dǎo)致舊版本的變體被錯(cuò)誤使用。一個(gè)可靠的解決方法是在Project面板中找到TMP的Shader文件如TMP_SDF-Mobile.shader右鍵選擇“Reimport”強(qiáng)制Unity使用當(dāng)前版本的Shader編譯器重新編譯它。4. 宏控制的完整渲染流程拆解4.1 從頂點(diǎn)到像素的數(shù)據(jù)流與宏開關(guān)要全局理解這些宏如何工作我們必須梳理一次完整的渲染數(shù)據(jù)流。當(dāng)我們?cè)诓馁|(zhì)上勾選“Underlay”選項(xiàng)時(shí)引擎和Shader編譯器會(huì)協(xié)同完成以下工作1. 材質(zhì)變體編譯當(dāng)你修改材質(zhì)并啟用Underlay時(shí)Unity的Shader編譯器會(huì)識(shí)別到#pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER。這會(huì)為當(dāng)前材質(zhì)生成一個(gè)特定的Shader變體。這個(gè)變體的代碼中UNDERLAY_ON宏被定義為1或true而UNDERLAY_INNER未定義。這意味著在后續(xù)的編譯中所有#if UNDERLAY_ON的代碼塊都會(huì)被包含而#if UNDERLAY_INNER的塊會(huì)被剔除。這保證了最終GPU執(zhí)行的代碼是最精簡(jiǎn)、只包含所需功能的。2. 頂點(diǎn)著色器階段在這個(gè)階段宏主要控制頂點(diǎn)輸出結(jié)構(gòu)體pixel_t的成員以及相應(yīng)的計(jì)算。如果任一UNDERLAY宏啟用結(jié)構(gòu)體中就會(huì)包含texcoord1和underlayParam這兩個(gè)字段。同時(shí)在VertShader函數(shù)內(nèi)部#if (UNDERLAY_ON | UNDERLAY_INNER)包圍的代碼塊會(huì)被激活計(jì)算層偏移layerOffset、層縮放layerScale和層偏差layerBias并將結(jié)果填充到輸出結(jié)構(gòu)體中。如果未啟用這些字段和計(jì)算代碼在編譯時(shí)就被移除頂點(diǎn)著色器輸出的數(shù)據(jù)量更小傳輸?shù)较袼刂鞯膸捳加靡哺佟?. 像素著色器階段這里是效果合成的最終舞臺(tái)宏控制著顏色混合的邏輯分支。首先計(jì)算文字本體顏色c。接著#ifdef OUTLINE_ON決定是否進(jìn)行描邊的顏色插值。然后#if UNDERLAY_ON和#if UNDERLAY_INNER根據(jù)編譯出的變體決定執(zhí)行哪一段底影混合代碼。這兩段代碼是互斥的不會(huì)同時(shí)存在。最后進(jìn)行矩形裁剪UNITY_VERSION宏決定使用哪套裁剪邏輯。在裁剪之后還有一個(gè)關(guān)鍵的步驟#if (UNDERLAY_ON | UNDERLAY_INNER) c * input.texcoord1.z;。這里的input.texcoord1.z存儲(chǔ)的是頂點(diǎn)輸入的原始顏色Alphainput.color.a。這一步至關(guān)重要它將頂點(diǎn)顏色透明度最終應(yīng)用到底影效果上。這意味著你可以通過修改頂點(diǎn)顏色例如在代碼中設(shè)置TMP_Text.color的Alpha來動(dòng)態(tài)控制整個(gè)文字包括底影的淡入淡出而底影顏色自身的Alpha_UnderlayColor.a只控制底影的固有透明度。4.2 參數(shù)聯(lián)動(dòng)與視覺調(diào)試技巧理解了流程我們來看看如何通過參數(shù)微調(diào)獲得理想效果。以下是一個(gè)參數(shù)影響關(guān)系的速查表參數(shù)主要影響視覺表現(xiàn)與ScaleRatioC的聯(lián)動(dòng)_UnderlayOffsetX/Y底影位置產(chǎn)生投影方向感。值為正時(shí)底影向右/下偏移。偏移量會(huì)乘以_ScaleRatioC因此當(dāng)字體縮放時(shí)底影偏移能保持相對(duì)比例。_UnderlayDilate底影大小正值使底影向外“膨脹”負(fù)值使其向內(nèi)“收縮”。直接影響layerBias的計(jì)算_ScaleRatioC會(huì)同步縮放此影響確保大小比例一致。_UnderlaySoftness底影邊緣硬度值越大從底影顏色到透明的過渡區(qū)域越寬邊緣越模糊。在計(jì)算layerScale時(shí)參與運(yùn)算_ScaleRatioC能保證軟度隨字體大小自適應(yīng)。_UnderlayColor底影顏色和強(qiáng)度RGB決定顏色Alpha決定該顏色層的最大不透明度。通常不直接聯(lián)動(dòng)但底影顏色的預(yù)乘RGB*A是正確混合的關(guān)鍵。調(diào)試技巧在Scene視圖或Game視圖調(diào)試時(shí)我習(xí)慣采用“分層剝離”法先關(guān)底影將Underlay Color的Alpha設(shè)為0或者直接禁用Underlay特性確保文字本體和描邊效果是你想要的。單獨(dú)調(diào)底影將文字本體顏色調(diào)成純色如白色描邊暫時(shí)關(guān)閉或調(diào)成對(duì)比色。然后慢慢調(diào)整底影的Offset、Dilate和Softness觀察底影形狀的變化是否符合預(yù)期。調(diào)整顏色與混合最后調(diào)整Underlay Color注意其Alpha值控制的是底影層的“濃度”而頂點(diǎn)顏色Alpha或材質(zhì)整體透明度控制的是整個(gè)文字含底影的“可見度”。理解這個(gè)區(qū)別能避免很多混合錯(cuò)誤。檢查版本差異如果懷疑裁剪問題可以嘗試在材質(zhì)上調(diào)整Mask Softness參數(shù)。如果在舊版Unity中軟遮罩無效而在新版中有效那很可能就是Shader版本編譯路徑的問題。5. 常見問題與排查技巧實(shí)錄在實(shí)際項(xiàng)目開發(fā)中與TMP_SDF宏相關(guān)的問題雖然不常發(fā)生但一旦出現(xiàn)往往令人困惑。以下是我總結(jié)的幾個(gè)典型問題及其排查思路。5.1 問題一啟用了Underlay但屏幕上什么都看不到排查步驟檢查顏色Alpha這是最常見的原因。首先確認(rèn)_UnderlayColor的Alpha值是否大于0。一個(gè)常見的疏忽是只調(diào)整了RGB忘了調(diào)A。檢查偏移和擴(kuò)張_UnderlayOffsetX/Y或_UnderlayDilate的值可能設(shè)置得過于極端導(dǎo)致底影完全偏移到了可視區(qū)域之外或者過度收縮以至于看不見。嘗試將它們重置為0再慢慢調(diào)整。檢查渲染隊(duì)列和混合模式TMP_SDF-Mobile Overlay.shader的渲染隊(duì)列Queue是“Overlay”混合模式是“Blend One OneMinusSrcAlpha”。如果你的場(chǎng)景中有其他Overlay物體或者混合設(shè)置被意外修改可能會(huì)造成遮擋或混合錯(cuò)誤。確保你的材質(zhì)球使用的是正確的Shader并且沒有在代碼中被動(dòng)態(tài)修改渲染狀態(tài)。檢查頂點(diǎn)顏色Alpha記住最終輸出顏色會(huì)乘以input.texcoord1.z即頂點(diǎn)顏色Alpha。如果你通過腳本動(dòng)態(tài)修改了TMP文本的color屬性并降低了Alpha那么底影的可見度也會(huì)同步降低。在調(diào)試時(shí)可以嘗試在代碼中暫時(shí)將color設(shè)置為Color.white。5.2 問題二從Unity舊版本升級(jí)后文字裁剪Mask異常問題現(xiàn)象升級(jí)Unity版本例如從5.2升級(jí)到2018.x或更高后原本使用RectMask2D或ScrollRect裁剪的TMP文本要么裁剪完全失效要么裁剪邊緣變得異常尖銳失去了軟邊效果。根本原因正如前文所述這是因?yàn)镾hader使用了錯(cuò)誤的條件編譯路徑??赡艿脑蛴蠸hader緩存Unity沒有為升級(jí)后的項(xiàng)目重新編譯TMP的Shader變體。材質(zhì)球引用項(xiàng)目中的材質(zhì)球可能還引用著舊版本編譯的Shader二進(jìn)制數(shù)據(jù)。解決方案重新導(dǎo)入Shader在Project窗口中導(dǎo)航到TextMesh Pro/Resources/Shaders文件夾找到你正在使用的SDF Shader文件例如TMP_SDF-Mobile.shader右鍵點(diǎn)擊并選擇“Reimport”。這能強(qiáng)制Unity使用新版本的編譯器重新編譯該Shader。更新材質(zhì)球如果問題依舊可能需要手動(dòng)更新材質(zhì)球。選中出問題的材質(zhì)球在Inspector面板頂部檢查Shader路徑是否正確指向TextMesh Pro/...。有時(shí)可以嘗試將Shader臨時(shí)切換為另一個(gè)再切換回來以刷新其關(guān)聯(lián)的變體。檢查RectMask2D組件確保RectMask2D組件正常工作并且其“Softness”參數(shù)如果適用被正確設(shè)置。新版Unity中軟遮罩的計(jì)算可能更加依賴于CanvasRenderer傳遞的數(shù)據(jù)。5.3 問題三Underlay Inner效果不符合預(yù)期看起來像外部發(fā)光排查思路UNDERLAY_INNER的效果高度依賴于_UnderlayDilate和_UnderlayOffset參數(shù)。Dilate為正值這會(huì)使底影的SDF閾值向外部擴(kuò)張導(dǎo)致底影區(qū)域變大更容易溢出到文字輪廓之外從而看起來像外部效果。嘗試將_UnderlayDilate設(shè)置為負(fù)值如-0.2到-0.5讓底影向文字內(nèi)部收縮。Offset值過大即使Dilate為負(fù)過大的Offset也可能將底影“推”出文字邊界。嘗試先將Offset歸零調(diào)整好Dilate獲得清晰的內(nèi)襯效果后再微調(diào)Offset來制造內(nèi)部的光源方向感。理解混合公式回顧UNDERLAY_INNER的像素著色器代碼它用(1 - saturate(d - input.underlayParam.y))來采樣底影內(nèi)部并與文字本體的可見區(qū)域sd相乘。這意味著底影只出現(xiàn)在“文字本體可見”與“底影內(nèi)部區(qū)域”的交集部分。如果文字描邊很粗sd區(qū)域文字描邊很大而底影內(nèi)部區(qū)域由負(fù)的Dilate定義很小那么最終顯示的內(nèi)襯區(qū)域也會(huì)很小。你需要協(xié)同調(diào)整描邊寬度_OutlineWidth和底影的Dilate來獲得最佳效果。5.4 性能考量與變體控制過多或不必要的Shader變體會(huì)增加包體大小和運(yùn)行時(shí)內(nèi)存占用并可能影響GPU的渲染批次合并。TMP_SDF著色器通過shader_feature來管理變體這是一種相對(duì)高效的方式因?yàn)樗痪幾g項(xiàng)目中實(shí)際材質(zhì)用到的特性組合。最佳實(shí)踐按需創(chuàng)建材質(zhì)避免為每一個(gè)微調(diào)了Underlay參數(shù)的文字都創(chuàng)建新的材質(zhì)實(shí)例。盡量復(fù)用材質(zhì)。對(duì)于需要?jiǎng)討B(tài)開關(guān)Underlay的情況可以考慮使用MaterialPropertyBlock來修改參數(shù)而不是替換整個(gè)材質(zhì)。清理未使用的材質(zhì)定期檢查項(xiàng)目的材質(zhì)資源刪除那些在場(chǎng)景中不再使用的、或參數(shù)重復(fù)的TMP材質(zhì)。理解變體數(shù)量一個(gè)典型的TMP_SDF著色器考慮OUTLINE_ON、UNDERLAY_ON、UNDERLAY_INNER、GLOW_ON在其他Shader中等特性理論上可以產(chǎn)生多個(gè)變體。但在實(shí)際項(xiàng)目中你的材質(zhì)通常只會(huì)啟用其中一兩種固定的組合。Unity的構(gòu)建管線會(huì)負(fù)責(zé)收集并只打包這些用到的變體。通過對(duì)TMP_SDF著色器中這些宏的深入分析我們不僅學(xué)會(huì)了如何調(diào)整一個(gè)底影效果更重要的是理解了Unity Shader中條件編譯、版本兼容、數(shù)據(jù)流傳遞和顏色混合的核心機(jī)制。這些知識(shí)是進(jìn)行任何Shader定制和調(diào)試的基石。下次當(dāng)你需要讓文字在復(fù)雜的游戲場(chǎng)景中脫穎而出或者解決一個(gè)棘手的渲染兼容性問題時(shí)希望這份深入底層的分析能給你帶來清晰的思路和解決問題的信心。