[toc]
# zbining 算法
这个图写的很明白了,第一次记录视椎体内每个 tile 的灯光列表
第二次记录每个 z 区间内的灯光个数 然后就可以直接找到对应视锥,对应区块内的灯光索引,而不需要对整个空间 Cluster 化
# 代码解析
首先值得注意的是,URP 本身是支持了 XR 的双目渲染,所以添加了 ViewIndex 这一个属性,在本节解析中,将 view 的数量看为 1 即可。 首先先计算有多少 item,item 即除了 Directional 灯光外的灯光与灯光探针的总数量
1 2 3 4 5 6 var visibleLights = renderingData.lightData.visibleLights.GetSubArray(lightOffset, m_LightCount);var reflectionProbes = renderingData.cullResults.visibleReflectionProbes;var reflectionProbeCount = math.min(reflectionProbes.Length, UniversalRenderPipeline.maxVisibleReflectionProbes);var itemsPerTile = visibleLights.Length + reflectionProbeCount;m_WordsPerTile = (itemsPerTile + 31 ) / 32 ;
然后计算最多需要多少 Tile
1 2 3 4 5 6 7 8 9 10 11 m_ActualTileWidth = 8 >> 1 ; do { m_ActualTileWidth <<= 1 ; m_TileResolution = (screenResolution + m_ActualTileWidth - 1 ) / m_ActualTileWidth; } while ((m_TileResolution.x * m_TileResolution.y * m_WordsPerTile * viewCount) > UniversalRenderPipeline.maxTileWords);
分配 Bin 的数量,Bin 的分配也很简单,即计算相机的 nearPlane 和 farPlane,在中间等间隔取一个 Bin
1 2 3 4 5 m_ZBinScale = (UniversalRenderPipeline.maxZBinWords / viewCount) / ((camera.farClipPlane - camera.nearClipPlane) * (2 + m_WordsPerTile)); m_ZBinOffset = -camera.nearClipPlane * m_ZBinScale; m_BinCount = (int )(camera.farClipPlane * m_ZBinScale + m_ZBinOffset);
# 划分灯光的 Z 区间范围
使用 JobSystem 进行实现,其中线程数使灯光的总数量,每个 Job 处理一个灯光
1 2 3 4 5 6 7 8 9 10 11 var minMaxZs = new NativeArray<float2>(itemsPerTile * viewCount, Allocator.TempJob);var lightMinMaxZJob = new LightMinMaxZJob { worldToViews = worldToViews, lights = visibleLights, minMaxZs = minMaxZs.GetSubArray(0 , m_LightCount * viewCount) }; var lightMinMaxZHandle = lightMinMaxZJob.ScheduleParallel(m_LightCount * viewCount, 32 , new JobHandle());
内部的实现如下:其中把 SpotLight 的范围先忽略,流程就是先计算出灯光在裁剪空间的下标,z 方向的范围是直接使用 light 的范围按照 AABB 计算的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 struct LightMinMaxZJob : IJobFor { public Fixed2<float4x4> worldToViews; [ReadOnly ] public NativeArray<VisibleLight> lights; public NativeArray<float2> minMaxZs; public void Execute (int index ) { var lightIndex = index % lights.Length; var light = lights[lightIndex]; var lightToWorld = (float4x4)light.localToWorldMatrix; var originWS = lightToWorld.c3.xyz; var viewIndex = index / lights.Length; var worldToView = worldToViews[viewIndex]; var originVS = math.mul(worldToView, math.float4(originWS, 1 )).xyz; originVS.z *= -1 ; var minMax = math.float2(originVS.z - light.range, originVS.z + light.range); if (light.lightType == LightType.Spot) { } minMax.x = math.max(minMax.x, 0 ); minMax.y = math.max(minMax.y, 0 ); minMaxZs[index] = minMax; } }
# 划分 ZBin
下属代码对 ZBin 进行划分,其中 batchCount 是指每个 job 里面处理的 bin 数量 其中 binCount 的计算公式如下:
即每个 Bin 中存储的是 (2 + 灯光和灯光探针) 的数量,即每个 word 存储的都是灯光的信息,以及多余了 2 个不知道什么的信息 另外,item 是灯光和灯光探针实际的个数,而 word 概念对应的是使用位来存储 item,即每个 word 可以存储 32 个 item 下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var zBinningBatchCount = (m_BinCount + ZBinningJob.batchSize - 1 ) / ZBinningJob.batchSize;var zBinningJob = new ZBinningJob { bins = m_ZBins, minMaxZs = minMaxZs, zBinScale = m_ZBinScale, zBinOffset = m_ZBinOffset, binCount = m_BinCount, wordsPerTile = m_WordsPerTile, lightCount = m_LightCount, reflectionProbeCount = reflectionProbeCount, batchCount = zBinningBatchCount, viewCount = viewCount, isOrthographic = camera.orthographic }; var zBinningHandle = zBinningJob.ScheduleParallel(zBinningBatchCount * viewCount, 1 , reflectionProbeMinMaxZHandle);
Job 内的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 struct ZBinningJob : IJobFor { public const int batchSize = 128 ; public const int headerLength = 2 ; [NativeDisableParallelForRestriction ] public NativeArray<uint > bins; [ReadOnly ] public NativeArray<float2> minMaxZs; public float zBinScale; public float zBinOffset; public int binCount; public int wordsPerTile; public int lightCount; public int reflectionProbeCount; public int batchCount; public int viewCount; public bool isOrthographic; static uint EncodeHeader (uint min, uint max ) { return (min & 0xFFFF ) ((max & 0xFFFF ) << 16 ); } static (uint , uint ) DecodeHeader(uint zBin) { return (zBin & 0xFFFF , (zBin >> 16 ) & 0xFFFF ); } public void Execute (int jobIndex ) { var batchIndex = jobIndex; var viewIndex = jobIndex / batchCount; var binStart = batchSize * batchIndex; var binEnd = math.min(binStart + batchSize, binCount) - 1 ; var binOffset = viewIndex * binCount; var emptyHeader = EncodeHeader(ushort .MaxValue, ushort .MinValue); for (var binIndex = binStart; binIndex <= binEnd; binIndex++) { bins[(binOffset + binIndex) * (headerLength + wordsPerTile) + 0 ] = emptyHeader; bins[(binOffset + binIndex) * (headerLength + wordsPerTile) + 1 ] = emptyHeader; } FillZBins(binStart, binEnd, 0 , lightCount, 0 , viewIndex * lightCount, binOffset); } void FillZBins (int binStart, int binEnd, int itemStart, int itemEnd, int headerIndex, int itemOffset, int binOffset ) { for (var index = itemStart; index < itemEnd; index++) { var minMax = minMaxZs[itemOffset + index]; var minBin = math.max((int )((isOrthographic ? minMax.x : math.log2(minMax.x)) * zBinScale + zBinOffset), binStart); var maxBin = math.min((int )((isOrthographic ? minMax.y : math.log2(minMax.y)) * zBinScale + zBinOffset), binEnd); var wordIndex = index / 32 ; var bitMask = 1u << (index % 32 ); for (var binIndex = minBin; binIndex <= maxBin; binIndex++) { var baseIndex = (binOffset + binIndex) * (headerLength + wordsPerTile); var (minIndex, maxIndex) = DecodeHeader(bins[baseIndex + headerIndex]); minIndex = math.min(minIndex, (uint )index); maxIndex = math.max(maxIndex, (uint )index); bins[baseIndex + headerIndex] = EncodeHeader(minIndex, maxIndex); bins[baseIndex + headerLength + wordIndex] = bitMask; } } } }
这段代码中如果在 view 只有一个的前提下,batchIndex 可以直接等于 jobIndex,viewIndex 可以直接设置为 0。 binStart 和 binEnd 就是当前 job 需要计算的 bin 的下标范围 另外需要注意的一点是,每个 BIN 存储的是灯光最大下标和最小下标的范围,编码使用的是一个 uint,高 16 位存储最大下标,低 16 位存储最小下标。即上述代码中的 EncodeHeader 然后在 FillZBins 中计算每个灯覆盖的最小的 binIndex 和最大的 binIndex 对于以上更新 bin 数据的注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void FillZBins (int binStart, int binEnd, int itemStart, int itemEnd, int headerIndex, int itemOffset, int binOffset ){ for (var index = itemStart; index < itemEnd; index++) { var minMax = minMaxZs[itemOffset + index]; var minBin = math.max((int )((isOrthographic ? minMax.x : math.log2(minMax.x)) * zBinScale + zBinOffset), binStart); var maxBin = math.min((int )((isOrthographic ? minMax.y : math.log2(minMax.y)) * zBinScale + zBinOffset), binEnd); var wordIndex = index / 32 ; var bitMask = 1u << (index % 32 ); for (var binIndex = minBin; binIndex <= maxBin; binIndex++) { var baseIndex = (binOffset + binIndex) * (headerLength + wordsPerTile); var (minIndex, maxIndex) = DecodeHeader(bins[baseIndex + headerIndex]); minIndex = math.min(minIndex, (uint )index); maxIndex = math.max(maxIndex, (uint )index); bins[baseIndex + headerIndex] = EncodeHeader(minIndex, maxIndex); bins[baseIndex + headerLength + wordIndex] = bitMask; } } }
# 划分 Tile - TilingJob
先看一下 Execute 方法:
其中以 TileLight 为例,其他的暂时不看,这个 Job 是对于每个灯执行一次,这个主要是标记每个灯光影响到的 Tile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void Execute (int jobIndex ){ var index = jobIndex % itemsPerTile; m_ViewIndex = jobIndex / itemsPerTile; m_CenterOffset = m_ViewIndex == 0 ? centerOffset.xy : centerOffset.zw; m_Offset = jobIndex * rangesPerItem; m_TileYRange = new InclusiveRange(short .MaxValue, short .MinValue); for (var i = 0 ; i < rangesPerItem; i++) { tileRanges[m_Offset + i] = new InclusiveRange(short .MaxValue, short .MinValue); } if (index < lights.Length) { if (isOrthographic) { TileLightOrthographic(index); } else { TileLight(index); } } else { TileReflectionProbe(index); } }
# 合并 Tile 与 LightRangeJob
这个 Job 是针对于 Tile 进行的,主要是根据上一个 job 中生成的灯光范围来计算 TileMask