Unity 2D Light (1) - Light Mesh

2D阴影

生成2d阴影通常有两种方案,一种是基于物理射线生成Light Mesh(也有叫ShadowMesh,我以为叫LightMesh更贴切点)。另外一种同unity3D阴影原理,就有是生成ShadowMap。html

这篇记录使用射线生成LightMesh的两种方法。git


方法1:经过射线扫描可视区域

由于使用了物理射线,因此须要遮挡物体有碰撞器(Collider)组件。ide

参考

SIGHT & LIGHT性能

基本流程

  1. 经过射线按照角度依次遍历可视区域
  2. 若是射线击中物体则保存击中点,未击中则保存射线终点
  3. 若是当前射线的击中状态与前一击中状态不一样则使用二分法找到边角顶点

EG: AB两射线中间发射一条射线C,未击中则重复在AC中间再发射一条射线,直到击中物体。击中点D并不必定是边缘点,偏差不可避免。若是到达设定最大次数仍未击中,这时候视做A点为边角顶点便可。优化

image

  1. 构建三角面片

结果

若是设定了最大射线距离,结果会趋近一个圆。3d

image

边角顶点(绿点)检测偏差,转角偏差没法避免,只能经过增长射线来减小视觉瑕疵(基本上要增长到300以上才没有明显的边缘抖动现象),这样会减小性能。code

image

若是限定射线距离,射线穿过边角,下图这种状况会穿过物体错误。htm

image

解决办法仍是要准确判断击中点是不是边角,使用collider.OverlapPoint能够判断射线穿过碰撞体并停在碰撞体内。(红框内的空白不应存在,并且还可能穿过碰撞体)blog

image

因此在没有碰撞体顶点的参与计算下,瑕疵仍是挺多的。这样还不如直接用第二种方法。固然增长足够多的射线能够解决视觉上的大部分瑕疵。排序

这种方案并不适合做为2D阴影生成,可是做为其余好比人物视野显示却是挺合适。

image

image

优化(未验证)

  1. 记录全部碰撞体顶点
  2. 只发射光源到顶点的射线
  3. 经过两个偏移(左右各偏移一点点)射线判断是不是边角顶点(若是有插值过的顶点法线更容易判断)
  4. 其余步骤相似

方法2(推荐):经过射线扫描边端点

逻辑上扫描的是边端点,概念上扫描的实际上是线段。

参考

2d Visibility

原理

扫描线段,记录距离最接近光源的线段,当最近线段变化则使用两条射线与前以最近线段相交构建三角网格。核心点是最近线段判断和线段排序(保证先扫到线段开端,再扫线段结束端)。

image

基本流程

1. 初始化线段列表

经过边顶点(即轮廓顶点),储存全部线段到一个列表里。

轮廓边缘顶点获取方式多种多样,可使用默认Sprite顶点、Collider、Custom Physics Shape等。

默认生成的Sprite顶点并不彻底贴合边缘。

image

Collider除了PolygonCollider2D其余都须要另行计算。

推荐使用Custom Physics Shape,自动生成的更贴合边缘,最重要的是能够自定义。

image

自定义阴影投射外形几乎是必需的。

image

2. 线段分割

光的边界通常设定为一个虚拟的正方形,将正方形的四条边插入线段列表。

分割全部相交线段。

image

可选优化: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

  • 弧度突变区域线段处理

因为端点遍历的开端是最小弧度值的那个,下图这种状况,就会致使先扫到结束端,扫描快结束的时候才扫到开始端。

image

不经优化的作法是不构建三角网格先扫描一轮(原文中的作法),这样结束时open里就有当前扫描的线段(初始扫描射线穿过的线段)。优化的作法应该在前面排序处理弧度突变线段时处理。

5. 构建Mesh

image

源码

link