Unity 2D Light (1) - Light Mesh
2D阴影
生成2d阴影一般有两种方案,一种是基于物理射线生成Light Mesh(也有叫ShadowMesh,我觉得叫LightMesh更贴切点)。另一种同unity3D阴影原理,就有是生成ShadowMap。
这篇记录使用射线生成LightMesh的两种方法。
方法1:通过射线扫描可视区域
因为使用了物理射线,所以需要遮挡物体有碰撞器(Collider)组件。
参考
基本流程
- 通过射线按照角度依次遍历可视区域
- 如果射线击中物体则保存击中点,未击中则保存射线终点
- 如果当前射线的击中状态与前一击中状态不同则使用二分法找到边角顶点
EG: AB两射线中间发射一条射线C,未击中则重复在AC中间再发射一条射线,直到击中物体。击中点D并不一定是边缘点,误差不可避免。如果到达设定最大次数仍未击中,这时候视作A点为边角顶点即可。
- 构建三角面片
结果
如果设定了最大射线距离,结果会趋近一个圆。
边角顶点(绿点)检测误差,转角误差无法避免,只能通过增加射线来减少视觉瑕疵(基本上要增加到300以上才没有明显的边缘抖动现象),这样会减少性能。
如果限定射线距离,射线穿过边角,下图这种情况会穿过物体错误。
解决办法还是要准确判断击中点是否是边角,使用collider.OverlapPoint可以判断射线穿过碰撞体并停在碰撞体内。(红框内的空白不该存在,而且还可能穿过碰撞体)
所以在没有碰撞体顶点的参与计算下,瑕疵还是挺多的。这样还不如直接用第二种方法。当然增加足够多的射线可以解决视觉上的大部分瑕疵。
这种方案并不适合作为2D阴影生成,但是作为其他比如人物视野显示倒是挺合适。
优化(未验证)
- 记录所有碰撞体顶点
- 只发射光源到顶点的射线
- 通过两个偏移(左右各偏移一点点)射线判断是否是边角顶点(如果有插值过的顶点法线更容易判断)
- 其他步骤类似
方法2(推荐):通过射线扫描边端点
逻辑上扫描的是边端点,概念上扫描的其实是线段。
参考
原理
扫描线段,记录距离最接近光源的线段,当最近线段变化则使用两条射线与前以最近线段相交构建三角网格。核心点是最近线段判断和线段排序(保证先扫到线段开端,再扫线段结束端)。
基本流程
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里就有当前扫描的线段(初始扫描射线穿过的线段)。优化的做法应该在前面排序处理弧度突变线段时处理。