网站开发的技术流程图,wordpress 关闭自动升级,手机绘图app软件下载,网上下载的免费网站模板怎么用一、什么是 Cluster Light#xff0c;它具体如何实现多点光源效果#xff1f;
对于移动设备#xff0c;如何支持场景中大量的实时点光源一直以来都是比较棘手的问题#xff0c;因此对于过去#xff0c;往往有如下两种常规方案#xff1a;
静态点光源直接烘焙#xff0…一、什么是 Cluster Light它具体如何实现多点光源效果
对于移动设备如何支持场景中大量的实时点光源一直以来都是比较棘手的问题因此对于过去往往有如下两种常规方案
静态点光源直接烘焙光源本身依靠自发光 Bloom 出效果动态点光源标记最重要的 1~4 盏shader 中只计算这些标记为 Important 的点光源的贡献
但如果场景中有大量的点光源又或者说点光源的数量、位置无法预知那么前两种方案就会完全不可行除此之外对于方案②场景中不同位置的点光源也很难分辨出哪个是 “Important” 的毕竟这个标签若要跟着场景走你很难说哪个光源重要或者不重要
因此基于空间划分后分块计算光照的思路就应运而生其可以在保证效果的同时减少 PixelShader 的计算量大体思路也很简单看一张图就多多少少有所理解 此图来源GDC2015Thomas Gareth Advancements in Tile-Based 有兴趣可以翻看当年的 PPT这里直接上重点一个简单的多点光源 ClusterLight 思路如下
ViewSpace即摄像机可见区域分块GPU ComputeShader计算每个块Cluster会受到哪些点光源影响在着色时根据像素获取对应的 Cluster并拿到光照列表正常计算点光源着色
1.1 简单 ClusterLight 实现 可参考链接这些都是知乎上个人实现的 ClusterLight Demo除此之外URP12 之后的版本也支持 PC 下的 ClusterLight大部分情况ClusterLight 方案都不包含阴影关于阴影的问题后面也会提到 Unity SRP学习笔记二Cluster Based Light CullingUnity SRP 实战四Cluster Based Lighting实时动态多光源渲染-Cluster Forward ShadingCluster Based Deferred Lighting-MaxwellGeng 1.1.1 第一步按照 ViewSpace 切割 Cluster
摄像机的可见区域即摄像机的视锥体按照 X, Y, Z 等距切割成多个 Cluster这里没有复杂的数学公式加上其没有前后依赖计算因此可以并行交给 ComputeShader 非常的合适 X, Y 轴的切割没啥好说的直接等距切割就 OK传统的 Frustum Light 方式 Z 方向不切割Frustum 截锥体如下图如果继续按 Z 轴切割就是本文的 Cluster 了 图片来源自https://www.3dgep.com/forward-plus/ 考虑到离摄像机越远的位置相同距离的 对于屏幕 pixel 的贡献就可能越小因此 Z 轴可以按照距离指数分割即离摄像机越远Z 方向上 Cluster 分的块就越大反之越小不过这个对于 Deferred Lighting或是在屏幕空间进行的光照计算优化明显目前测试下来 Forward 物体着色时计算光照优化不明显因此依然可以仅等距划分 Z
1.1.2 第二步收集光照信息再次计算每个 Cluster 包含哪些光源
到此 GPU 需要获取两个 StructuredBuffer一个是 ClusterBox 对应的 List每个 Cluster 数据包含八个 Vector3对应锥体的每个顶点 另一个则是场景中所有点光源列表
protected struct PointLight
{public Vector4 color;public Vector4 position;
};
其中 Color 的第四维为光源强度position 第四维存储光源半径
一样可以并行计算对于每个 Cluster 做一个几何判断即当前 Cluster 与球体是否相交计算方案有很多这里提供两个正确的经典思路
一是求出 Cluster 对应的 AABB 方形包围盒之后判断这个包围盒是否与球体相交这个方案相对后者计算量没那么大缺点就是可能会有浪费可能 Cluster 不会受到某个光源影响但仍然会统计这个光源
bool TestSphereVsAABB(float4 s, AABB aabb)
{float3 center (aabb.max1.xyz aabb.min1.xyz) * 0.5f;float3 extents (aabb.max1.xyz - aabb.min1.xyz) * 0.5f;float3 vDelta max(0, abs(center - s.xyz) - extents);float fDistSq dot(vDelta, vDelta);return fDistSq s.w * s.w;
}
二是对于 Cluster 的每个面判断面交6个面计算六次优点就是精准但是要额外考虑光源球体完全在 Cluster 内部的情况因此还要计算下空间相对位置比较麻烦
同样这一部分也交给 GPU Compute 计算最后可以得到一张光源分配查找表即每个 Cluster对应一个 LightList即该 Cluster 会接收到的点光源的列表不过考虑到每个列表的大小必然不会一样GPU 没法申请 动态大小的 List因此为了避免空间浪费可以多加一个 LightIndexList 用于存储光源索引此时光源查找表可以只记录每个 Cluster 对应的 LightIndexList 的起点和数量最后通过 LightIndexList 查询对应连续的一段内存来获取 LightList 的实际索引 [numthreads(32, 32, 1)]
void LightAssign(uint3 tid : SV_GroupThreadID, uint3 id : SV_GroupID)
{// cluster ID uint i tid.x, j tid.y, k id.x;uint3 clusterId_3D uint3(i, j, k);uint clusterId_1D Index3DTo1D(clusterId_3D);ClusterBox box _clusterBuffer[clusterId_1D];uint startIndex clusterId_1D * _maxNumLightsPerCluster;uint endIndex startIndex;for(int lid 0; lid _numLights; lid){PointLight pl _lightBuffer[lid];if(!ClusterLightIntersect(box, pl))continue;_lightAssignBuffer[endIndex] uint(lid);}LightIndex idx;idx.count endIndex - startIndex;idx.start startIndex;_assignTable[clusterId_1D] idx;
}
需要注意的是这个数据量还是不小的假设 ViewSpace X, Y, Z 分别按照 32, 32, 64 的大小切割限制每个 Cluster 最多受到 8 盏点光源影响这样就需要至少 32 * 32 * 64 * 8 524288 的 lightAssignID 大小
后续会介绍这一部分怎么优化或者是否有其它的存储方式
1.1.3 第三步光照计算
这一部分就比较简单了着色时判断当前 pixel 在哪个 cluster 中获取其光照索引表拿到其光照之后就是标准的遍历点光源计算光照出于性能考虑的话可以直接用最简单的兰伯特光照模型
#if defined(VIEW_CLUSTER_LIGHT)float2 scrPos i.scrPos.xy / i.scrPos.w;float depth LinearEyeDepth(i.pos.z, _ZBufferParams);float A LinearEyeDepth(0, _ZBufferParams);float B LinearEyeDepth(1, _ZBufferParams);// 计算 Cluster Based Lightinguint x floor(scrPos.x * _numClusterX);uint y floor(scrPos.y * _numClusterY);#if UNITY_REVERSED_Zuint z (_numClusterZ - 1) - floor(((depth - B) / A) * _numClusterZ);#elseuint z floor(((depth - A) / B) * _numClusterZ);#endif
#endif
不过考虑到 DirectX 和 GL 的平台差异还要处理一下 Reserve-Z 问题在计算 Cluster 的 Z 索引时对于 Reversed-Z 的平台要把 Z 部分的计算反过来那分割数量减去它这和你 ComputeShader 中的计算逻辑也有一定关系在 ComputerShader 中考虑好 Reserve-Z 应该也是可行的
光照计算部分代码就不贴了可以根据实际情况选择使用任意光照模型 还需要注意的是点光源的阴影计算并不包含其中依旧需要额外处理阴影的问题并且对于 foward 管线这部分没有高性能的方案一个简单地思路就是对于每个点光源求出其 CubeShadowmap对于多个点光源可以得到一个 TexCubeArray着色时通过 index 读取采样 shadowmap 1.2 世界空间 ClusterLight 分割
前面介绍的就是经典的 ViewSpace 分割方案但是技术一定是要依赖需求去动态调整的生搬硬套没有意义考虑到大多数手机游戏点光源往往都是静态烘焙的做法根本没有必要上动态的多光源
而需要动态点光源的可能是一些特殊的场景又或者是点光源位置不能确定的场景例如家园玩家可以任意摆放建筑和摆饰而部分摆饰会有光源又或者说是空间小室内场景。对于这些需求的特点就是我们没有必要将点光源和大世界绑定在一起对于如上情况而言可以布置或者说会出现动态点光源的空间是有限的此时按照世界空间分 Cluster 就成了一种可行的选择并且相对于 ViewSpace 的分块后者无需在摄像机改变视角时实时更新性能也会更好
既然是世界空间的分割那么分割范围大小就要有严格限制可以通过放置 Box 来确定其光源生效范围后续只对这个 Box 内的空间进行分割及光照计算 而后续计算 Cluster 对应光照数据的流程和前者 View-Space 的计算流程没有差异甚至可以共用一个 ComputeShader最后在着色时也无须考虑平台差异
#if defined(VIEW_CLUSTER_LIGHT)…… ViewSpace Cluster
#elsefloat3 dir maxBound - minBound;float X (worldPos.x - minBound.x) / dir.x;float Y (worldPos.y - minBound.y) / dir.y;float Z (worldPos.z - minBound.z) / dir.z;uint x floor(X * _numClusterX);uint z floor(Y * _numClusterZ);uint y floor(Z * _numClusterY);
#endif 二、移动平台性能优化及兼容
很可惜如果是无脑上 ClusterLight 的话无论是自己的方案或者是 URP UE 自带的方案在大部分中配机型上都很难跑的动甚至是不兼容抛开阴影不谈经过测试原因主要如下
部分手机理论应该支持 ComputeShader但是仍旧会出现进游戏闪退等问题对于①深度了解主要又分两种情况手机驱动尽管是 OpenGL3.1 以上版本但仍不支持支持 ComputeShader但是不支持 StructuredBufferSSBO可以进入游戏但是有很明显的掉帧主要原因出在 StructuredBuffer pixelShader 访问上
2.1 StructuredBuffer pixelShader 优化 StructuredBuffer 测试报告.md 对于 ClusterLight 的数据往往都通过 StructuredBuffer 存储这样是非常常见的操作
RWStructuredBufferClusterBox _clusterBuffer;
RWStructuredBufferPointLight _lightBuffer;
RWTexture3Dfloat4 _assignTable;
但是在 pixelShader 中读取 StructuredBuffer 对于部分手机而言会出现非常离谱的帧数下降
以 Mi6骁龙835为例正常使用 StructuredBuffer 未优化的性能如下 该数据对应测试场景可以见图 1.1.3下同 整体稳定在 30FPS但是运行一段时间后手机会降频735MHz - 515MHz此时无法稳定 30FPS
之前存储灯光是使用 StructuredBuffer 的一般情况下场景中的灯光都会有最大数量限制如果最大数量限制 128 个那么实际操作上就可以使用一个长度为 64 的 Matrix[] 来存储至多 128 盏光源信息
Matrix[64] 必然可以定义在 ConstantBuffer 中而非 StructuredBuffer前者读写性能远好于后者
float4x4 _lightBuffer[64];
float4x4 lit _lightBuffer[lightId / 2]; // 根据 id 查灯光表
float4 litPosition lit[(lightId % 2) * 2 1];
float4 litColor lit[(lightId % 2) * 2];
在仅做了这步优化后实机测试性能就有了肉眼可见的提升 可以看到原先 30FPS 提升到了 50FPS这也证明了这个优化路线是正确的 关于 StructuredBuffer 在移动设备上出现严重性能下降的原因推测 当前设备并不能很好的支持在 pixel 中访问 StructuredBuffer 或者本质上不支持 StructuredBuffer因此在使用 StructuredBuffer 时为了避免更坏情况直接闪退 or 不执行对应的逻辑会退化从而在读写上出现了不可预料的时间消耗 2.1.1 使用 Texture3D
除此之外每个 ClusterBox 存储光源信息也可不使用 StructuredBuffer 而是用 Texture3D 替代其 Texture3D 的每个 pixel 正好和切割后的每个 Cluster 一一对应
private int numClusterX 32;
private int numClusterY 32;
private int numClusterZ 32;
public JClusterCPUGenerate(int Z, BoxCollider collider)
{numClusterZ Z;worldBox collider.bounds;assignTable new Texture3D(numClusterX, numClusterY, numClusterZ, TextureFormat.RGBAFloat, true);Color[] colors new Color[numClusterX * numClusterY * numClusterZ];for (int i 0; i colors.Length; i){colors[i] Color.black;}assignTable.SetPixels(colors);
}
Texture3D 只能支持4通道也就是最大 ARGBFloat这就意味着没有特殊操作的话每个 Cluster 只能存储最多4盏灯这样的话还是需要像前面 1.1.2 的操作一样这里只存储两个 Int 数据对应 lightBuffer 的起始 Index 和灯光数量在着色计算时最终从 LightBuffer 里获取灯光这样就没有灯光数量限制了
此时 Texture3D 也可以使用 RGInt 格式足够使用 Texture3D 而非 StructuredBuffer 性能可以得到进一步提升 可以看到目前设备已经能够稳定 60FPS尽管运行一段时间后手机仍会降频
2.2.2 灯光数据位存储
前面提到过Texture3D 只能支持4通道也就是最大 ARGBFloat这就意味着没有特殊操作的话每个 Cluster 只能存储最多4盏灯但是真的只能存储4盏灯嘛考虑到场景中的灯光数量必然有一个上限以128的上限为例其灯光 Index 必然是在 0~127 的范围内此时对于 RGBAFloat 或 RGBAInt 格式一个通道就可以存储4盏灯光
例如一个 Cluster 受到第241636这四盏灯光影响那么其第一个通道的数据存储值就为 00000001 00000010 00001000 00100100 526337
在实际获取数据时再通过位运算就可以解算其灯光索引考虑到位运算效率很高因此不会带来太大的性能问题除此之外一张 Texture3D 单通道也可以存储至多16盏灯光
如果设置了16盏灯光为单个 pixel 可接受的点光源数量上限那么就无需在申请 LightBuffer 或 lightIndex 这样的额外的 StructuredBuffer 了
[numthreads(32, 32, 1)]
void LightAssign(uint3 tid : SV_GroupThreadID, uint3 id : SV_GroupID)
{// cluster ID uint i tid.x, j tid.y, k id.x;uint3 clusterId_3D uint3(i, j, k);uint clusterId_1D Index3DTo1D(clusterId_3D);ClusterBox box _clusterBuffer[clusterId_1D];uint startIndex 0, endIndex 0;int lightAssignID[16] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};for(int lid 0; lid _numLights endIndex 16; lid){PointLight pl _lightBuffer[lid];if(!ClusterLightIntersect(box, pl))continue;lightAssignID[endIndex] int(lid) 1;}int A (lightAssignID[0] 16) lightAssignID[1];int B (lightAssignID[2] 16) lightAssignID[3];int C (lightAssignID[4] 16) lightAssignID[5];int D (lightAssignID[6] 16) lightAssignID[7];_assignTable[clusterId_3D] float4(A, B, C, D);
}
以上代码为16位存储即每个 int 存储两盏灯光此时最多支持的单个 pixel 灯光上限为8盏事实上手机上也不太好布置太密的点光源一个 pixel 接受8盏光源已足矣否则计算量其实也优化不下来
到此就成功完全弃用了 StructuredBuffer以避免其读写慢带来的致命性能损耗 稳定 60FPS 的同时测试机在一段时间内也没有降频现象 2.2 CPU 实现 ClusterLight ComputeShader 手机兼容性报告 很可惜并非所有手机都能很好的支持 ComputeShader在引用中所有测试的 Android 手机中OpenGL ES 3.1以上的手机均使用的是 Shader Model 4.5 或 Shader Model 5.0在使用 unity 提供的 APISystemInfo.supportsComputeShaders 在上述手机显示为 true但通过运行一段 ComputeShader 程序在 Shader Model4.5 上运行结果却不符合预期
因此如果想要不兼容 CS 的手机也能实现 ClusterLight 动态点光源就需要考虑备选方案也就是使用 CPU 计算原先 CS 计算的部分
不过如果这部分任务交给 CPUCPU 要不考虑使用 JobSystem 来并行计算要不就考虑分帧不然如此大的计算量尽管 WorldSpace 的 ClusterLight 仅需一次计算但仍然会出现卡帧问题 下面给出一个分帧的实现分帧分什么当然是光源 —— 即每帧只处理一盏光源
思路也很简单对于每个当前 Add 的光源以其光源为中心开启 BFS搜索所有受到该光源影响到的 Cluster此时复杂度为线性
private void BFSCluster(PointLight light, bool isAdd)
{int clusterNum numClusterY * numClusterZ;QueueCluster queue new QueueCluster();uint[] flag new uint[clusterNum];Vector3 pos new Vector3(light.position.x, light.position.y, light.position.z);Cluster lightCluster Locate(light, pos);queue.Enqueue(lightCluster);flag[lightCluster.y * numClusterY lightCluster.z] | (uint)1 lightCluster.x;Vector3 perCusterlen new Vector3((worldBox.max.x - worldBox.min.x) / numClusterX, (worldBox.max.y - worldBox.min.y) / numClusterZ,(worldBox.max.z - worldBox.min.z) / numClusterY);while (queue.Count ! 0){Cluster s queue.Dequeue();ChangeLightState(s, light, isAdd);int[,] dirS { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 } };for (int i 0; i 6; i){Cluster n s;n.x dirS[i, 0];n.y dirS[i, 1];n.z dirS[i, 2];if (n.x numClusterX || n.y numClusterZ || n.z numClusterY || n.x 0 || n.y 0 || n.z 0)continue;if ((flag[n.y * numClusterY n.z] (uint)1 n.x) ! 0)continue;if (n.deep 0){if (!CheckLightDir(n, lightCluster, perCusterlen, light.position.w))continue;}queue.Enqueue(n);flag[n.y * numClusterY n.z] | (uint)1 n.x;}}
}
2.2.1 基于 BFS 的光源 Add 方案
关于 BFS代码中为队列实现的广度优先搜索默认看到这里的人都是了解的因此不会介绍这部分算法重点提一下搜索时的标记数组 Flag[]因为该 BFS 为避免重复搜索采用的是记忆化搜索的思路
Flag[] 的大小理论上应该和 Cluster 的数量一致但是如果每帧都申请并清空这样大小的 Flag[]必然也会浪费资源因为 Cluster 的数量可能会过万因此这部分需要做点处理
清空标记处理如果是分帧操作则没必要每帧都重新 new 一个数组也没必要像 memset 一样重置 flag[] 的值可以直接给 flag[] 填上当前的灯光 ID 作为标记此方案和①不兼容不过可以省掉大量的 flag[] 空间一样是利用位运算申请 flag 时只需要申请 X Y 两轴的大小Z 存储到对应的位中不过此方案也要求 Z 不能超过位数uint32上面的代码样例采用的就是这个方案