標籤:

Unity 2D 動態陰影怎麼實現 | Nexus遊戲說

10月14日,浙江傳媒學院數字媒體技術專業系副主任張帆老師在「Nexus遊戲說 Ⅱ」上,為大家分享了2D動態陰影效果演算法分析,以下內容是Nexus整理的本期Game Talk的精華內容:

在許多的俯視視角2D遊戲中,會有這種陰影繪製的需求,但是Unity的燈光系統只能作用於3D的模型上,對2D sprite的輪廓不能做到實時地投射,因此,需要有一套方案來解決。

解決方案一:射線方法

射線掃描交點——生成Mesh

基本思路:

1. 獲得光線與障礙物的交點數據;

2. 交點數據與光源位置點生成扇形多邊形;

3. 對多邊形進行著色處理。

最直接的方法:

1. 以燈光為中心,向場景發射相等角度間隔的射線;

2. 射線檢測碰撞盒,收集與該射線的交點。

3. 生成從光源點到每個交點的向量;

4. 對向量集按照角度大小進行排序;

5. 交點集合和燈光中心點生成扇形多邊形Mesh。

優點:實現簡單;

缺點:要得到與物體結合較為吻合的陰影效果,需要增加射線的數量,增加了渲染資源消耗。

射線方法:射線掃描效果

這種方法的話其實效率很低,最後只能做到每秒8幀。下面介紹優化過的方法。

射線方法:頂點掃描

遍歷邊界點——連接光點和邊界點——生成Mesh

優化方法:

1.找出障礙物頂點序列,包括方形碰撞盒/圓形碰撞盒/多邊形碰撞盒等。

2.從光源點向每個頂點做連線,生成保存交點和角度)的對象。

3.向左、右分別偏移一個較小(如0.02°)的角度,如果沒有碰到當前障礙物或其他障礙物,則保存從光源到光源影響範圍的光線到自定義對象中。可以保證光線與物體的契合度更好。

4.對光線對象按角度大小進行排序。

5.生成扇形多邊形Mesh。

優點:和邊緣吻合度較高;使用較少的射線;

缺點:會把在光線分為內的不必要的頂點計算在內;對凸變形(圓)邊緣不夠精確。

射線方法:頂點位置計算-方形

射線方法:頂點位置計算-多邊形

射線方法:頂點位置計算-圓形

射線方法:頂點掃描-偏移射線修正

射線方法:頂點掃描-連接不必要的射線

射線方法:頂點掃描-凸面體問題

射線方法:頂點掃描

射線方法:深度剔除

射線方法:深度剔除-攝像機

MainCamea : 把Clear Flags設置為Depth Only,只顯示前景/暗色背景/角色。Depth為0

ItemCamea : 把Clear Flags設置為Depth Only,只顯示道具。Depth為-1

BGCamea : 把Clear Flags設置為Depth Only,只顯示亮色背景,Depth為-2

射線方法:深度剔除-光線物體Shader

SubShader{

Tags{ "Queue" = "Geometry+10" }

ColorMask 0

ZWrite On

Pass{}

}

方案二:基於像素的掃描

優點:精確度高(每個像素都可建立一條光線);效率高(80%的工作在GPU上運行)。

缺點:需要多個Pass來處理距離貼圖。

第一步:獲得障礙物的像素

在光源中心架設一台攝像機

然後利用RenderTexture.GetTemporary()函數獲得該攝像機的圖像,轉化成黑白圖像,黑色為障礙物。

第二步:計算光源點與障礙物像素的最短距離

上代碼

float4 ComputeDistancesPS(float2 TexCoord : TEXCOORD0) : COLOR0

{

float4 color = tex2D(inputSampler, TexCoord);

//compute distance from center

float distance = color.a>0.3f?length(TexCoord - 0.5f):1.0f;

//save it to the Red channel

distance *= renderTargetSize.x;

return float4(distance,0,0,1);

}

分別計算每個黑色像素與中心的距離,並保存在到距離貼圖中,因為距離是一個float類型,因此可以只保存在一個顏色通道中,這裡用紅色通道來保存。這裡把該貼圖稱為距離貼圖。

float4 DistortPS(float2 TexCoord : TEXCOORD0) : COLOR0

{

//translate u and v into [-1 , 1] domain

float u0 = TexCoord.x * 2 - 1;

float v0 = TexCoord.y * 2 - 1;

//then, as u0 approaches 0 (the center), v should also approach 0

v0 = v0 * abs(u0);

//convert back from [-1,1] domain to [0,1] domain

v0 = (v0 + 1) / 2;

//we now have the coordinates for reading from the initial image

float2 newCoords = float2(TexCoord.x, v0);

//read for both horizontal and vertical direction and store them in separate channels

float horizontal = tex2D(inputSampler, newCoords).r;

float vertical = tex2D(inputSampler, newCoords.yx).r;

return float4(horizontal,vertical ,0,1);

連接兩條對角線,把距離貼圖分為四個區域。分別為左右和上下。代表燈光的左右範圍和上下範圍。

以左右為例,把鐳射狀光線映射成相互平行的圖像。其中下圖的紅色垂直邊為光源位置。

可以看出,以垂直中心線為基準,向左,距離中心點越遠,表示該像素跟光源的距離越遠。向右同理。

最後同時處理左右和上下,把左右距離數據保存在R通道,上下距離數據保存在G通道。(上下距離需要旋轉90°)

該貼圖這裡稱為延展距離貼圖。

float4 HorizontalReductionPS(float2 TexCoord : TEXCOORD0) : COLOR0

{

float2 color = tex2D(inputSampler, TexCoord);

float2 colorR = tex2D(inputSampler, TexCoord + float2(TextureDimensions.x,0));

float2 result = min(color,colorR);

return float4(result,0,1);

}

生成新的貼圖,寬度為延展距離貼圖的寬度縮減一半,高度不變。

新的貼圖每個像素點取值為當前像素點與靠近燈光的像素點的存儲值(距離)進行比較,取較小的值。

重複1和2,直到貼圖的寬度只剩下2個像素,左邊的像素表示燈光左(上)邊的最近距離,右(下)邊的像素表示燈光右邊的最近距離。

該2像素寬度的圖像這裡稱為最近距離貼圖。

第三步渲染光照陰影貼圖

上代碼:

float4 DrawShadowsPS(float2 TexCoord: TEXCOORD0) : COLOR0

{

// distance of this pixel from the center

float distance = length(TexCoord - 0.5f);

distance *= renderTargetSize.x;

//apply a 2-pixel bias

distance -=2;

//distance stored in the shadow map

float shadowMapDistance;

//coords in [-1,1]

float nY = 2.0f*( TexCoord.y - 0.5f);

float nX = 2.0f*( TexCoord.x - 0.5f);

float GetShadowDistanceH(float2 TexCoord)

{

float u = TexCoord.x;

float v = TexCoord.y;

u = abs(u-0.5f) * 2;

v = v * 2 - 1;

float v0 = v/u;

v0 = (v0 + 1) / 2;

float2 newCoords = float2(TexCoord.x,v0);

//horizontal info was stored in the Red component

return tex2D(shadowMapSampler, newCoords).r;

}

float GetShadowDistanceV(float2 TexCoord)

{

float u = TexCoord.y;

float v = TexCoord.x;

u = abs(u-0.5f) * 2;

v = v * 2 - 1;

float v0 = v/u;

v0 = (v0 + 1) / 2;

float2 newCoords = float2(TexCoord.y,v0);

//vertical info was stored in the Green component

return tex2D(shadowMapSampler, newCoords).g;

}

//we use these to determine which quadrant we are in

if(abs(nY)<abs(nX))

{

shadowMapDistance = GetShadowDistanceH(TexCoor

}

else

{

shadowMapDistance = GetShadowDistanceV(TexCoord);

}

//if distance to this pixel is lower than distance from shadowMap,

//then we are not in shadow

float light = distance < shadowMapDistance ? 1:0;

float4 result = light;

result.b = length(TexCoord - 0.5f);

result.a = 1;

return result;

}

最終效果

Nexus 遊戲說

最後送上本期Nexus遊戲說的大合影~


推薦閱讀:

Unity接入多個SDK的通用介面開發與資源管理(一)
Unity接入多個SDK的通用介面開發與資源管理(三)
Unity接入多個SDK的通用介面開發與資源管理(二)
Unity中的單例模式、回調函數、消息分發的使用區別?

TAG:unity | 游戏 |