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