【转载】在 UE4 中实现 LightMap 和 ShadowMap 贴图合并

1,503 阅读6分钟

原文链接:在 UE4 中实现 LightMap 和 ShadowMap 贴图合并 | YOung

前言

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,结合坐标 CoordinateScaleCoordinateBias,“标识” Mesh 在贴图中的采样区域。静态烘焙 Mesh 保存两套坐标 CoordinateScaleCoordinateBias,分别用于 LightMap 和 ShadowMap 贴图的采样。

  • 静态烘焙 Mesh 的 ShadowMap 贴图采样区域示意图

贴图合并

场景 umap 文件同级目录的 BuiltData.uasset 包含场景烘焙生成的数据。通过 GWorld 可以拿到运行时的烘焙数据,遍历所有静态烘焙 Mesh 的 FMeshMapBuildData 可以收集到所有关联的 LightMapTextureShadowMapTexture

贴图合并的基本操作是 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);
	}
}

需要 注意 的是:

  1. 静态烘焙 Mesh 关联的 LightMapTextureShadowMapTexture 的尺寸不一定相同,合并的时候以 LightMapTexture 的尺寸为上限。如果 ShadowMapTexture 尺寸小,那么是对 LightMapTextureAlpha 通道的部分区域赋值;如果 ShadowMapTexture 尺寸更大,则偏移其 Mip 直到 LightMapTexture 能够放下。

缩小 ShadowMapTexture 的尺寸会有阴影精度损失,但实际碰到 ShadowMapTexture 尺寸比 LightMapTexture 大的情况并不多,比如模型接受了一个大阴影,但 LightMap 分辨率又给的很低。

  1. 分布在同一张 LightMapTexture 的所有静态烘焙 Mesh,并不一定分布在同一张 ShadowMapTexture 上,所以 LightMapTextureShadowMapTexture 并不是一一对应的关系,合并的时候把关联了同一张 LightMapTexture 的静态烘焙 Mesh 的所有 ShadowMapTexture 依次合进 LightMapTextureAlpha 通道,直到 LightMapTexture 占满。

这种合并策略充分利用了实际大部分情况下,ShadowMapTexture 尺寸比 LightMapTexture 小,且 LightMapTextureShadowMapTexture 是一对多的特点。

  1. 贴图合并后需要重新计算静态烘焙 Mesh 用于 ShadowMapTexture 采样的坐标缩放和偏移。因为原坐标缩放偏移是对单张的 ShadowMapTexture 采样用的,合并后 ShadowMapTexture 变成了 LightMapTexture 的一部分,缩放偏移发生了改变(同尺寸合并,缩放偏移不变)。

原 ShadowMap 采样坐标公式为:

[公式]

贴图合并后的 ShadowMap 采样坐标公式为:

[公式]

  • LightMapTexture 对应多个 ShadowMapTexture 合并示意图

FShadowMap2D 修改

  1. 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;
  1. FShadowMap2DGetInteraction 接口修改为平台相关:
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 接口运行时判断在移动端低质量贴图模式且 bUseLQLightMapAlphaChanneltrue 的情况下,ShadowMapTexture 的采样坐标需要套用贴图合并的坐标 Scale 和 Bias,CoordinateScaleCoordinateBias 传递的值见前面的 ShadowMap 采样坐标公式推导。

新增加一套贴图合并相关的坐标 Scale 和 Bias,在运行时重新计算采样坐标,而不是直接在贴图合并的时候改掉原 CoordinateScaleCoordinateBias,是因为原坐标偏移缩放是 PC 和移动端通用的,直接改掉会使得非 ES3.1 模式下 ShadowMap 显示错误。

  1. 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 不再序列化到烘焙数据。其中,代码用 GEngineDefaultTexture 做占位用,防止其他代码的空指针异常。

引擎自带的 DefaultTexture 尺寸很小,且 UE4 相同贴图只会序列化一份,所以不会有额外内存开销。

管线修改

  1. 增加新的烘焙光照渲染策略类型以支持贴图合并模式:
LMP_MOBILE_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP,
LMP_MOBILE_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP_ALPHACHANNEL,
  1. 烘焙光照渲染策略修改:
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(加上显存),这个优化方案还是值得做的,当然前提是烘焙效果上能够接受ShadowMapTextureG8 无压缩到 ASTC6x6 的精度损失,Enjoy~~~

引擎修改库(基于 4.6.2 版本,已经失效),作者其他仓库地址

参考文献

[1] UE4 Lightmap 格式解析

[2] ASTC 纹理压缩格式详解