UE | 关于StaticMesh RenderData的DDCKey计算造成的一个难查的bug | Blurred code

UE | 关于StaticMesh RenderData的DDCKey计算造成的一个难查的bug

2026/09/13

LastMod:2026/09/13

Categories: UE

Engine Version: 5.5.4

最近碰到个bug给我查背过气了..

先上一个极端简化后的代码,当StaticMesh在Cook或者在Editor里进行物理检查时候,可以根据做碰撞的LOD(虚幻可以指定某一级的LOD模型作为碰撞)上的每个Section的材质来判断物理材质。

这里Bug的表现是: Section.MaterialIndex 指向一个不存在的Index。出现问题的Mesh是曾经添加一个材质,后面又删除了,但是RenderData.LODREsources[LODIndex].Section.MaterialIndex里记录的仍然是已经被删除的材质,导致了一系列物理判断的问题。

** 另外吐槽一下虚幻的又一个垃圾设计,为什么物理的表现会依赖Section上记录的材质呢,强行把渲染和物理两个不相干的模块耦合在一起了。**

bool UStaticMesh::GetPhysicsTriMeshDataCheckComplex(struct FTriMeshCollisionData* CollisionData, bool bInUseAllTriData, bool bInCheckComplexCollisionMesh)
{

    ....
	// Get the LOD level to use for collision
	// Always use 0 if asking for 'all tri data'
	const int32 UseLODIndex = bInUseAllTriData ? 0 : FMath::Clamp(LODForCollision, 0, GetRenderData()->LODResources.Num()-1);
	FStaticMeshLODResources& LOD = GetRenderData()->LODResources[UseLODIndex];

	for(int32 SectionIndex = 0; SectionIndex < LOD.Sections.Num(); ++SectionIndex)
	{
		const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
        for (uint32 TriIdx = Section.FirstIndex; TriIdx < OnePastLastIndex; TriIdx += 3)
        {
            ...
            CollisionData->MaterialIndices.Add(Section.MaterialIndex);  // BUG: Section.MaterialIndex里出现了超过StaticMesh里的Materials.Num()的索引
        }
	}

难查和好查的点

首先我锁定了一个必现此bug的Mesh,无论是Editor还是Cooked的客户端,物理材质始终获取不正确,出来是引擎默认的物理材质。能必现的Bug都是能解决的。

这里有个越界Index,那么我首先让AI搓了一个小工具,遍历目标Mesh上的RenderData下的所有LOD的所有Section,挨个检查MaterialIndex是否是stale的值并自动修复。 然而,事情并不是那么简单,自己搓的工具根本扫描不出来目标Mesh上的异常数据,显示一切正常。

RenderData会自修复

void UStaticMesh::FinishPostLoadInternal(FStaticMeshPostLoadContext& Context)
{
	{
#if WITH_EDITOR
		if (GetRenderData())
		{
			GetRenderData()->ResolveSectionInfo(this);
		}
#endif
	}
}
void FStaticMeshRenderData::ResolveSectionInfo(UStaticMesh* Owner)
{
	int32 LODIndex = 0;
	int32 MaxLODs = LODResources.Num();
	for (; LODIndex < MaxLODs; ++LODIndex)
	{
		FStaticMeshLODResources& LOD = LODResources[LODIndex];
		for (int32 SectionIndex = 0; SectionIndex < LOD.Sections.Num(); ++SectionIndex)
		{
			FMeshSectionInfo Info = Owner->GetSectionInfoMap().Get(LODIndex,SectionIndex);
			FStaticMeshSection& Section = LOD.Sections[SectionIndex];
			Section.MaterialIndex = Info.MaterialIndex;
			Section.bEnableCollision = Info.bEnableCollision;
			Section.bCastShadow = Info.bCastShadow;
			Section.bVisibleInRayTracing = Info.bVisibleInRayTracing;
			Section.bAffectDistanceFieldLighting = Info.bAffectDistanceFieldLighting;
			Section.bForceOpaque = Info.bForceOpaque;
		}


原来在每个StaticMesh执行完PostLoad后,会在PostLoad阶段重新计算RenderData里的数据,如果发现和StaticMesh上U类记录的数据不一样,这里会重新修复RenderData内部的数据。

UStaticMesh::GetPhysicsTriMeshDataCheckComplex的执行时机

UE_关于StaticMesh_RenderData的DDCKey计算造成的一个难查的bug-2026-09-13-19-35-24 UE_关于StaticMesh_RenderData的DDCKey计算造成的一个难查的bug-2026-09-13-19-36-34

下了个断点在UStaticMesh::GetPhysicsTriMeshDataCheckComplex,发现执行时机在GetRenderData的时候发现RenderData尚未完全准备好,主线程此时正在同步等待,而GetPhysicsTriMeshDataCheckComplex的调用点来自异步线程,看起来这个时候似乎Mesh在Load阶段读取到的RenderData是个错误的值,然后随即在StaticMesh::PostLoad阶段被修复会正确的值。

DDC Key的错误计算

StaticMesh的RenderData是一个带缓存的值,它平时是存放在DDC里的。他的调用点在

const FString KeySuffix = BuildStaticMeshDerivedDataKeySuffix(TargetPlatform, Owner, LODGroup);
DerivedDataKey = BuildStaticMeshDerivedDataKey(KeySuffix);
static FString BuildStaticMeshDerivedDataKeySuffix(const ITargetPlatform* TargetPlatform, UStaticMesh* Mesh, const FStaticMeshLODGroup& LODGroup)
{
    // 有问题的代码
	// Append the section material slot mappings for LOD0, as they are baked into the Nanite build.	
	const FMeshSectionInfoMap& SectionInfoMap = Mesh->GetSectionInfoMap();
	int32 NumLOD0Sections = SectionInfoMap.GetSectionNumber(0);
	KeySuffix += TEXT("_");
	for (int32 SectionIndex = 0; SectionIndex < NumLOD0Sections; SectionIndex++)
	{
		KeySuffix += LexToString(SectionInfoMap.Get(0, SectionIndex).MaterialIndex);
	}
	return KeySuffix;
}

跟到下面发现,RenderData的记录的MaterialIndex只包含了LOD0的所有Section0的MaterialIndex

这显然是一个错误的假设。假如我有一个StaticMesh想要LOD0和LOD1使用不同的材质,LOD1的Section完全可以修改成LOD0完全不同的MaterialIndex,然而这样的变化却不会触发RenderData的cache key发生变化,导致DDC仍然缓存的是旧的RenderData。

这里应该包含所有LOD的Index,修改后问题消失。