現(xiàn):UV偏移與柔邊技巧)
簡介一份面向Unity開發(fā)者與初學(xué)者的Shader實(shí)戰(zhàn)資源圍繞“Logo流光效果”展示純代碼和流光圖片疊加兩種實(shí)現(xiàn)思路幫助讀者理解ShaderLab語法、紋理UV動畫及Shader Graph可視化編輯流程。壓縮包共43個文件以asset、shader、mat、unity等Unity工程文件為主另含png貼圖、json配置及項(xiàng)目設(shè)置文件整體僅92KB輕量易用適合直接在編輯器中打開學(xué)習(xí)。目前已有1420人瀏覽學(xué)習(xí)配套工程中包含了場景、材質(zhì)和完整Shader源碼可對照查看時間變量控制、顏色混合及UV偏移等關(guān)鍵實(shí)現(xiàn)便于快速復(fù)用并制作個性化流光效果。對于想入門Unity Shader或希望提升界面動效表現(xiàn)力的開發(fā)者這份資源能提供直觀的范例與可運(yùn)行的參考工程。1. 為什么說Logo流光效果是Shader入門最容易出成就感的一個案例做Unity開發(fā)的人多半都有過這種經(jīng)歷策劃或UI同學(xué)指著一個靜態(tài)Logo說“讓它閃一下”你第一反應(yīng)是疊幾張序列幀圖做UV動畫或者直接調(diào)CanvasGroup的Alpha。結(jié)果做出來的效果要么發(fā)灰、要么邊緣發(fā)硬怎么看都像P了條光帶上去。真正能解決問題的路徑是越過UI層直接寫在Shader里——用像素著色器對采樣坐標(biāo)做一次隨時間變化的偏移讓光帶在Logo的Alpha邊界內(nèi)反復(fù)掃過。這套做法的核心只有兩個一個可循環(huán)的UV偏移量以及一個能約束光帶形狀的軟邊函數(shù)。本文就用“UV偏移法”作為主路線把Logo流光效果從能看做到耐看。適合三類人剛理清Shader基本結(jié)構(gòu)的Unity開發(fā)者被UI特效反復(fù)折騰的客戶端工程師以及想給數(shù)字孿生或WebGL項(xiàng)目加點(diǎn)“活氣”卻怕性能崩掉的技術(shù)美術(shù)。老手也能在參數(shù)邊界、SRP兼容和驗(yàn)證手法上找到一點(diǎn)可帶走的東西。2. 流光效果的本質(zhì)是用UV偏移做時間動畫而不是改貼圖Alpha2.1 為什么“便宜”的UV偏移法能撐起Logo流光Logo流光效果的目標(biāo)很明確一條有方向、有軟邊的亮帶沿著Logo表面周期性滑過。最容易想到的實(shí)現(xiàn)是Shader里直接采樣一張遮罩圖用ScrollOffset挪動采樣坐標(biāo)。這個思路的復(fù)雜度低到幾乎不需要額外貼圖——因?yàn)長ogo本身的Alpha通道就承擔(dān)了遮罩的角色。UV偏移的精髓在于它改變的是“采樣位置”而不是“像素顏色”。對同一張Logo貼圖來說所有顏色信息都還在只是采樣點(diǎn)沿著某個方向移動。配合上時間變量就能造成“光帶在走”的視覺錯覺。在經(jīng)典的Surface Shader或Unlit Shader里核心代碼只有幾行// 輸入結(jié)構(gòu)體里定義了uvfloat2 uv : TEXCOORD0; float speed _Speed * _Time.y; float2 uv i.uv; uv.x frac(speed); // 只偏移X讓采樣位置周期性左移或右移 fixed4 col tex2D(_MainTex, uv);這段代碼的意思是_Time.y是Unity內(nèi)置的累計(jì)運(yùn)行時間乘以速度系數(shù)后用它做X軸偏移值。frac()函數(shù)把偏移值截?cái)嘣?到1之間保證光帶循環(huán)往復(fù)不會越跑越遠(yuǎn)。此時Logo上的像素顏色會整體移動等于整張圖在平移而不是只有一條光帶在動——所以還差一個關(guān)鍵步驟用原始UV未偏移的UV做約束只讓特定區(qū)域的顏色與偏移后的采樣結(jié)果發(fā)生混合才能形成“光帶”。這個“基于原始位置約束”的步驟看起來不起眼卻是區(qū)分拙劣貼圖平移和高級流光效果的分水嶺。如果沒有它光帶掃過的地方顏色會被整體抹掉Logo主軸上的紋理細(xì)節(jié)也會丟失。2.2 從“能讓它動”到“讓它只在該亮的地方亮”流光效果如果不做區(qū)域約束整張圖都會閃。更準(zhǔn)確的做法是構(gòu)造一條“亮度函數(shù)”它的值域只在Logo內(nèi)部生效越靠近Logo邊緣亮度越低。最常見的約束方式是拿采樣UV和中心點(diǎn)做距離計(jì)算// 光帶中心位置隨時間變化這里把它限制在0.15~0.85之間循環(huán) float centerX lerp(0.15, 0.85, (frac(_Time.y * _Speed) 1) * 0.5); float strip 1 - saturate(abs(i.uv.x - centerX) / _StripWidth);這行代碼把UV的X坐標(biāo)和光帶中心位置做了絕對值差運(yùn)算然后用_StripWidth控制光帶寬度。saturate把負(fù)值截?cái)酁?意味著離中心越遠(yuǎn)亮度越低。如果再套一層smoothstep光帶的邊緣就能從硬邊變成柔邊這也是后面第三節(jié)要展開的參數(shù)化處理。要注意的是這套計(jì)算是基于原始UVi.uv的而不是偏移后的UV。為什么要用兩個不同的UV分別服務(wù)于采樣和遮罩核心原因是如果采樣坐標(biāo)已經(jīng)偏移光帶位置會隨偏移方向漂移那你看到的光帶就不是Logo上的光帶而是一條“追不上”的虛影。所以正常做法是用一個offsetUV去采樣貼圖讓顏色位置發(fā)生“被推著走”的變化用一個originUV去計(jì)算光帶的位置和亮度保證光帶在原點(diǎn)附近反復(fù)掃。這樣兩步交錯在一起Logo上的光帶既有了移動感又不會變成整張圖在滑動。這個雙UV思路往深了說可以演變成UV擾動、法線偏移等高級玩法但用在Logo流光是完全夠用的。2.3 為什么說“改貼圖Alpha”不是正經(jīng)Shader做法有些教程會讓美術(shù)把Logo的Alpha做成黑白漸變再用它當(dāng)遮罩。這個做法在項(xiàng)目里能跑但代價是要額外維護(hù)一張貼圖而且Alpha的漸變一旦不干凈光帶邊緣就會出現(xiàn)嚴(yán)重的鋸齒或灰邊。更麻煩的是同一套Shader要復(fù)用到其他Logo你不得不重新做一張配套黑圖。正確的做法是讓Shader自己從主貼圖的Alpha通道中提取遮罩。Logo的Alpha邊界天然是封閉的直接用采樣結(jié)果當(dāng)mask即可// 采樣主貼圖得到Alpha作為區(qū)域的軟邊遮罩 fixed4 col tex2D(_MainTex, uv); half alphaMask col.a; // 直接取Alpha通道做邊界 col.rgb _LightColor.rgb * strip * alphaMask;這樣光帶只會出現(xiàn)在Alpha大于0的地方而且Alpha本身的漸變會自然柔和邊緣。如果Logo外圈帶鋸齒Shader層面可以做一步透明度混合但大多數(shù)UI Logo的貼圖Alpha都是干凈的直接用即可。這個取舍決定了你的Shader是“一套通吃”還是“每個Logo配一個變體”。3. 用Shader Graph和手寫Shader把Logo流光拆成3個必調(diào)參數(shù)3.1 手寫Shader的完整最小實(shí)現(xiàn)我先給出一份可直接掛到Unlit Shader上的完整代碼。以Unity 2021及以上、Built-in渲染管線為例。用這個版本的原因是它不依賴任何URP或HDRP的節(jié)點(diǎn)模塊方便新手直接上手排查。Shader Custom/LogoTicker { Properties { _MainTex (Logo Texture, 2D) white {} _Speed (Speed, Range(0.1, 5.0)) 1.0 _StripWidth (Strip Width, Range(0.05, 0.5)) 0.15 _LightColor (Light Color, Color) (1, 0.85, 0.5, 1) _Angle (Angle, Range(0, 6.283)) 0.0 _Softness (Softness, Range(0.001, 0.8)) 0.1 } SubShader { Tags { Queue Transparent RenderType Transparent } Blend SrcAlpha One // 直接疊加保留光感 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _Speed; float _StripWidth; float _Softness; fixed4 _LightColor; float _Angle; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } // 旋轉(zhuǎn)UV坐標(biāo)實(shí)現(xiàn)不同角度的流光 float2 RotateUV(float2 uv, float angle) { float s sin(angle); float c cos(angle); return float2(uv.x * c - uv.y * s, uv.x * s uv.y * c); } fixed4 frag (v2f i) : SV_Target { float2 originalUV i.uv; float2 rotUV RotateUV(originalUV, _Angle); // 光帶走動范圍 float t frac(_Time.y * _Speed); float center lerp(0.0, 1.0, t); // 柔邊計(jì)算用距離與寬度的關(guān)系做出軟邊 float dist abs(rotUV.x - center); float strip 1 - smoothstep(0, _StripWidth _Softness, dist); // 提取Alpha做Logo遮罩 fixed4 col tex2D(_MainTex, originalUV); float mask col.a; // 光帶的RGB加到主圖上透明度不覆蓋原圖 col.rgb _LightColor.rgb * strip * mask * _LightColor.a; col.a mask; return col; } ENDCG } } }代碼邏輯依次做了三件事。第一把輸入的UV按角度做了旋轉(zhuǎn)這直接改變流光的方向參數(shù)設(shè)為0代表水平掃過設(shè)為1.57π/2則是垂直掃過。第二用時間變量t在0到1之間往返移動配合center變量控制光帶中心位置。第三在最后合成顏色時用strip * mask把光帶約束在Logo的Alpha邊界內(nèi)。這個mask取自原貼圖的透明通道所以哪怕Logo是圓形的、字母型的光帶都能自動貼合外形。_LightColor.a這個參數(shù)往往被人忽略。把它調(diào)低比如0.5流光的高光會變?nèi)踹m合暗色底Logo調(diào)高1.0光帶會明顯發(fā)亮適合科技感強(qiáng)的Logo演示。在UI場景中我一般會把主貼圖的Alpha和流光的Alpha分開控制因?yàn)閁I經(jīng)常要做整體淡入淡出如果混在一起容易讓光帶半透明化。3.2 Shader Graph版的映射關(guān)系如果你項(xiàng)目用的是URP和Shader Graph不敢直接掛CGPROGRAM可以用下列節(jié)點(diǎn)完全等價地實(shí)現(xiàn)上述邏輯Texture2D節(jié)點(diǎn)采樣LogoTexAdd節(jié)點(diǎn) / Multiply節(jié)點(diǎn)組合時間軸用Time節(jié)點(diǎn)輸出Time.y乘Speed再取Frac在Math下拉菜單里有Lerp節(jié)點(diǎn)把Frac結(jié)果作為TA設(shè)為0B設(shè)為1得到光帶中心UV節(jié)點(diǎn) → Split節(jié)點(diǎn)取R即U軸Subtract節(jié)點(diǎn) (UV.R - Center) Abs節(jié)點(diǎn)取絕對值Smoothstep節(jié)點(diǎn)輸入距離Edge0和Edge1分別填寬度的“內(nèi)軟邊”和“外軟邊”SampleTexture2D后的Alpha通道直接連到Multiply作為遮罩Add節(jié)點(diǎn)把光帶顏色與原圖RGB相加最后輸出Shader Graph里最容易被忽略的是節(jié)點(diǎn)的運(yùn)算順序。Unity的Lerp節(jié)點(diǎn)T是浮點(diǎn)數(shù)要求必須0~1之間才能正確插值。而Frac的輸出天然在0~1之間直接接即可。很多人在Graph里做的效果“閃得亂”是因?yàn)闆]有對時間變量做Frac而是直接用了Time節(jié)點(diǎn)裸奔導(dǎo)致光帶位置在0.1和3.7之間反復(fù)跳變。3.3 參數(shù)表以及“改了之后效果咋不對”的排查順序參數(shù)名取值建議作用出問題時表現(xiàn)_Speed0.5 ~ 3.0控制流光每秒循環(huán)次數(shù)速度太快會導(dǎo)致視覺閃爍太慢像貼紙_StripWidth0.05 ~ 0.25控制光帶寬度UV空間太小只剩一條線太大變成整圖泛光_Softness0.001 ~ 0.3控制光純度過大會讓光帶邊緣糊成一片像起霧_Angle0 ~ 6.283控制掃射角度非45度時要注意貼圖UV是否帶旋轉(zhuǎn)不然方向?qū)Σ簧蟔LightColor.a0.3 ~ 1控制流光強(qiáng)度調(diào)低后光帶幾乎看不見調(diào)高后過曝排錯的第一件事先降速、加寬、加柔邊確認(rèn)光帶本身在動之后再看是否貼合Logo輪廓。如果光帶動了但Logo邊緣沒變化說明mask取值不對多半是貼圖格式被壓縮成了ETC2不帶Alpha通道。把貼圖格式調(diào)成RGBA32或ASTC且勾選Alpha Is Transparency才能讀到干凈的Alpha。如果光帶是整條彩帶橫掃沒有跟隨Logo形狀檢查是否忘了在最終顏色里乘mask。4. 讓Logo流光在UI和粒子中不出格需要解決透明排序和SRP兼容4.1 三種使用場景的適配方式同樣是Logo流光掛在不同對象上問題完全不同。歸納下來有三種使用場景4.1.1 UI里的Image組件把Shader材質(zhì)賦給Image組件的Material屬性時最大的坑是Image自身的頂點(diǎn)顏色CanvasRenderer的Color會把Shader輸出結(jié)果再乘一遍。這意味著如果你在Shader里把Alpha設(shè)成了mask而CanvasGroup整體透明度是0.5最終光帶的明暗紋理仍能顯示但透明度會被雙重削弱。為了讓流光效果在UI里明亮我一般會讓Shader的頂點(diǎn)著色器做一步“頂?shù)簟表旤c(diǎn)色影響o.color fixed4(1, 1, 1, 1); // 在vert里強(qiáng)制頂點(diǎn)色為白色防止UI誤乘這樣改之后UI的CanvasGroup透明度仍可用但不會把光帶的顏色壓暗。另外UI節(jié)點(diǎn)如果Scale被壓縮成負(fù)數(shù)UV會翻轉(zhuǎn)流光方向會反向。排查光帶方向時先檢查RectTransform的Scale是否為負(fù)。4.1.2 3D平面上的Logo如場景大屏、虛擬展廳掛在Quad或玻璃材質(zhì)上時主要問題變成了透視映射。Quad的UV范圍是[0,1]和UI一致但當(dāng)攝像機(jī)拉開視角后原來水平的流光會因?yàn)橥敢曣P(guān)系看起來變斜。此時調(diào)整_Angle就能修正。真正需要留意的是Logo貼圖的Wrap模式。如果WrapMode是Repeat那么光帶邊緣紋理可能溢出到Logo背后造成奇怪的輝光。把Wrap Mode改為Clamp能避免這種邊緣溢色。4.1.3 粒子系統(tǒng)里的Logo貼花在粒子流里用同一個Shader可能會出現(xiàn)半透不半透的奇怪混合。因?yàn)榱W幽J(rèn)Blend Mode為AlphaBlend如果底層的Shader用了Additive混合Blend SrcAlpha One再次疊加在粒子的半透明區(qū)域上會產(chǎn)生“二次發(fā)光”。我的建議是粒子場景單獨(dú)做一個變體把Blend改回SrcAlpha OneMinusSrcAlpha然后光帶亮度靠_LightColor.a控制而不是靠混合模式。4.2 在URP/HDRP里的遷移要點(diǎn)URP環(huán)境下內(nèi)置管線的CGPROGRAM會出現(xiàn)兩次警告一次提示UnityCG.cginc不推薦使用一次提示APPROPRIATE混合問題。我把常見的兼容寫法整理成下表管線需要改的不用改的Built-in直接用上述代碼無URP#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl頂點(diǎn)函數(shù)用TransformObjectToHClip替換UnityObjectToClipPos代碼整體邏輯不變HDRP需要把Shader改成HLSL且?guī)haderPass聲明直接用CGPROGRAM基本報(bào)錯若只是做預(yù)覽可以在材質(zhì)上勾選Disable SRP Batcher勉強(qiáng)跑但不推薦另外一個能看到但不能忽略的問題是URP里如果啟用SRP Batcher你的Shader必須聲明CBUFFER_START(UnityPerMaterial)包含所有材質(zhì)屬性否則SRP Batcher會把材質(zhì)屬性當(dāng)成全局變量導(dǎo)致多個Logo材質(zhì)互相串參數(shù)。這是新手最容易踩的暗坑。4.3 性能層面流光效果必須在意增量開銷單純從GPU指令數(shù)看一個Unlit的全屏像素Shader開銷極低但在UI Canvas里面Shader的采樣次數(shù)會成倍放大。對Logo流光來講哪怕你的Shader寫得再省只要Canvas開啟了Mask或RectMask2DGPU就要多做一次裁剪計(jì)算再疊加上光帶里的smoothstep和abs在舊款手機(jī)上可能超幀。常見做法是把流光的Shader只掛在一個獨(dú)立Image組件上而不是讓整個Canvas都掛帶特效的材質(zhì)。在性能調(diào)優(yōu)時優(yōu)先關(guān)注這兩個指標(biāo)# 用Unity Profiler的GPU Usage看Shader耗時 # 重點(diǎn)觀察DrawCall數(shù)量是否因材質(zhì)變體增多而增加 # 如果是把Shader內(nèi)的屬性都用[Toggle]做成Keyword減少變體數(shù)量[Toggle]加Keyword的做法是把雙路面和光帶參數(shù)分離。沒有光帶時渲染純Logo不執(zhí)行流光邏輯指令數(shù)直接少一半。這個方案在UI頻繁重繪時能明顯降本。5. 從“掃一下”到“掃得高級”兩套變形玩法與驗(yàn)證手法5.1 雙流光交叉與延遲掃射如果你不甘心只做一條橫光帶可以讓兩條光帶以不同速度和方向錯峰掃過制造出類似“呼吸”的高級感。具體只要讓上一節(jié)的Shader從“單層流光”升級成“雙層流光”。我建議的做法是復(fù)制一份光帶計(jì)算函數(shù)把_Speed和_Angle各復(fù)制一個屬性再在最后的加法步驟里多疊加一次。這樣不需要改主貼圖的采樣邏輯只是對同一個主顏色增加一次增量。在參數(shù)上一條速度取1.2、寬度0.2另一條速度取0.7、寬度0.3第二條的角度旋轉(zhuǎn)45度。兩條流光疊加后會產(chǎn)生“無序而有序列”的閃爍感適合用于活動Logo或Splash畫面。5.2 按Logo明暗局部提亮讓流光更貼合內(nèi)容另一種更細(xì)膩的做法是讓光帶只在Logo較暗的區(qū)域浮現(xiàn)避開高光區(qū)。這要求把主貼圖的亮度采樣值帶進(jìn)遮罩運(yùn)算fixed3 texColor col.rgb; float lum dot(texColor, fixed3(0.299, 0.587, 0.114)); float darkMask 1 - saturate(lum * 3.0); col.rgb _LightColor.rgb * strip * mask * darkMask;這里的lum是亮度乘3是把亮度閾值壓縮到0~0.33之間darkMask意味著“暗部亮亮部暗”。實(shí)際觀感是光帶像水流一樣從暗色區(qū)域滑過而不是鋪滿整條Logo。用在白底Logo上尤其明顯。5.3 怎么驗(yàn)證一條流光Shader效果是不是對的建議用三個步驟固定時間變量把_Time.y臨時替換成0.3檢查光帶是否停在Logo中央且不跑動。如果此時光帶顏色正常再動起來。若不動時就有問題說明Shader里光帶疊加部分寫錯了。Frame Debugger檢查DrawCall打開Window - Analysis - Frame Debugger找到處理該Logo的DrawCall查看該材質(zhì)應(yīng)用了哪些混合因子。如果發(fā)現(xiàn)RenderQueue不對導(dǎo)致UI被遮擋把Shader的Queue改成Transparent。用RenderDoc或Xcode GPU capture抓一幀查看該Shader在最終顏色輸出時col.a的值是否等于貼圖Alpha。如果不等于代表col.a被光帶顏色污染了需要強(qiáng)制col.a mask。最后一個調(diào)試小技巧把_LightColor.a設(shè)為0如果此時LogoSprite的顏色和原始Logo完全一致說明流光疊加沒有污染基礎(chǔ)顏色如果顏色出現(xiàn)偏移檢查加法運(yùn)算是否不小心寫在了采樣之前。這是驗(yàn)證Shader整潔度的最有效的開關(guān)。本文還有配套的精品資源點(diǎn)擊獲取