2D陰影
生成2d陰影一般有兩種方案,一種是基于物理射線生成Light Mesh(也有叫ShadowMesh,我覺得叫LightMesh更貼切點),另一種同unity3D陰影原理,就有是生成ShadowMap,
這篇記錄使用射線生成LightMesh的兩種方法,
方法1:通過射線掃描可視區域
因為使用了物理射線,所以需要遮擋物體有碰撞器(Collider)組件,
參考
SIGHT & LIGHT
基本流程
- 通過射線按照角度依次遍歷可視區域
- 如果射線擊中物體則保存擊中點,未擊中則保存射線終點
- 如果當前射線的擊中狀態與前一擊中狀態不同則使用二分法找到邊角頂點
EG: AB兩射線中間發射一條射線C,未擊中則重復在AC中間再發射一條射線,直到擊中物體,擊中點D并不一定是邊緣點,誤差不可避免,如果到達設定最大次數仍未擊中,這時候視作A點為邊角頂點即可,

- 構建三角面片
結果
如果設定了最大射線距離,結果會趨近一個圓,

邊角頂點(綠點)檢測誤差,轉角誤差無法避免,只能通過增加射線來減少視覺瑕疵(基本上要增加到300以上才沒有明顯的邊緣抖動現象),這樣會減少性能,

如果限定射線距離,射線穿過邊角,下圖這種情況會穿過物體錯誤,

解決辦法還是要準確判斷擊中點是否是邊角,使用collider.OverlapPoint可以判斷射線穿過碰撞體并停在碰撞體內,(紅框內的空白不該存在,而且還可能穿過碰撞體)

所以在沒有碰撞體頂點的參與計算下,瑕疵還是挺多的,這樣還不如直接用第二種方法,當然增加足夠多的射線可以解決視覺上的大部分瑕疵,
這種方案并不適合作為2D陰影生成,但是作為其他比如人物視野顯示倒是挺合適,


優化(未驗證)
- 記錄所有碰撞體頂點
- 只發射光源到頂點的射線
- 通過兩個偏移(左右各偏移一點點)射線判斷是否是邊角頂點(如果有插值過的頂點法線更容易判斷)
- 其他步驟類似
方法2(推薦):通過射線掃描邊端點
邏輯上掃描的是邊端點,概念上掃描的其實是線段,
參考
2d Visibility
原理
掃描線段,記錄距離最接近光源的線段,當最近線段變化則使用兩條射線與前以最近線段相交構建三角網格,核心點是最近線段判斷和線段排序(保證先掃到線段開端,再掃線段結束端),

基本流程
1. 初始化線段串列
通過邊頂點(即輪廓頂點),儲存所有線段到一個串列里,
輪廓邊緣頂點獲取方式多種多樣,可以使用默認Sprite頂點、Collider、Custom Physics Shape等,
默認生成的Sprite頂點并不完全貼合邊緣,

Collider除了PolygonCollider2D其他都需要另行計算,
推薦使用Custom Physics Shape,自動生成的更貼合邊緣,最重要的是可以自定義,

自定義陰影投射外形幾乎是必需的,

2. 線段分割
光的邊界一般設定為一個虛擬的正方形,將正方形的四條邊插入線段串列,
分割所有相交線段,

可選優化:1.相同邊合并,2.裁剪掉正方形外的線段,3.裁剪物體內的線段即只保留相交物體的輪廓邊線段(這個應該稍復雜點)
3. 初始化端點串列
將線段兩端儲存在一個串列,
線段頂點同時是一個線段的開始和另一個線段的結束,所以端點串列的大小是頂點大小的兩倍,也就是說將有兩個位置一樣的端點,但一個代表線段開端,另一個代表線段結束保存在端點串列里,
端點需要包括的資料:1. 位置,2. 是否是開始端點,3. 端點所在的線段
接下來是比較重要的端點排序:
1. 計算端點相對與光源中心的弧度 radian
2. 通過弧度交換線段的開始和結束(180°的弧度突變處需要手動處理)
3. 通過弧度排序所有端點,相同弧度開始端點在前
Atan2計算出弧度的大小范圍是 (-3.14, 3.14] 即從-180°逆時針遞增,180°達到最大,所以180°為起始掃描線,
之所以排序是保證接下來掃描時,首先掃到的是線段開端,
4. 掃描
參考文章2d Visibility上的互動示例是順時針掃描,我這邊是逆時針,不過沒什么影響,只要保證先掃到線段開端,順逆時針的結果是一樣的,
起始掃描為-180°,當然實際上遍歷的是排好序的端點串列,
掃描步驟偽代碼:
list<線段> open; // 保存當前掃描的線段,按與光源中心點的距離排序,即最接近光源中心的排最前面
beginRadian; // 掃描線所在弧度,初始化為最初掃描到的最近線段的開端弧度
foreach (端點 in 端點串列)
{
最近的線段_old = open.first() // 獲取最近的線段,可能為空
if(端點 是 開始) 將端點所在的線段保存到open(需排序),
else 將端點所在的線段從open中洗掉, // 因為前面排序保證了總是先掃描到開端,所以掃到結束端點時open必然有其所在的線段,
最近的線段_new = open.first()
if(最近的線段_new != 最近的線段_old)
{
保存構建三角網格頂點, // 光源中心以 beginRadian,當前遍歷端點的弧度構建兩個射線 與 最近的線段_old 相交得兩個交點+光源中心構成三角網格的三個頂點,
beginRadian = 端點的弧度
}
}
- 線段排序
線段排序參考 segment-sorting,
- 弧度突變區域線段處理
由于端點遍歷的開端是最小弧度值的那個,下圖這種情況,就會導致先掃到結束端,掃描快結束的時候才掃到開始端,

不經優化的做法是不構建三角網格先掃描一輪(原文中的做法),這樣結束時open里就有當前掃描的線段(初始掃描射線穿過的線段),優化的做法應該在前面排序處理弧度突變線段時處理,
5. 構建Mesh

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/217327.html
標籤:其他
上一篇:移掉 K 位數字
