前言
UE4 预烘焙的光照信息存储在离线构建的贴图“簇”中,包含存储颜色信息的 LightMap 贴图和存储静态阴影信息的 ShadowMap 贴图。
LightMap 贴图分为 PC/主机平台使用的高质量(HQ)和移动平台使用的低质量(LQ)两种类型。从贴图格式上来说,高质量 LightMap 贴图格式为 RGBA32 ,保留了 Alpha 通道用于存储亮度信息,而低质量 LightMap 贴图格式为 RGB24,颜色通道预乘了亮度信息[1]。
ShadowMap 贴图存储了场景静态的阴影遮挡关系,与实时 Cascade Shadow Mapping 结合计算阴影:CSM 范围内的阴影通过实时计算得到,范围外的阴影从预烘焙的静态贴图中获取。ShadowMap 贴图的 RGBA 通道可保存最多 4 个方向光的静态阴影。
UE4 的实时阴影也叫 ShadowMap,本文的 ShadowMap 都是指的静态ShadowMap,个人感觉 Unity 的 ShadowMask 叫法更合适。
在 UE4 的移动端灯光方案中,ShadowMap 贴图只有 1 个通道的静态阴影生效,对应的贴图格式为 G8,低质量 LightMap 贴图格式为 RGB24,在默认的 ASTC6x6 压缩格式下,同尺寸不带 Alpha 通道和带 Alpha 通道的贴图内存占用是一样的(效果会有差异)[2] , 能不能利用一下低质量 LightMap 贴图“浪费”的 Alpha 通道?把只有 1 个通道生效 ShadowMap 贴图合并进 LightMap 贴图 Alpha 通道,这样的话,移动端静态 Mesh 的烘焙光照渲染管线可以少一张 ShadowMap 贴图的内存占用和采样,提升渲染效率。
本文将介绍 ShadowMap 贴图合并进 LightMap 贴图 Alpha 通道的主要实现过程。
实现过程
参与烘焙的静态 Mesh 顶点的 UV1 保存 LightMapUV,结合坐标 CoordinateScale 和 CoordinateBias,“标识” Mesh 在贴图中的采样区域。静态烘焙 Mesh 保存两套坐标 CoordinateScale 和 CoordinateBias,分别用于 LightMap 和 ShadowMap 贴图的采样。
- 静态烘焙 Mesh 的 ShadowMap 贴图采样区域示意图
贴图合并
场景 umap 文件同级目录的 BuiltData.uasset 包含场景烘焙生成的数据。通过 GWorld 可以拿到运行时的烘焙数据,遍历所有静态烘焙 Mesh 的 FMeshMapBuildData 可以收集到所有关联的 LightMapTexture 和 ShadowMapTexture。
贴图合并的基本操作是 Texel-By-Texel 的拷贝——遍历 LightMapTexture 的所有 Mipmaps,把对应 Mip 级别的 ShadowMapTexture 的 Texel 值依次按坐标赋给 LightMapTexture 对应 Texel 的 A 值:
const int32 TextureSizeX_LightMap = LightMapTexture->GetSizeX();
const int32 TextureSizeY_LightMap = LightMapTexture->GetSizeY();
const int32 TextureSizeX_ShadowMap = ShadowMapTexture->GetSizeX();
const int32 TextureSizeY_ShadowMap = ShadowMapTexture->GetSizeY();
int32 MipBias_ShadowMap = 0;
{
int32 MipSizeX_ShadowMap = TextureSizeX_ShadowMap;
while (MipSizeX_ShadowMap > TextureSizeX_LightMap * (1 - LightMapTextureUVBias.X))
{
MipBias_ShadowMap++;
MipSizeX_ShadowMap = FMath::Max(1, TextureSizeX_ShadowMap >> MipBias_ShadowMap);
}
}
const int32 NumMips = LightMapTexture->Source.GetNumMips();
check(NumMips > 0);
for (int32 MipIndex = 0; MipIndex < NumMips; ++MipIndex)
{
FColor* MipData_LightMap = (FColor*)LightMapTexture->Source.LockMip(0, 0, MipIndex);
TArray64<uint8> MipData_ShadowMap;
bool bHasMipData_ShadowMap = ShadowMapTexture->Source.GetMipData(MipData_ShadowMap, MipIndex + MipBias_ShadowMap);
if (!bHasMipData_ShadowMap)
{
continue;
}
const int32 MipSizeX_LightMap = FMath::Max(1, TextureSizeX_LightMap >> MipIndex);
const int32 MipSizeY_LightMap = FMath::Max(1, TextureSizeY_LightMap >> MipIndex);
const int32 MipSizeX_ShadowMap = FMath::Max(1, TextureSizeX_ShadowMap >> (MipIndex + MipBias_ShadowMap));
const int32 MipSizeY_ShadowMap = FMath::Max(1, TextureSizeY_ShadowMap >> (MipIndex + MipBias_ShadowMap));
for (int32 SrcY = 0; SrcY < MipSizeY_ShadowMap; SrcY++)
{
for (int32 SrcX = 0; SrcX < MipSizeX_ShadowMap; SrcX++)
{
int32 DstX = SrcX + LightMapTextureUVBias.X * MipSizeX_LightMap;
int32 DstY = SrcY + LightMapTextureUVBias.Y * MipSizeY_LightMap;
FColor& DstColor = MipData_LightMap[DstY * MipSizeX_LightMap + DstX];
DstColor.A = MipData_ShadowMap[SrcY * MipSizeX_ShadowMap + SrcX];
}
}
// Unlock all mip levels.
for (int32 MipIndex = 0; MipIndex < NumMips; ++MipIndex)
{
LightMapTexture->Source.UnlockMip(0, 0, MipIndex);
}
}
需要 注意 的是:
- 静态烘焙 Mesh 关联的
LightMapTexture和ShadowMapTexture的尺寸不一定相同,合并的时候以LightMapTexture的尺寸为上限。如果ShadowMapTexture尺寸小,那么是对LightMapTexture的 Alpha 通道的部分区域赋值;如果ShadowMapTexture尺寸更大,则偏移其 Mip 直到LightMapTexture能够放下。
缩小
ShadowMapTexture的尺寸会有阴影精度损失,但实际碰到ShadowMapTexture尺寸比LightMapTexture大的情况并不多,比如模型接受了一个大阴影,但 LightMap 分辨率又给的很低。
- 分布在同一张
LightMapTexture的所有静态烘焙 Mesh,并不一定分布在同一张ShadowMapTexture上,所以LightMapTexture和ShadowMapTexture并不是一一对应的关系,合并的时候把关联了同一张LightMapTexture的静态烘焙 Mesh 的所有ShadowMapTexture依次合进LightMapTexture的 Alpha 通道,直到LightMapTexture占满。
这种合并策略充分利用了实际大部分情况下,
ShadowMapTexture尺寸比LightMapTexture小,且LightMapTexture跟ShadowMapTexture是一对多的特点。
- 贴图合并后需要重新计算静态烘焙 Mesh 用于
ShadowMapTexture采样的坐标缩放和偏移。因为原坐标缩放偏移是对单张的ShadowMapTexture采样用的,合并后ShadowMapTexture变成了LightMapTexture的一部分,缩放偏移发生了改变(同尺寸合并,缩放偏移不变)。
原 ShadowMap 采样坐标公式为:
贴图合并后的 ShadowMap 采样坐标公式为:
LightMapTexture对应多个ShadowMapTexture合并示意图
FShadowMap2D 修改
FShadowMap2D增加一套合并相关的坐标 Scale 和 Bias :
/** Whether the shadow-map is stored in the alpha channel of the low-quality light-map. */
bool bUseLQLightMapAlphaChannel;
/** The scale which is applied to the shadow-map coordinates, only available when bUseLQLightMapAlphaChannel is true. */
FVector2D UseLQLightMapAlphaChannel_UVScale;
/** The bias which is applied to the shadow-map coordinates, only available when bUseLQLightMapAlphaChannel is true. */
FVector2D UseLQLightMapAlphaChannel_UVBias;
FShadowMap2D的GetInteraction接口修改为平台相关:
FShadowMapInteraction FShadowMap2D::GetInteraction(ERHIFeatureLevel::Type InFeatureLevel) const
{
if (Texture)
{
bool bHighQuality = AllowHighQualityLightmaps(InFeatureLevel);
if (!bHighQuality && bUseLQLightMapAlphaChannel)
{
return FShadowMapInteraction::Texture(Texture, CoordinateScale * UseLQLightMapAlphaChannel_UVScale, CoordinateBias * UseLQLightMapAlphaChannel_UVScale + UseLQLightMapAlphaChannel_UVBias, bChannelValid, InvUniformPenumbraSize, true);
}
return FShadowMapInteraction::Texture(Texture, CoordinateScale, CoordinateBias, bChannelValid, InvUniformPenumbraSize);
}
return FShadowMapInteraction::None();
}
上述代码中的 GetInteraction 接口运行时判断在移动端低质量贴图模式且 bUseLQLightMapAlphaChannel 为 true 的情况下,ShadowMapTexture 的采样坐标需要套用贴图合并的坐标 Scale 和 Bias,CoordinateScale 和 CoordinateBias 传递的值见前面的 ShadowMap 采样坐标公式推导。
新增加一套贴图合并相关的坐标 Scale 和 Bias,在运行时重新计算采样坐标,而不是直接在贴图合并的时候改掉原
CoordinateScale和CoordinateBias,是因为原坐标偏移缩放是 PC 和移动端通用的,直接改掉会使得非 ES3.1 模式下 ShadowMap 显示错误。
FShadowMap2D烘焙数据系列化修改:
void FShadowMap2D::Serialize(FArchive& Ar)
{
FShadowMap::Serialize(Ar);
if (Ar.IsCooking())
{
...
else if (Ar.CookingTarget()->PlatformName().Contains("Android") && bUseLQLightMapAlphaChannel)
{
Ar << GEngine->DefaultTexture;
}
else
{
Ar << Texture;
}
}
else
{
Ar << Texture;
}
...
if (Ar.UE4Ver() >= VER_UE4_STATIC_SHADOWMAP_USELQLIGHTMAPALPHACHANNEL)
{
Ar << bUseLQLightMapAlphaChannel << UseLQLightMapAlphaChannel_UVScale << UseLQLightMapAlphaChannel_UVBias;
}
}
FShadowMap2D 关联的 ShadowMapTexture 合并进 LightMapTexture 之后,原 ShadowMapTexture 不再序列化到烘焙数据。其中,代码用 GEngine 的 DefaultTexture 做占位用,防止其他代码的空指针异常。
引擎自带的
DefaultTexture尺寸很小,且 UE4 相同贴图只会序列化一份,所以不会有额外内存开销。
管线修改
- 增加新的烘焙光照渲染策略类型以支持贴图合并模式:
LMP_MOBILE_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP,
LMP_MOBILE_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP_ALPHACHANNEL,
- 烘焙光照渲染策略修改:
template< bool UseLQLightMapAlphaChannel >
class TMobileDistanceFieldShadowsAndLQLightMapPolicy : public TDistanceFieldShadowsAndLightMapPolicy<LQ_LIGHTMAP>
{
typedef TDistanceFieldShadowsAndLightMapPolicy<LQ_LIGHTMAP> Super;
public:
...
static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
Super::ModifyCompilationEnvironment(Parameters, OutEnvironment);
FMobileDirectionalLightCSMPolicy::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("USE_LQ_LIGHTMAP_ALPHACHANNEL"), UseLQLightMapAlphaChannel);
}
...
};
引擎原有的 FMobileDistanceFieldShadowsAndLQLightMapPolicy 修改为模板类,根据类型参数 UseLQLightMapAlphaChannel 值来定义用于 Shader 的 USE_LQ_LIGHTMAP_ALPHACHANNEL 变体。
Shader 修改
如果定义了 USE_LQ_LIGHTMAP_ALPHACHANNEL ,则从 LightMapTexture 的 Alpha 通道获取 Shadow 值:
// Fetch the distance field data
#if USE_LQ_LIGHTMAP_ALPHACHANNEL
half DistanceField = Texture2DSample(LightmapResourceCluster.LightMapTexture, LightmapResourceCluster.LightMapSampler, ShadowMapCoordinate).a;
#else
half DistanceField = Texture2DSample(LightmapResourceCluster.StaticShadowTexture, LightmapResourceCluster.StaticShadowTextureSampler, ShadowMapCoordinate).r;
#endif
结果
上图为真机上 ThirdPerson 测试场景的 RenderDoc 截图和 memreport -full 的内存对比截图,可以看出贴图合并后静态烘焙 Mesh 的烘焙光照渲染管线可以少了 ShadowMapTexture 采样,内存方面也没有了 ShadowMap 的内存占用。
PS:合并贴图优化在实际项目测试下来,每个场景烘焙贴图数据大概能省
20%左右,包体大小能降一些,内存方面考虑到贴图内存占用基本上是 double(加上显存),这个优化方案还是值得做的,当然前提是烘焙效果上能够接受ShadowMapTexture从 G8 无压缩到 ASTC6x6 的精度损失,Enjoy~~~
引擎修改库(基于 4.6.2 版本,已经失效),作者其他仓库地址
参考文献
[2] ASTC 纹理压缩格式详解