現(xiàn)指南)
1. 項(xiàng)目概述與核心價(jià)值最近在做一個(gè)風(fēng)格化項(xiàng)目需要實(shí)現(xiàn)一種卡通風(fēng)格的云朵消散效果不是那種寫(xiě)實(shí)的、基于體積云的物理模擬而是更偏向于2D動(dòng)畫(huà)或者三渲二風(fēng)格里云像棉花糖一樣被風(fēng)吹散或者被角色“打散”的視覺(jué)表現(xiàn)。市面上現(xiàn)成的插件要么太寫(xiě)實(shí)、性能開(kāi)銷大要么風(fēng)格不匹配調(diào)來(lái)調(diào)去總感覺(jué)差了點(diǎn)意思。于是決定自己動(dòng)手從建模到Shader實(shí)現(xiàn)一條龍走通。這個(gè)流程的核心是有向距離場(chǎng)SDF與自定義Shader的結(jié)合再配合Blender進(jìn)行高效的模型預(yù)處理。最終效果可控性強(qiáng)性能友好風(fēng)格也能完全自定義。簡(jiǎn)單來(lái)說(shuō)這個(gè)方案就是在Blender里制作帶有“厚度”信息的低面數(shù)云朵模型并烘焙出它的SDF紋理。然后在Unity的Shader中利用這張SDF紋理通過(guò)一些數(shù)學(xué)技巧動(dòng)態(tài)地控制云朵的“消散”邊界和內(nèi)部細(xì)節(jié)。它特別適合需要大量風(fēng)格化云朵、且云朵需要有動(dòng)態(tài)形變或消失效果的游戲比如平臺(tái)跳躍、卡通渲染的RPG或者休閑游戲。即使你Shader零基礎(chǔ)跟著這個(gè)流程也能一步步實(shí)現(xiàn)出來(lái)。下面我就把從建模到Shader的完整實(shí)現(xiàn)流程以及中間踩過(guò)的坑和優(yōu)化技巧毫無(wú)保留地分享出來(lái)。2. 核心思路與技術(shù)選型解析2.1 為什么選擇SDF有向距離場(chǎng)實(shí)現(xiàn)一個(gè)物體的溶解、消散效果常見(jiàn)思路有頂點(diǎn)動(dòng)畫(huà)、遮罩紋理Alpha Mask和SDF。頂點(diǎn)動(dòng)畫(huà)對(duì)模型拓?fù)浣Y(jié)構(gòu)依賴大且難以做出平滑、有機(jī)的邊緣變化。遮罩紋理需要精心繪制且變化模式固定缺乏靈活性。SDF則完美解決了這些問(wèn)題。你可以把SDF理解為一張“地圖”這張地圖上每個(gè)像素或紋素的值記錄了該位置到物體表面最近的距離。在物體內(nèi)部的點(diǎn)距離為負(fù)在物體表面的點(diǎn)距離為0在物體外部的點(diǎn)距離為正。有了這張“距離地圖”我們?cè)赟hader里就可以做很多事比如設(shè)定一個(gè)閾值所有距離大于這個(gè)閾值的部分被“切掉”消散并且由于距離值是連續(xù)變化的切出來(lái)的邊緣也會(huì)是平滑漸變的這正是我們想要的卡通化柔和邊緣。對(duì)于云朵這種形狀不規(guī)則、但又希望消散邊緣平滑自然的對(duì)象SDF是天作之合。我們只需要在建模階段烘焙好SDF圖運(yùn)行時(shí)在Shader里進(jìn)行簡(jiǎn)單的閾值比較和邊緣處理就能用極低的性能代價(jià)驅(qū)動(dòng)復(fù)雜的形態(tài)變化。2.2 整體技術(shù)管線設(shè)計(jì)整個(gè)流程可以拆解為四個(gè)核心環(huán)節(jié)形成一個(gè)清晰的生產(chǎn)管線建模與預(yù)處理Blender創(chuàng)建低面數(shù)的云朵基礎(chǔ)模型。這里的核心不是高模細(xì)節(jié)而是塑造一個(gè)大概的體積形態(tài)。然后我們將這個(gè)模型放入一個(gè)立方體“烤箱”中烘焙出它的SDF紋理一張灰度圖。SDF紋理烘焙Blender這是最關(guān)鍵的一步。我們將利用Blender的“烘焙”功能計(jì)算模型到立方體內(nèi)每個(gè)點(diǎn)的距離并輸出為一張2D紋理。這張紋理就是后續(xù)所有效果的“數(shù)據(jù)源”。Shader開(kāi)發(fā)Unity Shader Graph / HLSL在Unity中創(chuàng)建Unlit或Surface Shader讀取SDF紋理。通過(guò)一個(gè)可動(dòng)態(tài)調(diào)整的“消散閾值”參數(shù)將SDF值與閾值比較決定像素的顯示云體與透明消散。同時(shí)加入對(duì)SDF值的額外處理來(lái)模擬卡通化的軟邊緣和內(nèi)部噪波細(xì)節(jié)。效果集成與動(dòng)畫(huà)控制Unity C#編寫(xiě)簡(jiǎn)單的腳本控制消散閾值的動(dòng)態(tài)變化如從0到1從而驅(qū)動(dòng)云朵從完整到完全消散的動(dòng)畫(huà)過(guò)程??梢詫⑵渑c游戲事件如玩家攻擊、風(fēng)吹過(guò)觸發(fā)關(guān)聯(lián)。這個(gè)管線的優(yōu)勢(shì)在于解耦美術(shù)在Blender里負(fù)責(zé)造型和烘焙程序在Unity里負(fù)責(zé)效果邏輯和交互。SDF紋理作為中間資產(chǎn)使得復(fù)雜形態(tài)變化的數(shù)據(jù)驅(qū)動(dòng)成為可能。2.3 工具選型Blender Unity Shader GraphBlender開(kāi)源免費(fèi)功能強(qiáng)大其Cycles渲染器內(nèi)置了烘焙SDF準(zhǔn)確說(shuō)是“容積吸收”或“環(huán)境光遮蔽”的變通使用的能力完全滿足我們的需求。相比其他付費(fèi)3D軟件其流程更透明可控。Unity Shader Graph對(duì)于不熟悉HLSL代碼的開(kāi)發(fā)者來(lái)說(shuō)Shader Graph是絕佳選擇。它通過(guò)節(jié)點(diǎn)可視化編輯Shader理解SDF處理流程采樣、計(jì)算、比較非常直觀。當(dāng)然用代碼寫(xiě)Unlit Shader也同樣高效本文會(huì)兼顧兩種方式的思路。注意雖然標(biāo)題提到了“卡通云”但此SDF消散方案的應(yīng)用絕不限于云朵。任何需要做平滑溶解、動(dòng)態(tài)生長(zhǎng)/消退、形態(tài)遮罩的卡通風(fēng)格物體比如魔法屏障、幽靈、軟泥怪、地形腐蝕效果等都可以套用這個(gè)流程。其核心思想是用預(yù)計(jì)算的3D空間信息SDF來(lái)驅(qū)動(dòng)2D屏幕空間的像素表現(xiàn)。3. Blender建模與SDF烘焙實(shí)戰(zhàn)詳解很多人覺(jué)得在Blender里烘焙SDF很神秘其實(shí)只要理解其本質(zhì)是“計(jì)算距離場(chǎng)并存成圖片”步驟就非常清晰了。下面我以制作一朵簡(jiǎn)單的卡通云為例。3.1 云朵基礎(chǔ)模型創(chuàng)建我們的目標(biāo)不是高模而是一個(gè)能代表云朵體積的“殼”。過(guò)高的面數(shù)會(huì)急劇增加烘焙計(jì)算量和紋理分辨率需求。新建球體在Blender中添加一個(gè)經(jīng)緯球UV Sphere。選中球體進(jìn)入編輯模式Tab鍵。粗略塑形使用縮放S、移動(dòng)G和旋轉(zhuǎn)R工具配合衰減編輯快捷鍵O這是塑造有機(jī)形狀的神器。開(kāi)啟衰減編輯后你的操作會(huì)像筆刷一樣影響一個(gè)球形區(qū)域內(nèi)的頂點(diǎn)拉出云朵那種蓬松、不規(guī)則的感覺(jué)。多復(fù)制ShiftD幾個(gè)球體將它們縮放、旋轉(zhuǎn)后組合在一起形成一團(tuán)云的基本形態(tài)。布爾合并與精簡(jiǎn)選中所有球體使用布爾修改器Boolean Modifier操作選擇“并集Union”將它們合并為一個(gè)整體。然后應(yīng)用Apply這個(gè)修改器。接著添加一個(gè)重構(gòu)網(wǎng)格修改器Remesh Modifier模式選擇“體素Voxel”適當(dāng)調(diào)整體素尺寸如0.05m可以生成一個(gè)拓?fù)渚鶆颉⒘餍蔚乃芫W(wǎng)格。最后可以再加一個(gè)精簡(jiǎn)修改器Decimate Modifier將面數(shù)減少到2000-5000面左右確保是低面數(shù)模型。實(shí)操心得在塑形階段不必追求完美。SDF烘焙會(huì)對(duì)形狀進(jìn)行“模糊化”和“平滑化”所以模型的一些小瑕疵在最終效果里并不明顯。重點(diǎn)在于整體體積感和比例。3.2 創(chuàng)建烘焙用的“烤箱”與UVSDF需要在一個(gè)封閉的空間內(nèi)計(jì)算這個(gè)空間就是“烤箱”通常用一個(gè)立方體Cube來(lái)充當(dāng)。新建立方體這個(gè)立方體要完全包裹住你的云朵模型并保持一定的距離。太大浪費(fèi)紋理精度太小會(huì)切掉模型。通常讓云朵模型占據(jù)立方體70%-80%的空間比較合適。拆分UV關(guān)鍵步驟選中這個(gè)立方體進(jìn)入編輯模式。全選所有面打開(kāi)UV編輯窗口。點(diǎn)擊“UV” - “智能UV投射Smart UV Project”。這步是為立方體的六個(gè)面生成一套UV坐標(biāo)后續(xù)SDF紋理將存儲(chǔ)在這套UV上。記住我們烘焙的是立方體空間內(nèi)的距離場(chǎng)所以UV是打在“烤箱”立方體上而不是“食物”云模型上。材質(zhì)與烘焙設(shè)置在材質(zhì)屬性中為立方體新建一個(gè)材質(zhì)。在材質(zhì)節(jié)點(diǎn)編輯器里確保原理化BSDF節(jié)點(diǎn)連接著“表面”輸出即可無(wú)需復(fù)雜設(shè)置。最關(guān)鍵的一步在立方體的材質(zhì)節(jié)點(diǎn)中添加一個(gè)圖像紋理Image Texture節(jié)點(diǎn)。點(diǎn)擊“新建”創(chuàng)建一張新圖像命名為“Cloud_SDF”。圖像尺寸建議1024x1024或2048x2048顏色設(shè)置為“非彩色”即灰度圖。將這張新建的圖像的色彩空間從“sRGB”改為“非彩色Non-Color”這一點(diǎn)至關(guān)重要因?yàn)镾DF是數(shù)據(jù)不是顏色信息用sRGB空間會(huì)導(dǎo)致精度損失和計(jì)算錯(cuò)誤。將這個(gè)圖像紋理節(jié)點(diǎn)的“顏色”輸出連接到一個(gè)自發(fā)光Emission著色器節(jié)點(diǎn)再將自發(fā)光節(jié)點(diǎn)連接到材質(zhì)的“表面”輸出。這樣我們就把這張圖像指定為立方體表面的輸出目標(biāo)。3.3 執(zhí)行SDF烘焙核心步驟Blender沒(méi)有直接的“Bake SDF”按鈕我們需要巧妙地利用“環(huán)境光遮蔽Ambient Occlusion”或“容積吸收Volume Absorption”來(lái)模擬。方法一利用Cycles渲染器的“環(huán)境光遮蔽”節(jié)點(diǎn)推薦這是更直觀的方法利用了AO計(jì)算射線碰撞距離的原理。設(shè)置渲染引擎在渲染屬性中將渲染引擎切換到Cycles。設(shè)備根據(jù)性能選擇GPU計(jì)算或CPU計(jì)算。配置立方體材質(zhì)節(jié)點(diǎn)刪除剛才連接的自發(fā)光節(jié)點(diǎn)。添加“環(huán)境光遮蔽Ambient Occlusion”節(jié)點(diǎn)。將其“顏色”輸出直接連接到材質(zhì)輸出的“表面”。關(guān)鍵參數(shù)調(diào)整在環(huán)境光遮蔽節(jié)點(diǎn)屬性中將“距離Distance”設(shè)置為一個(gè)略大于立方體對(duì)角線長(zhǎng)度的值例如如果立方體尺寸是4m可以設(shè)為6m。這個(gè)距離定義了AO計(jì)算的最大范圍必須覆蓋整個(gè)立方體空間。確保環(huán)境光遮蔽節(jié)點(diǎn)的“僅局部Only Local”選項(xiàng)是取消勾選的這樣它才會(huì)計(jì)算來(lái)自立方體外部即模型的遮蔽。配置云朵模型材質(zhì)選中云朵模型給它一個(gè)簡(jiǎn)單的材質(zhì)比如純白色自發(fā)光。確保云朵模型在渲染時(shí)是可見(jiàn)的。執(zhí)行烘焙在渲染屬性中找到“烘焙Bake”面板。烘焙類型選擇“環(huán)境光遮蔽Ambient Occlusion”。勾選“選定到活動(dòng)Selected to Active”。重要在3D視圖中先選中云朵模型“食物”最后按住Shift再選中立方體“烤箱”這樣立方體會(huì)成為高亮的“活動(dòng)對(duì)象”。在烘焙設(shè)置中將“最大距離Max Distance”設(shè)置為和環(huán)境光遮蔽節(jié)點(diǎn)中相同的值如6m。展開(kāi)“輸出Output”設(shè)置確保輸出目標(biāo)是你之前為立方體創(chuàng)建的“Cloud_SDF”圖像。點(diǎn)擊“烘焙Bake”按鈕。等待計(jì)算完成你將在圖像編輯器里看到一張灰度圖。白色區(qū)域代表立方體表面離云朵模型很遠(yuǎn)SDF正值大黑色區(qū)域代表表面緊貼或深入云朵模型SDF負(fù)值大灰色過(guò)渡帶就是距離為0附近的表面。方法二利用“容積吸收”著色器這種方法更接近SDF的物理定義但設(shè)置稍復(fù)雜。為云朵模型創(chuàng)建一個(gè)新材質(zhì)在節(jié)點(diǎn)編輯器中刪除原理化BSDF添加一個(gè)體積吸收Volume Absorption節(jié)點(diǎn)和一個(gè)材質(zhì)輸出Material Output節(jié)點(diǎn)。將體積吸收節(jié)點(diǎn)連接到材質(zhì)輸出的“體積”。將體積吸收的“顏色”設(shè)為白色“密度Density”設(shè)為一個(gè)很高的值比如1000。這樣云朵模型內(nèi)部就變成了一個(gè)完全不透明的體積。立方體的材質(zhì)和烘焙設(shè)置同方法一但烘焙類型選擇“發(fā)射Emit”。原理是Cycles會(huì)計(jì)算從立方體表面發(fā)出的光線在到達(dá)云朵這個(gè)高密度體積時(shí)被吸收的程度這個(gè)“吸收距離”的倒數(shù)關(guān)系可以映射為SDF。踩坑記錄初次烘焙很可能失敗出現(xiàn)全黑或全白的圖。請(qǐng)按以下順序排查① 是否勾選了“選定到活動(dòng)”選擇順序是否正確先選模型后選立方體② 立方體UV是否正確展開(kāi)③ 圖像紋理的色彩空間是否設(shè)置為“非彩色”④ 環(huán)境光遮蔽節(jié)點(diǎn)的“距離”和烘焙的“最大距離”是否設(shè)置得足夠大⑤ 確保場(chǎng)景中有光源或者打開(kāi)場(chǎng)景世界環(huán)境的“環(huán)境紋理”雖然AO計(jì)算不完全依賴光源但Cycles的某些設(shè)置下無(wú)光會(huì)導(dǎo)致錯(cuò)誤。3.4 SDF紋理的后期處理與導(dǎo)出烘焙得到的灰度圖就是我們的SDF紋理但通常需要一些簡(jiǎn)單的處理來(lái)優(yōu)化它在Shader中的使用效果。反相可選根據(jù)你的烘焙方法得到的灰度圖可能內(nèi)部是黑值小外部是白值大。而在Shader中我們通常希望模型內(nèi)部SDF值為負(fù)或小值。你可以在Blender的圖像編輯器里或者導(dǎo)出后在Photoshop/GIMP中對(duì)圖像進(jìn)行反相Invert。一個(gè)更穩(wěn)妥的方法是在Shader中采樣后用1.0 - sampledValue來(lái)反相這樣更靈活。模糊/平滑有時(shí)烘焙出的SDF在模型邊緣梯度變化太尖銳??梢栽趫D像軟件中施加一個(gè)輕微的高斯模糊如半徑1-2像素讓距離場(chǎng)更加平滑這樣最終消散邊緣會(huì)更柔和。導(dǎo)出將處理好的圖像保存為PNG或TGA格式導(dǎo)入U(xiǎn)nity。務(wù)必在Unity的導(dǎo)入設(shè)置中將紋理類型Texture Type設(shè)置為“Default”并取消勾選“sRGB (Color Texture)”同樣將其視為非色彩數(shù)據(jù)。將Wrap Mode設(shè)置為“Clamp”防止邊緣采樣時(shí)出現(xiàn)重復(fù)的接縫。至此Blender部分的工作全部完成。我們得到了一張關(guān)鍵的SDF紋理它編碼了云朵模型在三維空間中的形狀信息。4. Unity Shader實(shí)現(xiàn)從SDF到消散效果有了SDF紋理接下來(lái)就是在Unity里讓它“動(dòng)”起來(lái)。我將分別用Shader Graph和代碼HLSL兩種方式講解核心邏輯你可以選擇適合自己的。4.1 Shader Graph節(jié)點(diǎn)化實(shí)現(xiàn)對(duì)于不熟悉代碼的開(kāi)發(fā)者Shader Graph是快速上手的利器。我們創(chuàng)建一個(gè)Unlit Graph?;A(chǔ)設(shè)置與屬性創(chuàng)建以下屬性Properties_MainTex(Texture2D): 云朵的基礎(chǔ)顏色/紋理圖。_SDFTex(Texture2D): 導(dǎo)入的SDF紋理。_DissolveThreshold(Range(0, 1)): 消散閾值0代表云朵完整1代表完全消散。這是我們動(dòng)態(tài)控制效果的主要參數(shù)。_EdgeWidth(Range(0, 0.5)): 消散邊緣的寬度用于控制軟邊的大小。_EdgeColor(Color): 消散邊緣的顏色比如亮白色或淡藍(lán)色增強(qiáng)卡通感。將_SDFTex的采樣器狀態(tài)Sampler State設(shè)置為“Clamp”。核心邏輯節(jié)點(diǎn)連接采樣SDF使用Sample Texture 2D節(jié)點(diǎn)對(duì)_SDFTex進(jìn)行采樣UV輸入使用模型的UV坐標(biāo)因?yàn)镾DF紋理是烘焙在立方體UV上的而立方體模型導(dǎo)入U(xiǎn)nity后其UV保持不變。閾值比較將采樣得到的SDF值假設(shè)范圍0~1其中0.5附近對(duì)應(yīng)模型表面與_DissolveThreshold進(jìn)行比較。這里有一個(gè)關(guān)鍵技巧我們不是直接比較而是先對(duì)SDF值做一個(gè)偏移和縮放使其與閾值比較時(shí)能產(chǎn)生一個(gè)平滑過(guò)渡帶。節(jié)點(diǎn)流SDF Sample-Remap節(jié)點(diǎn)可選用于調(diào)整SDF數(shù)據(jù)的有效范圍例如從 [0,1] 重映射到 [-0.5, 0.5]讓0對(duì)應(yīng)表面- 與_DissolveThreshold相減Subtract。我們得到delta sdf - threshold。生成Alpha遮罩delta值小于0表示該點(diǎn)在云朵內(nèi)部或深處應(yīng)顯示。delta值大于_EdgeWidth表示該點(diǎn)遠(yuǎn)離消散邊界應(yīng)完全透明。delta值在0到_EdgeWidth之間就是我們要的軟邊過(guò)渡區(qū)。使用Smoothstep節(jié)點(diǎn)可以優(yōu)雅地實(shí)現(xiàn)這個(gè)過(guò)渡。連接方式Alpha 1 - Smoothstep(0, _EdgeWidth, delta)。這樣delta在0時(shí)Alpha為1在_EdgeWidth時(shí)Alpha為0中間平滑過(guò)渡。邊緣色添加在過(guò)渡區(qū)我們還可以混合進(jìn)邊緣色。計(jì)算一個(gè)邊緣因子edgeFactor Smoothstep(0, _EdgeWidth, delta) - Smoothstep(_EdgeWidth - smallValue, _EdgeWidth, delta)這個(gè)因子在過(guò)渡帶中間最強(qiáng)兩邊衰減。然后用這個(gè)因子將_EdgeColor混合到基礎(chǔ)顏色上。最終輸出將處理后的顏色和計(jì)算出的Alpha連接到Master Stack的Base Color和Alpha端口。對(duì)于Unlit Graph還需要將Alpha連接到Alpha Clip Threshold以實(shí)現(xiàn)透明裁剪或者使用混合模式如Blend SrcAlpha OneMinusSrcAlpha。添加細(xì)節(jié)噪波進(jìn)階 單純的閾值消散邊緣可能太“干凈”不符合云朵的質(zhì)感。我們可以添加一些噪波Noise來(lái)打破邊緣的規(guī)則性。采樣一張?jiān)氩▓D或使用程序化噪波節(jié)點(diǎn)如Gradient Noise。將噪波值添加到_DissolveThreshold上或者與SDF值相加。例如adjustedThreshold _DissolveThreshold (noise * 0.1 - 0.05)。這樣消散邊界就會(huì)變得參差不齊更有自然消散的感覺(jué)。注意噪波的UV最好使用世界空間或?qū)ο罂臻g坐標(biāo)而不是模型UV以避免紋理拉伸和跟隨模型移動(dòng)。4.2 HLSL代碼實(shí)現(xiàn)Unlit Shader如果你更喜歡代碼的精確控制下面是一個(gè)簡(jiǎn)化的Unlit Shader核心片段Shader Custom/CartoonCloudDissolve { Properties { _MainTex (Base Color, 2D) white {} _SDFTex (SDF Texture, 2D) white {} _DissolveThreshold (Dissolve Threshold, Range(0, 1)) 0.0 _EdgeWidth (Edge Width, Range(0, 0.3)) 0.1 _EdgeColor (Edge Color, Color) (1,1,1,1) _NoiseTex (Noise Tex, 2D) white {} _NoiseStrength (Noise Strength, Range(0, 0.5)) 0.05 } SubShader { Tags { RenderTypeTransparent QueueTransparent } Blend SrcAlpha OneMinusSrcAlpha Cull Off // 雙面渲染云朵通常需要 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; // 用于世界空間噪波 }; struct v2f { float2 uv : TEXCOORD0; float2 uvSDF : TEXCOORD1; float3 worldPos : TEXCOORD2; float4 vertex : SV_POSITION; }; sampler2D _MainTex, _SDFTex, _NoiseTex; float4 _MainTex_ST, _SDFTex_ST, _NoiseTex_ST; float _DissolveThreshold, _EdgeWidth, _NoiseStrength; float4 _EdgeColor; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.uvSDF TRANSFORM_TEX(v.uv, _SDFTex); // 假設(shè)模型UV與烘焙UV一致 o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { // 采樣基礎(chǔ)顏色和SDF fixed4 col tex2D(_MainTex, i.uv); float sdf tex2D(_SDFTex, i.uvSDF).r; // SDF存儲(chǔ)在r通道 // 添加基于世界坐標(biāo)的噪波到閾值上使邊界不規(guī)則 float2 noiseUV i.worldPos.xz * _NoiseTex_ST.xy _NoiseTex_ST.zw * _Time.y; // 讓噪波動(dòng)起來(lái) float noise tex2D(_NoiseTex, noiseUV).r; float adjustedThreshold _DissolveThreshold (noise - 0.5) * _NoiseStrength; // 核心消散計(jì)算 float delta sdf - adjustedThreshold; // 假設(shè)sdf值越大越靠近外部 // 計(jì)算Alphadelta越小越內(nèi)部alpha越接近1 float alpha 1 - smoothstep(0, _EdgeWidth, delta); // 計(jì)算邊緣光暈因子 float edgeFactor smoothstep(0, _EdgeWidth, delta) - smoothstep(_EdgeWidth - 0.01, _EdgeWidth, delta); // 混合邊緣色 col.rgb lerp(col.rgb, _EdgeColor.rgb, edgeFactor * _EdgeColor.a); // 最終輸出Alpha為0時(shí)完全裁剪 clip(alpha - 0.001); return fixed4(col.rgb, alpha); } ENDCG } } FallBack Diffuse }這個(gè)Shader實(shí)現(xiàn)了基礎(chǔ)消散、動(dòng)態(tài)噪波邊界和邊緣著色。將材質(zhì)賦給導(dǎo)入的立方體模型即我們的“烤箱”模型調(diào)整_DissolveThreshold參數(shù)就能看到云朵逐漸消散的效果。4.3 效果優(yōu)化技巧深度寫(xiě)入ZWrite與渲染隊(duì)列對(duì)于半透明物體通常設(shè)置ZWrite Off并放在QueueTransparent。但多個(gè)云朵疊加時(shí)可能出現(xiàn)排序問(wèn)題。如果云朵是獨(dú)立不重疊的可以嘗試ZWrite On和QueueAlphaTest利用clip進(jìn)行硬裁剪性能更好且無(wú)排序問(wèn)題。性能優(yōu)化SDF紋理分辨率不宜過(guò)高512x512或1024x1024對(duì)于卡通風(fēng)格通常足夠。如果場(chǎng)景中云朵很多考慮使用紋理圖集Texture Atlas將多個(gè)SDF打包到一張大圖上減少Draw Call。動(dòng)態(tài)變化除了整體閾值還可以根據(jù)世界坐標(biāo)、視角方向來(lái)擾動(dòng)閾值實(shí)現(xiàn)“風(fēng)吹散”的方向性效果。例如將_DissolveThreshold與dot(worldNormal, windDirection)結(jié)合讓背風(fēng)面先消散。5. 常見(jiàn)問(wèn)題、排查與效果增強(qiáng)在實(shí)際操作中你肯定會(huì)遇到一些預(yù)期之外的情況。這里我把常見(jiàn)問(wèn)題和解決方案整理成表方便你快速排查。問(wèn)題現(xiàn)象可能原因解決方案模型全黑/全白無(wú)消散效果1. SDF紋理采樣UV錯(cuò)誤。2. SDF紋理導(dǎo)入U(xiǎn)nity時(shí)未取消sRGB。3. Shader中SDF值與閾值比較邏輯反了。1. 檢查模型UV通道確保與烘焙UV一致。在Shader中可視化UV輸出為顏色檢查。2. 在Unity中檢查SDF紋理導(dǎo)入設(shè)置確?!皊RGB (Color Texture)”未勾選。3. 嘗試反轉(zhuǎn)SDF值1.0 - sdf或反轉(zhuǎn)閾值比較邏輯threshold - sdf。消散邊緣鋸齒嚴(yán)重1. SDF紋理分辨率過(guò)低。2. 邊緣過(guò)渡計(jì)算太硬未使用smoothstep。3. 模型本身面數(shù)過(guò)低UV展開(kāi)有拉伸。1. 嘗試使用更高分辨率的SDF紋理如2048x2048。2. 確保使用smoothstep進(jìn)行軟邊緣插值并適當(dāng)增加_EdgeWidth。3. 檢查烘焙用的立方體模型UV是否均勻展開(kāi)避免極端拉伸。效果不隨閾值均勻變化中間有跳變SDF紋理數(shù)據(jù)有誤可能存在無(wú)效區(qū)域或烘焙錯(cuò)誤?;谺lender檢查烘焙結(jié)果。在圖像編輯器中將視圖切換到“著色Rendered”檢查SDF灰度圖是否平滑連續(xù)有無(wú)異常的色塊或斷層。確保烘焙時(shí)模型和立方體位置沒(méi)有意外移動(dòng)。多個(gè)云朵共用Shader時(shí)消散動(dòng)畫(huà)不同步所有材質(zhì)實(shí)例共享同一個(gè)_DissolveThreshold屬性。為每個(gè)云朵創(chuàng)建獨(dú)立的材質(zhì)實(shí)例Material Instance然后通過(guò)腳本分別控制每個(gè)實(shí)例的_DissolveThreshold。移動(dòng)相機(jī)時(shí)邊緣出現(xiàn)閃爍或抖動(dòng)可能是深度測(cè)試ZTest與Alpha裁剪沖突或紋理過(guò)濾問(wèn)題。嘗試將Shader的深度測(cè)試模式改為ZTest LEqual或關(guān)閉深度寫(xiě)入ZWrite Off。確保SDF紋理的過(guò)濾模式Filter Mode設(shè)置為“Bilinear”或“Trilinear”。想要更“蓬松”的云內(nèi)部?jī)H靠SDF邊界變化內(nèi)部缺乏細(xì)節(jié)。在Shader中除了邊界消散還可以根據(jù)SDF值即到表面的距離來(lái)調(diào)制內(nèi)部的不透明度或顏色。例如讓云中心更不透明越靠近邊緣越透明。公式internalDensity saturate(-sdf * _DensityScale)假設(shè)內(nèi)部sdf為負(fù)。5.1 效果增強(qiáng)思路基礎(chǔ)消散效果實(shí)現(xiàn)后可以進(jìn)一步豐富細(xì)節(jié)粒子增強(qiáng)在云朵消散的邊緣通過(guò)Unity的粒子系統(tǒng)Particle System生成一些細(xì)小的、飄散的白色粒子點(diǎn)模擬云被吹散的碎末。粒子發(fā)射器的形狀可以綁定到云朵模型上并根據(jù)消散閾值控制粒子的發(fā)射率。頂點(diǎn)動(dòng)畫(huà)在Shader中對(duì)云朵模型的頂點(diǎn)沿法線方向進(jìn)行輕微的、隨機(jī)的波動(dòng)模擬云朵柔軟、流動(dòng)的質(zhì)感。這個(gè)動(dòng)畫(huà)強(qiáng)度也可以與消散閾值關(guān)聯(lián)在消散時(shí)波動(dòng)更劇烈。多層SDF疊加烘焙兩張不同“密度”的SDF紋理。一張用于控制整體的外形消散另一張精度更高或經(jīng)過(guò)不同模糊處理的用于模擬內(nèi)部更細(xì)碎的孔洞消散兩者結(jié)合可以產(chǎn)生更豐富的層次感。與風(fēng)場(chǎng)交互在游戲世界中定義一個(gè)風(fēng)場(chǎng)向量。在Shader中根據(jù)像素點(diǎn)在世界空間中的位置和法線方向計(jì)算與風(fēng)場(chǎng)的點(diǎn)乘用這個(gè)值來(lái)偏移_DissolveThreshold從而實(shí)現(xiàn)風(fēng)從特定方向吹散云朵的效果。6. 完整項(xiàng)目集成與性能考量將制作好的云朵預(yù)制體集成到項(xiàng)目中時(shí)還需要考慮一些工程化和性能問(wèn)題。預(yù)制體制作一個(gè)完整的云朵預(yù)制體應(yīng)包含帶Mesh Filter和Mesh Renderer的立方體模型承載SDF UV、上面編寫(xiě)的材質(zhì)、以及一個(gè)控制腳本如CloudDissolveController。控制腳本示例using UnityEngine; public class CloudDissolveController : MonoBehaviour { public float dissolveSpeed 0.5f; private Material _material; private float _currentThreshold 0f; private bool _isDissolving false; void Start() { _material GetComponentRenderer().material; // 獲取材質(zhì)實(shí)例 _material.SetFloat(_DissolveThreshold, 0f); } void Update() { if (_isDissolving) { _currentThreshold Time.deltaTime * dissolveSpeed; _material.SetFloat(_DissolveThreshold, _currentThreshold); if (_currentThreshold 1f) { _isDissolving false; // 可以在這里觸發(fā)銷毀對(duì)象或回收 // gameObject.SetActive(false); } } } // 供其他腳本調(diào)用開(kāi)始消散 public void StartDissolve() { _isDissolving true; } }性能優(yōu)化批處理Batching確保所有云朵模型使用相同的Mesh立方體和材質(zhì)。通過(guò)材質(zhì)屬性塊MaterialPropertyBlock來(lái)分別設(shè)置每朵云的_DissolveThreshold而不是創(chuàng)建多個(gè)材質(zhì)實(shí)例這樣可以保持動(dòng)態(tài)合批Dynamic Batching或GPU Instancing的有效性。LOD多層次細(xì)節(jié)對(duì)于遠(yuǎn)處的小云朵可以使用更低分辨率的SDF紋理甚至用更簡(jiǎn)單的Shader變體比如去掉噪波和邊緣色。視錐體剔除Frustum Culling這是自動(dòng)的但要確保云朵的Renderer組件和Collider如果有設(shè)置合理不要過(guò)大。對(duì)象池Object Pooling對(duì)于頻繁出現(xiàn)和消失的云朵如被擊散的云使用對(duì)象池進(jìn)行復(fù)用避免頻繁的Instantiate和Destroy操作。從Blender的一個(gè)簡(jiǎn)單球體開(kāi)始經(jīng)過(guò)SDF烘焙、Shader編寫(xiě)、到Unity中的集成與控制一套完整的、可定制的卡通云消散效果管線就搭建完成了。這套方法的美妙之處在于其靈活性和數(shù)據(jù)驅(qū)動(dòng)性。一旦管線打通美術(shù)設(shè)計(jì)師可以自由地創(chuàng)造各種形狀的云朵甚至其他物體烘焙SDF而程序只需微調(diào)Shader參數(shù)就能獲得風(fēng)格一致、性能可控的動(dòng)態(tài)效果。它不僅僅是實(shí)現(xiàn)了一個(gè)效果更是提供了一種解決風(fēng)格化動(dòng)態(tài)形變問(wèn)題的思路。