[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;
//每个Tile最少需要占多少个word
m_WordsPerTile = (itemsPerTile + 31) / 32;

然后计算最多需要多少 Tile

1
2
3
4
5
6
7
8
9
10
11
//先计算Tile的大小
//Tile目前为4
m_ActualTileWidth = 8 >> 1;
do
{
//每次扩大两倍,
m_ActualTileWidth <<= 1;
m_TileResolution = (screenResolution + m_ActualTileWidth - 1) / m_ActualTileWidth;
}
//直到达到最大的Tileword为止,就是可以分配的最多的Tile
while ((m_TileResolution.x * m_TileResolution.y * m_WordsPerTile * viewCount) > UniversalRenderPipeline.maxTileWords);

分配 Bin 的数量,Bin 的分配也很简单,即计算相机的 nearPlane 和 farPlane,在中间等间隔取一个 Bin

1
2
3
4
5
// Use to calculate binIndex = z * zBinScale + zBinOffset
//计算公式也很简单,就是在nearPlane和farPlane之间取等间距间隔
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
//itemsPerTile即灯光或者lightProbe,即每个Job处理一个灯光
var minMaxZs = new NativeArray<float2>(itemsPerTile * viewCount, Allocator.TempJob);
//计算出每个灯光在z轴上影响的范围
var lightMinMaxZJob = new LightMinMaxZJob
{
worldToViews = worldToViews,
lights = visibleLights,
minMaxZs = minMaxZs.GetSubArray(0, m_LightCount * viewCount)
};
// Innerloop batch count of 32 is not special, just a handwavy amount to not have too much scheduling overhead nor too little parallelism.
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;
//粗略计算灯光的AABB(Z方向),合理推测Tile只划分了xz,没有划分y
var minMax = math.float2(originVS.z - light.range, originVS.z + light.range);
//Spot光源需要另外计算
if (light.lightType == LightType.Spot)
{
//计算SpotLight范围
}

minMax.x = math.max(minMax.x, 0);
minMax.y = math.max(minMax.y, 0);
//存储的是每个灯光的AABB(Z方向)
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
//每个Batch计算128个Bin,总共划分多少个Job线程
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
{
// Do not use this for the innerloopBatchCount (use 1 for that). Use for dividing the arrayLength when scheduling.
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)
{
//?有啥意义,job本身就是第i个batch,这个是因为XR可能有两个View所以这样做的吧
var batchIndex = jobIndex; //% batchCount;
//viewIndex可以理解,只要不超过
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;
}

// Regarding itemOffset: minMaxZs contains [lights view 0, lights view 1, probes view 0, probes view 1] when
// using XR single pass instanced, and otherwise [lights, probes]. So we figure out what the offset is based
// on the view count and index.

// Fill ZBins for lights.
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++)
{
//item的Z深度
var minMax = minMaxZs[itemOffset + index];
//计算出每个item的最小bin和最大bin
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);
//然后对于每个bin进行赋值
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++)
{
//item的Z深度
var minMax = minMaxZs[itemOffset + index];
//计算出每个item的最小bin和最大bin
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);

//将灯光的index写入到最小到最大的bin中,这个32到底是什么,是指最多32个灯光么
//灯光是item,word是一个int32位码,wordIndex算的是根据lightIndex计算是第几个word
var wordIndex = index / 32;
var bitMask = 1u << (index % 32);
//然后对于每个bin进行赋值
for (var binIndex = minBin; binIndex <= maxBin; binIndex++)
{
//binoffset在view为一个时,默认为0,每个bin存储的是 2个header和每个bin里有多少item
var baseIndex = (binOffset + binIndex) * (headerLength + wordsPerTile);
//这里就是找到对应的bin -> bins[baseIndex](灯光)
var (minIndex, maxIndex) = DecodeHeader(bins[baseIndex + headerIndex]);
//更新每个bin的最小与最大灯光的index
minIndex = math.min(minIndex, (uint)index);
maxIndex = math.max(maxIndex, (uint)index);
//更新最大与最小
//baseIndex偏移headerIndex
//第一个很好说,就是直接encoder
//第一个header存储最小到最大的下标
bins[baseIndex + headerIndex] = EncodeHeader(minIndex, maxIndex);
//第二个是更新第i个灯光在的word里面的灯光列表
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