
1. 問題現象與核心需求解析在Unity3D項目里尤其是涉及到一些需要表現通透質感的場景比如玻璃窗、水洼、半透明塑料或者一些特效材質我們經常會使用帶有Alpha通道的半透明Shader。一個典型的場景是一個使用Standard或URP/HDRP中類似著色器并調整了渲染模式為“Transparent”的MeshRenderer在接收來自平行光或其他光源的陰影時會出現一個非常惱人的問題——陰影顯示不全看起來像是被生硬地裁剪掉了一塊邊緣參差不齊完全破壞了視覺上的真實感和沉浸感。這個問題本質上是一個渲染管線中深度測試與顏色混合的經典沖突。對于不透明物體渲染流程相對直接先繪制深度再根據深度決定像素是否被覆蓋。但對于半透明物體為了能透過它看到后面的東西我們必須關閉深度寫入ZWrite Off并采用從后往前的順序進行Alpha混合。然而陰影映射Shadow Mapping技術依賴于從光源視角渲染的深度圖。當這個半透明物體參與陰影接收計算時它的“透明”特性與深度圖的“非此即彼”的遮擋判斷就產生了矛盾。GPU在判斷這個像素點是否在陰影中時可能會因為半透明物體自身的深度信息不完整或不參與深度測試而錯誤地認為該點未被遮擋應完全照亮或者被錯誤地深度比較導致本該投射到它上面的陰影片段被丟棄視覺上就表現為陰影缺失或被裁剪。所以我們的核心需求非常明確讓一個渲染隊列在透明區間通常是QueueTransparent的MeshRenderer能夠正確地、完整地接收來自其他不透明物體投射的動態陰影并且其自身投射的陰影如果也需要也能符合物理直覺。這不僅是一個視覺修復問題更是對渲染原理理解的深化。2. 渲染原理深度拆解為什么陰影會“消失”要解決問題必須深入理解Unity的渲染和陰影機制。我們以內置渲染管線或通用渲染管線URP為例其陰影生成主要依賴于傳統的陰影映射技術。2.1 陰影映射Shadow Mapping流程簡述從光源視角渲染深度將相機移動到光源位置看向場景渲染一張只包含深度信息的紋理圖即陰影貼圖Shadow Map。這張圖記錄了從光源角度看離光源最近的物體的深度值。?從相機視角渲染場景在正常的攝像機渲染通道中對于每個需要計算陰影的像素片元我們將其世界坐標轉換到光源的裁剪空間得到該點在光源視角下的深度值。?深度比較將這個計算出的深度值與陰影貼圖中對應位置存儲的深度值進行比較。如果片元深度 陰影貼圖深度意味著有更近的物體擋住了光則該片元在陰影中。否則該片元被照亮。2.2 半透明物體的“尷尬”境地問題就出在上述流程的第一步和第三步與半透明物體的渲染特性格格不入。在生成陰影貼圖階段光源視角默認情況下Unity的陰影投射Pass通常是ShadowCaster會為物體生成深度。但對于標準半透明Shader如Standard材質的Transparent模式其ShadowCasterPass可能因為物體被標記為“透明”而被特殊處理或者其自身的頂點/片元著色器輸出不符合深度生成的要求。更關鍵的是半透明物體通常不向深度緩沖區寫入深度ZWrite Off這使得在光源視角下它可能無法在陰影貼圖中留下有效的、代表“遮擋”的深度信息。即使留下了這個深度值代表的是這個透明表面的位置但光其實可以部分穿過它這本身就是一個物理矛盾。在接收陰影階段相機視角當計算一個位于半透明物體表面的像素是否在陰影中時GPU會進行上述深度比較。此時參與比較的“陰影貼圖深度”可能是后面不透明物體的深度因為半透明物體沒寫入深度也可能是半透明物體自身不準確的深度。無論是哪種情況比較結果都很可能出錯導致本該被陰影覆蓋的區域被錯誤地判定為“未被遮擋”從而陰影缺失。那些顯示出來的、邊緣被裁剪的陰影往往是深度比較在透明物體邊界處發生精度誤差或混合計算錯誤導致的視覺假象。2.3 渲染隊列Render Queue的關鍵影響Unity使用渲染隊列來排序物體的繪制順序。不透明物體Queue 2500如Geometry2000通常從前向后渲染利用深度測試快速丟棄被遮擋的片元。透明物體Queue 2500如Transparent3000則從后向前渲染并開啟Alpha混合。陰影計算與渲染隊列緊密相關。默認的陰影接收邏輯可能并未為Transparent隊列的物體做特殊優化導致在陰影紋理采樣和混合計算時沒有正確處理其背后的深度信息從而引發裁剪。3. 解決方案全景與選型策略解決“半透明物體接收陰影異常”不是一個單一開關而是一個根據項目需求內置管線/URP/HDRP、性能預算和效果要求進行權衡的“工具箱”。下面我將這些方案從易到難、從效果到性能進行梳理。3.1 方案一修改材質與著色器最直接但有限制這是最先應該嘗試的方法成本最低。調整渲染模式與混合狀態將材質的渲染模式從Transparent改為Fade。Fade模式同樣支持透明但有時在陰影處理上比Transparent更友好因為它通常意味著物體表面是均勻消散的而非折射。注意Fade模式在邊緣處理上可能不如Transparent平滑對于類似玻璃的物體可能不適用。在自定義Shader中嘗試調整混合命令。除了常見的Blend SrcAlpha OneMinusSrcAlpha可以試試Blend One OneMinusSrcAlpha預乘Alpha混合有時能改善與后期效果包括陰影的兼容性。關鍵一步強制深度寫入。在SubShader的Tags中嘗試添加IgnoreProjectorTrue可能避免一些舊式投影器的干擾但核心是深度。對于某些半透明物體我們可以謹慎地開啟深度寫入。在Pass中添加ZWrite On。這違背了完全正確的半透明渲染順序但能提供一個明確的深度值給陰影計算可能會立刻修復陰影裁剪。代價是可能引發自身層疊渲染錯誤比如兩個這樣的透明物體交叉時渲染順序會亂。這通常用于那些“實質上是非透明但表面有透明紋理”的物體比如帶透明孔的柵欄。使用專門的透明陰影接收ShaderUnity提供了一些內置的解決方案。例如可以嘗試使用Legacy Shaders/Transparent/Cutout/Soft Edge Unlit這類著色器它們內部可能包含了處理陰影接收的特殊邏輯。更專業的做法是復制Standard著色器或基于URP的Lit著色器創建一個自定義版本。核心修改點在于其用于陰影接收的ShadowCasterPass 以及處理光照的ForwardBase/ForwardAddPass。我們需要確保在接收陰影的Pass中即使表面透明也能正確地參與陰影貼圖的深度比較和顏色衰減計算。這通常涉及在片元著色器中對陰影采樣結果與自身的Alpha值進行某種形式的混合而不是簡單地丟棄。實操心得不要一上來就想著寫復雜Shader。優先在材質Inspector面板里調整。將Rendering Mode從Transparent切換到Cutout鏤空是一個極佳的測試手段。如果陰影顯示正常了那問題就鎖定在Alpha混合與深度處理的矛盾上。Cutout模式使用Alpha Test在片元級別直接決定像素的“有”或“無”因此它有明確的深度陰影計算就正常。這驗證了我們的問題分析。3.2 方案二調整光源與質量設置項目級調整如果方案一不能完全解決或者需要全局調整可以修改光源和項目設置。光源陰影設置陰影類型將平行光的陰影類型從Hard Shadows改為Soft Shadows。軟陰影在邊緣有漸變過渡有時能掩蓋或減輕因深度精度問題導致的硬裁剪邊緣。如網絡資料提及軟陰影能讓透明物體的陰影邊緣更柔和自然。陰影強度Strength適當降低陰影強度讓陰影變淡。雖然不能解決缺失問題但可以使殘留的、顯示不全的陰影不那么突兀作為一種視覺妥協。陰影分辨率Resolution提高陰影貼圖的分辨率如從Medium調到High或Very High。更高的分辨率能提供更精確的深度細節可能會減少因采樣精度不足導致的陰影邊緣錯誤判斷。但這會顯著增加顯存占用和渲染開銷。項目質量設置Quality Settings進入Edit - Project Settings - Quality。找到當前使用的質量等級下的Shadows設置。Shadow Distance陰影距離確保你的半透明物體在陰影距離內。太遠的物體不會被渲染陰影。Shadow Cascades陰影層級增加Cascade數量如從No Cascades到Two Cascades或Four Cascades。CSM技術為近處場景提供更高精度的陰影貼圖可能改善近處透明物體的陰影質量。Shadowmask Mode如果使用混合光照嘗試不同的Shadowmask模式觀察對透明物體的影響。3.3 方案三自定義著色器深度改造終極方案當上述方案均不奏效或你對效果有極致要求時就需要動手修改或編寫著色器。這是最徹底的方法也是性能影響最可控的方法。核心思路是將陰影接收的計算與表面的顏色渲染進行“解耦”處理。分離渲染通道思路我們可以設計一個雙Pass的Shader。第一個Pass專門用于寫入深度ZWrite On,ColorMask 0只寫深度不輸出顏色這個Pass使用一個簡化的、不考慮透明的頂點變換。這樣在生成陰影貼圖和主相機深度圖時這個物體會被當作一個“實心”的遮擋物從而獲得正確的深度信息。第二個Pass再執行正常的半透明顏色渲染ZWrite Off,Blend開啟。這樣陰影計算基于第一個Pass的深度而視覺表現基于第二個Pass的透明顏色。這種方法能完美解決接收陰影的問題但無法處理物體自身投射的透明陰影它投射的陰影依然是實心的。修改ShadowCaster Pass在自定義Shader中找到或添加ShadowCasterPass。確保在這個Pass里頂點著色器正常輸出裁剪空間位置片元著色器簡單輸出一個固定的深度或者基于紋理Alpha進行Clip如果是Cutout類型。對于真正的半透明物體我們需要讓ShadowCasterPass 也考慮Alpha值。一種常見技巧是在片元著色器中使用clip(color.a - _Cutoff)即使對于混合透明也設置一個極低的_Cutoff閾值如0.01讓幾乎完全透明的部分不產生陰影遮擋而大部分區域則產生。這模擬了“部分遮擋”的效果。在片元著色器中手動處理陰影衰減在基礎顏色渲染Pass的片元著色器里我們通常會調用UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)來獲取陰影衰減值。對于透明物體我們可以對這個atten值進行修改。例如// 假設 _Alpha 是當前片元的透明度 fixed shadow atten; // 原始的陰影衰減1.0完全照亮0.0完全陰影 // 方案A讓陰影也“透明化”。物體越透明陰影越淡。 fixed finalShadow lerp(1.0, shadow, _Alpha); // 方案B更物理的近似陰影強度不應超過物體本身的不透明度。 fixed finalShadow max(shadow, 1.0 - _Alpha); // 然后將 finalShadow 應用于光照計算 fixed3 lighting (ambient _LightColor0.rgb * ndotl * finalShadow) * albedo;這種方法給了我們最大的藝術控制權可以做出“薄紗陰影淡厚玻璃陰影深”的效果。方案選型決策表方案適用場景優點缺點性能影響材質參數調整快速驗證簡單透明物體如UI、簡單特效簡單快捷無需代碼效果有限可能不適用于復雜透明幾乎無光源/質量設置項目整體陰影質量提升緩解邊緣問題全局生效調整方便治標不治本可能無法根本解決缺失中提高分辨率、增加Cascade會增開銷自定義雙Pass深度需要精確接收陰影的靜態透明物體如窗戶陰影接收效果完美實現復雜自身陰影投射不透明增加Draw Call中高增加一個Pass修改ShadowCaster需要自身投射半透明陰影的動態物體如粒子能影響自身投射的陰影Shader編寫復雜效果物理準確性待商榷低手動混合陰影衰減需要高度藝術化控制陰影的透明物體控制粒度最細效果靈活需要深入理解光照模型和Shader編寫低4. 實戰在URP中實現一個完整的解決方案讓我們以一個具體的案例在Universal Render Pipeline (URP)中修復一個藍色半透明玻璃板的陰影接收問題。我們將采用“自定義著色器”方案因為它最通用、最有效。4.1 環境準備與問題復現創建一個新的URP項目或確保現有項目已啟用URP。在場景中放置一個平面作為地面一個立方體作為陰影投射物一個Quad我們將把它做成半透明玻璃板位于立方體和地面之間。創建一個新的材質命名為Glass_ShadowProblem暫時使用URP自帶的Lit著色器將其Surface Type改為Transparent調整Base Map顏色為藍色并降低Alpha。將該材質賦予Quad。調整平行光確保開啟Cast Shadows。運行游戲。你應該會看到地面和立方體上的陰影正常但Quad玻璃板上的陰影要么完全消失要么只在邊緣有殘缺的顯示。這就是我們要解決的問題。4.2 創建自定義URP Lit著色器在Project窗口中右鍵Create - Shader - Universal Render Pipeline - Lit Shader命名為CustomLit_TransparentShadow.shader。雙擊打開該Shader進行編輯。我們將基于此進行修改。4.3 關鍵代碼修改詳解我們主要修改兩個部分一是確保ShadowCasterPass能處理透明物體的深度二是在片元著色器中自定義陰影衰減的計算。第一步修改 Properties 和 SubShader Tags在Properties塊中我們添加一個用于控制陰影接收強度的屬性。Properties { _BaseColor(Base Color, Color) (1,1,1,1) _BaseMap(Base Map (RGB) Alpha (A), 2D) white {} // ... 其他原有屬性 _ShadowReceiveStrength(Shadow Receive Strength, Range(0, 1)) 1.0 }在SubShader的Tags中我們明確其隊列和渲染類型這對于透明物體很重要。SubShader { Tags { RenderTypeTransparent QueueTransparent RenderPipelineUniversalPipeline } LOD 300 // ... Passes }第二步重寫 ShadowCaster Pass可選但推薦URP的Lit.shader包含一個復雜的ShadowCasterPass。為了簡化我們可以直接使用URP提供的ShadowCaster渲染狀態。但為了處理Alpha我們需要在片元著色器中對Alpha進行測試。找到名為ShadowCaster的Pass或者如果不存在在HLSLINCLUDE塊后添加一個。核心是片元著色器struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; // ... 其他所需變量如用于采樣Alpha紋理的UV }; half4 ShadowPassFragment(Varyings input) : SV_TARGET { // 采樣基礎紋理的Alpha通道 half alpha SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).a; // 與基礎顏色的Alpha混合如果使用 alpha * _BaseColor.a; // 關鍵Alpha Test。即使對于透明混合也丟棄極透明的部分避免產生無效陰影。 // 使用一個很小的閾值比如0.01。這不會影響視覺上的半透明效果但能讓陰影生成更穩定。 half alphaClipThreshold 0.01; clip(alpha - alphaClipThreshold); // 如果需要還可以在這里應用距離陰影淡化等效果 return 0; }這個Pass確保在生成陰影貼圖時只有不透明度高于0.01的部分才會被當作遮擋物這比完全當作實心物體更合理。第三步修改光照著色器以自定義陰影衰減這是修復陰影接收的核心。我們需要找到片元著色器中進行光照計算的部分通常在LitInput.hlsl和Lighting.hlsl之后。在URP中我們通常在SurfaceData和InputData準備好后調用UniversalFragmentPBR函數。為了干預陰影計算我們需要自定義光照循環或修改傳入的陰影衰減值。一個更直接的方法是在片元著色器主函數中在計算完光照后手動調整最終顏色與陰影的關系。但更規范的做法是影響MainLightRealtimeShadow函數返回的衰減值。由于URP封裝較深我們可以采用一個取巧但有效的方法在片元著色器輸出前根據屏幕空間陰影紋理重新計算。實際上URP的UniversalFragmentPBR內部已經處理了陰影。我們可以通過修改SurfaceData的alpha值來間接影響陰影的“強度感知”但這不精確。更好的方法是編寫一個自定義的照明函數。考慮到復雜度這里給出一個在片元著色器末尾進行后處理的實用方案// 在Fragment函數中假設你已經計算出了最終的半透明顏色 finalColor half4 finalColor ...; // 你的光照計算結果 // 1. 獲取當前片元的屏幕空間陰影值。 // URP中可以通過采樣屏幕空間陰影紋理實現但需要獲取坐標。 // 更簡單的方法使用內置的 GetMainLight 函數它已經包含了實時陰影衰減信息。 Light mainLight GetMainLight(inputData.shadowCoord); // inputData需要包含shadowCoord half shadowAttenuation mainLight.shadowAttenuation; // 這是一個0-1的值1表示無陰影 // 2. 根據物體自身的透明度調整陰影的影響。 // 公式最終陰影衰減 在“無陰影”和“完全陰影”之間根據物體不透明度進行插值。 // 物體越透明它“身上”的陰影就應該越淡。 half objectOpacity surfaceData.alpha; // surfaceData.alpha來自你的表面數據 half adjustedShadow lerp(1.0, shadowAttenuation, objectOpacity); // 3. 將調整后的陰影衰減應用到漫反射/高光光照部分。 // 我們需要回溯影響光照計算。一個簡化的重算 // 假設 mainLight.color 是燈光顏色NDotL是法線點積光方向 half3 directLighting mainLight.color * (inputData.normalWS.x * ... ) * adjustedShadow; // 簡化表示 // 然后重新組合 finalColor.rgb (ambient directLighting) * surfaceData.albedo; // 由于直接修改光照計算較復雜對于首次嘗試我們可以采用一個視覺近似 // 讓陰影區域的透明物體顏色變暗的程度與其不透明度成正比。 // 這是一個屏幕空間的后處理效果不物理但快速 half shadowFactor shadowAttenuation; // 原始的陰影因子 half visualShadowStrength objectOpacity * _ShadowReceiveStrength; // 物體不透明度 * 用戶控制的強度 // 將顏色向陰影方向混合。如果shadowFactor小陰影深則顏色變暗。 finalColor.rgb lerp(finalColor.rgb * 0.5, finalColor.rgb, max(shadowFactor, 1 - visualShadowStrength));這段代碼提供了一個思路在著色器最后階段根據物體自身的透明度和原始的陰影衰減對最終輸出顏色進行二次混合模擬出“透明物體上陰影變淡”的效果。參數_ShadowReceiveStrength允許美術師在材質面板上調節陰影顯示的強度。4.4 材質配置與效果驗證將我們編寫的CustomLit_TransparentShadow著色器賦給之前創建的Glass_ShadowProblem材質。在材質Inspector面板你會看到新增的Shadow Receive Strength滑塊。將其調整為1。運行游戲。現在觀察Quad上的陰影。與之前使用標準Lit著色器相比陰影應該能更完整地顯示出來。雖然可能不是100%物理準確因為我們的方法是視覺修正但陰影裁剪問題會得到極大緩解陰影會根據玻璃的透明度有所減淡看起來更自然。嘗試調整Shadow Receive Strength和材質的Alpha值觀察陰影強度的變化直到達到滿意的視覺效果。注意事項這種自定義著色器的方法雖然強大但意味著你脫離了URP內置著色器的自動升級和優化路徑。當URP版本更新時可能需要手動同步更新你的自定義著色器。務必在項目早期評估如果大量物體需要此特性將其整合為一個可復用的Shader變體或Shader Graph子圖是更可維護的做法。5. 常見問題排查與性能優化指南即使按照上述步驟操作你可能還會遇到一些棘手的情況。下面是一些常見問題及其排查思路。5.1 陰影仍然完全缺失檢查渲染隊列Render Queue確保你的材質Shader的渲染隊列號大于2500透明隊列。可以在Shader的SubShader中使用Tags { QueueTransparent }或在材質面板手動調整Render Queue為3000。錯誤的隊列可能導致物體在陰影計算階段被提前剔除。檢查光照模式標簽LightMode Tag在自定義Shader的Pass中必須包含正確的LightModeTag。對于接收實時陰影的主Pass通常是LightModeUniversalForward。缺少此標簽該Pass可能不會被包含在渲染陰影的通道中。檢查Shader的ShadowCaster Pass確保你的Shader有一個有效的ShadowCasterPass。Unity在生成陰影貼圖時會尋找這個Pass。如果沒有物體將無法投射陰影也可能以某種默認方式參與陰影接收計算導致異常。你可以復制URP Lit Shader中的ShadowCasterPass到你的Shader中。檢查光源和物體距離確認物體在光源的陰影距離Shadow Distance內并且沒有被其他物體的ShadowCaster組件意外禁用。5.2 陰影邊緣閃爍或抖動Z-fighting原因當半透明物體與接收陰影的表面如地面距離非常近時兩者的深度值在精度上過于接近在陰影比較時會產生深度沖突導致邊緣像素在“在陰影中”和“不在陰影中”之間來回跳動。解決方案增加深度偏移Depth Bias在光源組件上或項目的質量設置中增加Shadow Bias參數。這個參數會在深度比較時添加一個微小的偏移將陰影稍微“推離”或“拉近”物體從而避免深度沖突。但設置過大會導致陰影與物體分離“彼得潘”現象。調整物體位置在場景設計上盡量避免半透明物體與接收面完全貼合即使只有微小的偏移如0.001個單位也能極大緩解問題。使用更高精度的深度紋理在URP Asset設置中將Depth Texture設置為16 bit或24 bit如果平臺支持以提高深度比較的精度。5.3 性能開銷考量自定義雙Pass方案每個使用此Shader的物體將額外增加一個DepthOnlyPass的渲染開銷。對于移動平臺或大量透明物體需謹慎使用。建議僅對場景中關鍵的、近距離的透明物體使用。高分辨率陰影貼圖提高Shadow Resolution和增加Shadow Cascades會顯著增加GPU內存帶寬和計算量。應在質量設置中針對不同平臺PC、主機、移動端進行分級配置。屏幕空間陰影調整我們示例中在片元著色器進行的后期混合計算雖然每像素一次但計算量很小性能影響通常可忽略不計。但如果場景中有成千上萬個這樣的透明片元仍需納入性能預算。靜態批處理中斷使用自定義Shader可能會打斷Unity的靜態批處理優化。如果物體是靜態的確保Shader支持批處理例如不使用每個實例獨有的材質屬性。5.4 進階使用Shader Graph實現URP/HDRP如果你使用的是Shader Graph思路是相通的創建一個新的Lit Shader Graph。將Surface Type設置為Transparent。在Master Stack的Fragment階段找到Shadow Color節點。默認情況下它可能不受Alpha影響。你需要將Alpha輸出連接到Shadow Color的Alpha通道或者用一個Lerp節點根據Alpha值在“無陰影顏色”和“全陰影顏色”之間混合。這相當于在代碼中手動混合陰影衰減。要影響陰影投射需要修改Shadow CasterPass對應的子圖。在Shader Graph的Graph Settings中可以添加一個Shadow CasterBlock。你可以將主圖的Alpha輸出連接到Shadow Caster的Alpha輸入并設置一個Alpha Clip Threshold實現我們之前在代碼中做的Alpha Test。Shader Graph提供了可視化的方式來實現上述邏輯對于不熟悉HLSL代碼的開發者來說更加友好且能更好地與URP/HDRP的渲染管線集成。解決Unity中半透明物體的陰影問題是一個從理解渲染原理到實踐Shader編程的完整過程。它沒有銀彈需要你根據項目的具體情況是手機游戲還是PC游戲是寫實風格還是卡通風格和物體的具體特性是均勻透明的玻璃還是邊緣透明的毛發來選擇最合適的方案。從最簡單的材質參數調整開始逐步深入到光源設置最后在必要時才動用自定義著色器這把“手術刀”。記住性能與效果的平衡永遠是實時渲染的藝術。