大世界稀疏八叉树GI
基础思路
unity的GI,在无动态光源项目里,一般采用 distance shadowmask的烘焙模式。
这里不讨论直接光照与阴影 只讨论间接光的计算方式 是这样的情况
- 静态物体间接光漫反射 由Lightmap提供
- 静态物体间接光高光反射 由ReflectionProbe提供
- 动态态物体间接光漫反射 由LightProbe提供
- 动态物体间接光高光反射 由ReflectionProbe提供
这种模式在一般小场景问题不大,但是到了大世界就会遇到很多阻碍。根据我这4年大世界开发经历,先总结一下间接光方面的痛点。
- 大世界的LightProbe 如果稀疏摆放 工作量很大,如果均匀摆放对应一块块3dTex ,哪怕支持不同位置不同密度占用内存和硬盘也很大。
- Lightmap 与probe的 烘焙都很慢,虽然可以分块烘焙但为了解决接缝需要相邻处加载周围地块一些物件一起烘焙。
- Lightmap 内存、显存占用很大,记得一次2公里x2公里场景就需要500M.改善方式也有划区域烘焙,然后对Lightmap做成TextureStreaming 甚至VirtualTexture。但硬盘占用省不了。
- 实际场景美术的灯光组 验证烘焙结果只看Lightmap效果,不会仔细查看每个位置的LightProbe 放入人物后亮度是否匹配。发现问题 调灯光需要重新烘焙一次非常重度。这一条理论上是人的问题可以避免,但还是要考虑真实存在的境况。
- 有些小型的静态物件 为了省Lightmap与烘焙速度 会选择LightProbe模式,与Lightmap的部分也不匹配,中型静态物件 或树木,用Lightmap 消耗不起,用LightProbe只能逐个对象分配效果不行,用LightProbeProxyVolume,性能更差。特别是移动的中型物体 在cpu 每帧插值出大量probe数据性能又差了一些。
- 相同的材质,因为分配的LightProbe数据不同而无法合批渲染。
以上问题虽然都能想出相应的改善措施,但并不容易一起解决。而主要问题都是出在 间接光的漫反射上。所以 如果让间接光的漫反射,统一都用gpu端的SH(物体不需要cpu分配 在shader内 根据WorldPos 插值采样 记录了2阶或3阶SH数据的 tex3D)那么整个世界瞬间清静了。后来查了下这种方式应该叫 Irradiance Volume。第一次见到这个可以先看大佬的文章:
针对这些痛点,需要针对性的给出一些列更统一的方案,大致分这4个方面。
- 自动摆放probe
- 适合大世界的 SH 烘焙工具
- 稀疏八叉树存储
- clipmap的实现
先简单解释下然后挨个展开描述
为了提高美术工作效率,自动摆放probe当然是好的,实在实现的不好,就自动个大概再手工修特殊区域。
不管是unity内置的各种烘焙,还是 bakery,还是导入ue烘焙。生成SH的方式都不够快。所以利用之前自己的拍摄低分辨率cubemap(radiancemap)->irradiancemap->sh 实现自己烘焙工具。
均匀的 Irradiance Volume 存储为tex3D,会导致存储量太大,所以 用稀疏八叉树来存probe,但为了八叉树的相邻节点插值效率,需要存储8个角点的数据,这里会存在重复记录,算适量的空间换时间吧。具体做法是参考全境封锁的做法。
最后如果shader直接采样八叉树数据会存在2个问题,一个是需要多次跳转树节点(在gpu端是 csbuffer ),会有 7,8次 采样,而且缓存命中较差。另一个问题更大 2个大小不同的 相邻八叉树格子 插值出的结果不同会跳变,所以要用 computeshader 实时把八叉树数据 写入一个tex3D,让shader采样,因为一次的三线性插值采样就能同时解决这二个问题。如果是很小的图 这样就够了,但大世界,tex3D不能在高精度情况下满足这种尺寸开销。所以需要做clipmap。下面是借图描述,实际上probe 打算采用2阶SH,plancement 仔细看是非均匀的摆放方式。
自动摆放probe
基本思路是用八叉树节点放一个probe,根据规则确定每个节点当前位置是否要细分。严谨来说,需要考虑这几点,
- 是否在射灯 点光源范围内,如果在一般位置不同都会变化那么需要细分
- 是否附近有mesh存在,如果存在他可能会阻挡光照 或 他本身材质不同 反弹的光线也会不同 需要细分
- 是否处于天光可见性的分界处,除了直接光的反弹,间接光里还有一个超大比重部分 是天光。
但一定要注意如果你不是中台的引擎开发要考虑最全的功能支持,你一定要针对具体项目做出取舍。比如我觉得很多项目,并没有多么丰富多彩的表面反弹,反而是 室内外的区别(天光可见性分界处)才是最重要的,也是性价比最高的。有时候真的仅仅实现他就够了的。原神和对马岛 都有单独区分室内外的做法。
下图示意 八叉树根据附近是天光可见性是否变化 决定细分。分别进行 1,2,3次细分的结果。可以将探针设置在八叉树网格的中心,也可以设置到8角处。 后者会存在大量位置重复的probe 根据烘焙情况考虑是否去重,如果unity自己烘焙,会自动去除重复位置的probe的。
烘焙后看下效果,可以看到 室内外的间接光漫反射变化 已经基本符合环境了,但是在门口处会发黑。这是因为 八叉树也是一种规则摆放,这种摆放难免会出现在墙壁内部的情况。
![](https://pic1.zhimg.com/v2-ca2967235142ed61b6b20cda947d390c.jpg?source=382ee89a)
修复的方案是如果在碰撞体内部 需要做2个小处理
- 寻找一定范围内离自己最近的外表面(可用远处射线 连续多次射向自己)
- 如果没有找到最近的外表面,删除这个位置的 探针,比如大量的地面下探针可删除
SH的数据处理
具体需要哪些数据 如何组织呢?我们可以看下,unity是如何用SH数据参与计算的,比如我们这里用到2阶,那么builtin里可以找到这个函数,还有那张经典的SH图。可以知道 前2阶 需要4个球,但每个球需要用3个float表示颜色。所以需要12个float,这与函数内提供的float总数量一致。
但具体的转换关系,需要查询已经做好推导的公式。 https://www. jianshu.com/p/99f4775c9 3b9 写代码验证下只输出 unity_SHAr 的4个分量对比,除了ui显示的精度问题 是对的上的。
List<Vector4> CalculateSHVairentMimicUnity(SphericalHarmonicsL2 sh)
List<Vector4> Y = new List<Vector4>();
for (int ic = 0; ic < 3; ++ic)
Vector4 coefs = new Vector4();
coefs.x = sh[ic, 3];
coefs.y = sh[ic, 1];
coefs.z = sh[ic, 2];
coefs.w = sh[ic, 0] - sh[ic, 6];
Y.Add(coefs);
for (int ic = 0; ic < 3; ++ic)
Vector4 coefs = new Vector4();
coefs.x = sh[ic, 4];
coefs.y = sh[ic, 5];
coefs.z = sh[ic, 6] * 3.0f;
coefs.w = sh[ic, 7];
Y.Add(coefs);
Vector4 coefs = new Vector4();
coefs.x = sh[0, 8];
coefs.y = sh[1, 8];
coefs.z = sh[2, 8];