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) { 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遊戲說的大合影~