UE 5.3 OpenVDB代码浅析

前言

UE官方支持VDB的最低版本是5.3, 用的是OpenVDB v8.1.0版本(此版本不支持NanoVDB, NanoVDB的支持得在 OpenVDB v9.0.0版本)

UE5.0、5.1有第三方插件unreal-vdb支持了OpenVDB及NanoVDB

接下来分析的是基于UE5.3官方支持的VDB功能

1. 导入&存储

1. USparseVolumeTextureFactory类, 导入或重导VDB, 包括UAnimatedSparseVolumeTexture及UStaticSparseVolumeTexture
2. 核心导入方法是ConvertOpenVDBToSparseVolumeTexture, 序列和静态VDB都用该方法处理导入
3. 每一个VDB文件都会对应一个FTextureData类实例, 用来缓存导入的数据

FSparseVolumeTextureDataProviderOpenVDB, 在Initialize方法里包装了FTextureData创建信息, 而后Create时生成FTextureData

1.1 页表和TileData数量设置

1.1.1 PageTable数量设置
页表的数量是分辨率内积直接得到, 而页表分辨率是默认构造FHeader时(用CreateInfo里的VirtualVolumeAABB等信息)计算, 每个页表元素是uint32
#define SPARSE_VOLUME_TILE_RES 16
PageTableVolumeResolution.xyz = VirtualVolumeSize.xyz / SPARSE_VOLUME_TILE_RES;
PageTableVolumeResolution.xyz = RoundUpToPowerOfTwo(PageTableVolumeResolution.xyz);

1.1.2 TileData数量设置

TileDataA及TileDataB, 每个TileData是uint8
TileData数量 = 非空Tile数(即有效页数)* 每PaddedTile所含的Voxel数量 * 每Voxel所占用uint8的数量
每个PaddedTile的Voxel数量为
NumVoxelsPerPaddedTile = (16+2*1)*(16+2*1)*(16+2*1);
也就是SPARSE_VOLUME_TILE_RES+上下左右前后各自补一个Voxel
每个Voxel占的大小基于VDB导入设置里属性来
FormatSize[2] = {GPixelFormats[Header.AttributesFormats[0]].BlockBytes, GPixelFormats[Header.AttributesFormats[1]].BlockBytes}
若如下图导入配置, 则FormatSize[0] = 1Byte, FormatSize[1] = 0Byte
Tips: 不支持三通道属性, 若面板每属性配置3通道值, 该属性将占用4通道空间

1.2 页表及TilesData的压缩, 及存储流式数据

FResource::Build方法中, 数据导入完成后, 先压缩数据进StreamableBulkData, 而后存储StreamableBulkData至StreamableMipLevels. (SparseVolumeTexture.cpp)

1.2.1 页表的压缩
CompressPageTable方法, 压缩页表数据进StreamableBulkData
分配两倍非空页表线性连续空间M、N, M每空间用32bit存储三维坐标(X/Y/Z分别占用11/11/10位), N每空间用32bit存储物理Tile的索引

1.2.2 TileData的压缩
CompressTiles方法, 压缩TileData进StreamableBulkData
先将默认FallbackValue从Vector4转成uint8值数组, 用于后续直接与PhysicalTileData数据比对, 过滤掉空Voxel

对非空Voxel全局索引按字(Word)编码成 Key = VIndex / 32, Value = 1u << VIndex % 32
const int64 WordIndex = TileIndex * SVT::NumOccupancyWordsPerPaddedTile + (VoxelIndex / 32);
OccupancyBits[AttributesIdx][WordIndex] |= 1u << (static_cast<uint32>(VoxelIndex) % 32u);

同时统计每Tile下非空Voxel数量
PrefixSums[AttributesIdx][TileIndex]++;
Tile非空体素统计及置位

压缩所有非空Voxel数据

压缩数据内存布局, 从左到右依次存储
OccupancyBits描述物理Volume全局体素非空置位表, 即非空的体素, 索引所在字(Word), 并对其相应bit置位, bit的数量等同于所有Voxel包括非空的数量
TileDataOffsets描述Voxel数量偏移, 俩临近TileDataOffset差表示前一个TileData的非空Voxel数量
TileData描述每Voxel的密度、温度等数据
OccupancyBitsTileDataOffsetsTileData
OccupancyBitsOffset[0]OccupancyBitsOffset[1]TileDataOffsetsOffset[0]TileDataOffsetsOffset[1]TileDataOffset[0]TileDataOffset[1]
OccupancyBitsSize[0]OccupancyBitsSize[1]TileDataOffsetsSize[0]TileDataOffsetsSize[1]TileDataSize[0]TileDataSize[1]
1.2.3 存储流式数据
FResources::Cache(SparseVolumeTexture.cpp)保存磁盘前, 进一步压缩BulkData

2. 纹理

2.1 数据更新至RHI

FStreamingManager::AddInternal方法(SparseVolumeTextureStreamingManager.cpp)中, 拷StreamableMipLevels数据, 通过RootTileUploader上传至CS, 以更新TileDataTextureRHI

2.2 渲染页表及Tile纹理

Shader文件 UpdateSparseVolumeTexture.usf

2.3 采样页表及Tile纹理

用ComputeShader, 渲染至LightTexture


采样HeterogeneousVolumeRadiance纹理, 混合至SceneColor

3. 其他

UE5.3 SVT的一些限制

页表纹理大小限制2GB内, SVT的X、Y值禁止超过32K, Z值禁止超过16K
这个通过上面的分析也好理解, 上面提到VDB的数据在UE5.3里是分Tile, 每Tile分辨率是, 即, 且页表存储的数据X、Y维被压缩至11bit, Z维被压缩至10bit, 也就等同于Voxel的索引X、Y、Z维最大限制分别为32K()、32K()、16K().

4. 小结

目前重心探索VDB的压缩存储方面, 具体渲染没过多深入, 一般镂空Voxel大概占比30%~50%
目前测试的数据不多, 动态的VDB暂没深究, 测试资产包网盘地址见 CloudPackVDB

压缩前[vdb]打包前[内部压缩]Mip0打包后[打包压缩, uasset]压缩比
静态云样例019.2MB约10MB4.39MB22.86%
样例, 原始数据19.2MB, 截图为TileData及PageTable数据压缩后, 打包压缩前

5. 参考

  1. OpenVDB说明文档
  2. UE5.3 Sparse Volume Texture