跳转至

LOD不完全指南

电子计算机的发明起源于第二次世界大战期间,当时火炮和弹道计算变得日益复杂,原有的计算工具已经无法满足需求,为了解决这些问题,科学家和工程师们开始研制电子计算机。

伴随着电子计算机的出现,电子游戏也应运而生。

在电子游戏的发展进程中,总有那么一些脑洞大开的创意和技术,它不断地突破硬件开发者思维的设限,极致地压榨硬件的性能,驱动着计算机向着一个惊人的方向发展:

LOD(Level of Detail ) ,正是在这个发展过程中出现并一直延用至今的一项技术,它旨在:

  • 在不同 重要性(Significance) 的场合,采用不同精度的策略,从而尽可能的减少资源的浪费,提升整个系统的上限。

这样的思想策略体现在游戏制作的方方面面。

模型

在模型上, LOD 的应用主要是为模型创建多个精度级别的低模,在近距离时,使用高精度模型,远距离时,使用低精度模型。

在用于实时渲染领域且具有一定规模的三维场景中,为了保证程序的帧率,模型LOD的制作通常是开发团队无法绕开的难题。

高额的时间和人力成本让很多独立开发者和小型团队搭建复杂场景时需要面临非常多非常多的困难。

直到UE的虚拟几何体技术—— Nanite ,横空出世,从而大幅度降低了场景制作的门槛。

Nanite 会分析模型几何的重要性信息,自动生成高质量的LOD,不仅占用了更少的存储空间,还可以以非常高的性能绘制,它无疑是几何渲染的未来。

但目前而言,在一些特定场景下,我们还不能完全放弃LOD:

  • 现代PC的GPU算力通常是冗余的,大部分的中端GPU足以应付一些常见的复杂场景渲染,但其他平台,如移动设备,X Box,Switch...,对于有较高画面要求的项目,它们的算力目前而言更多只是堪堪够用,为了追求更好的视觉效果,甚至往往需要定制一些比较Hack的渲染机制来进行极致的优化,而所分配剩余的算力很难支撑起Nanite的运作。
  • Nanite的模型简化算法在一些特殊的模型类型上还有很大的优化空间,比如植被,如果是植被密度较大范围较广的复杂场景,使用LOD能更大程度地压缩性能,这在之前的文章中有提到。

之期大部分的游戏项目,通常是由美术人员在DDC软件中来制作LOD,美术人员在提供模型源文件的同时,按照一定规范来生产模型对应的LOD。

这种做法,目前来看是存在一定局限性的:

  • 早期的模型制作流程比较“务实”,大部分时候还是只能通过人工建模的方式,而现在涌现了许多网格处理的SDK,它们往往会提供一些有效的模型简化算法,无需重新建模,就能生成高品质的LOD。
  • 人工制作LOD往往需要高额的成本,团队自身有足够的人力物力还好,如果是交给外包团队去做,这无疑会是一笔巨大的开销。
  • 任何方法论都有一定适用条件,模型制作也一样,除了一些老牌制作厂商之外,大部分团队应该都很难在游戏制作之初,就能制定出一套尽善尽美的制作规范,往往很多隐秘的问题随着开发进度的推进逐渐浮现,而美术制作又是整个项目中费用消耗的大头,对于LOD制作来说,如果源模型或者规范变了,那么之前制作的“产品”就白费了,所以我们需要将关注点从制作具体的 LOD ,转化为去搭建一条可迭代的 LOD 生产管线。
  • 源模型虽然是由美术人员制作的,但他们往往对性能并不敏感,对LOD的认识也不是很深,在面对那些无论怎么制作都很难满足规范的模型时,他们所表现出来的,只会是无措。

此时如果有人对几何处理,图形渲染,编辑器方向有深入的了解,以程序化的方式包揽这些事务,就可以为团队节省出不少制作成本,这类专业人员一般被定义为 管线TA / 引擎管线工程师

参考

在阐述LOD制作之前,我们先了解一下一些现代游戏中的模型制作规格:

这些资料可以作为有效的参考,避免源模型就出现那种超出实时渲染规格的情况,这是 3D - ACE 给出的一个标准:

  • PC 和游戏机
游戏资产 多边形计数 (UE) 三角形数 (UE) 多边形计数 (Unity) 三角形计数 (Unity)
低细节角色 10,000-20,000 20,000-40,000 5,000-10,000 10,000-20,000
高细节角色 20,000-60,000 40,000-120,000 10,000-30,000 20,000-60,000
简单道具 500-2,000 1,000-4,000 250-1,000 500-2,000
复杂道具 2,000-10,000 4,000-20,000 1,000-5,000 2,000-10,000
基本环境 10,000-50,000 20,000-100,000 5,000-25,000 10,000-50,000
详细环境 50,000-200,000 100,000-400,000 25,000-100,000 50,000-200,000
  • 移动和低端设备
游戏资产 多边形计数 三角形计数
低细节角色 1,000-5,000 2,000-10,000
高细节角色 5,000-10,000 10,000-20,000
简单道具 100-500 200-1,000
复杂道具 500-2,000 1,000-4,000
基本环境 2,000-10,000 4,000-20,000
详细环境 10,000-20,000 20,000-40,000
  • VR/AR 设备
游戏资产 多边形计数 三角形计数
低细节角色 2,000-10,000 4,000-20,000
高细节角色 10,000-20,000 20,000-40,000
简单道具 500-1,500 1,000-3,000
复杂道具 1,500-5,000 3,000-10,000
基本环境 5,000-15,000 10,000-30,000
详细环境 15,000-30,000 30,000-60,000

原博客位于:

3D-ACE中有很多有价值的 博客 参考,感兴趣的小伙伴可以看下:

image-20241023201143597

制作流程

在制作LOD之前,我们最好搭建一个简单的预览关卡,通过一个简单的编辑器工具来自动排列,去完成这样的目的:

  • 根据资产列表或者资产路径确定要预览的所有资产。
  • 根据模型的包围球半径作为间隔进行横向排布。
  • 将LOD的屏幕尺寸转化为距离,纵向排布所有LOD。
  • 在生成的模型上扩展显示一些关键信息,比如屏幕尺寸,距离,顶点数,面数...

在这样的视图下我们能更加直观地查看和管理模型的规格:

image-20241109114502541

在早期的LOD制作流程中,我们一般将 模型与相机 之间的 距离 作为LOD切换的判断标准,每隔一段距离,切换一个精度的简模,在这种方式下我们可能会制作出这样的 LOD 链

image-20241109115221157

但很快有人发现,使用 距离 似乎并不能很直观地确立LOD之间的距离间隔,因为有的模型大,有的模型小,为了保证每个模型采用合适的精度分布,就必须为其评估出不同的距离间隔,为了解决这个问题,我们引入了另一个参数 —— 屏幕尺寸(Screen Size) :它会综合评估 相机距离相机的透视矩阵 以及模型的 包围球半径 得到一个数值,它可以粗略当作是模型的占屏比,这是它的计算公式:

float ConvertDistanceToScreenSize(float ObjectSphereRadius, float Distance)
{
    const float FOV = 90.0f;                                // 相机参数可以统一确认
    const float FOVRad = FOV * (float)UE_PI / 360.0f;
    const FMatrix ProjectionMatrix = FPerspectiveMatrix(FOVRad, 1920, 1080, 0.01f);     
    const float ScreenMultiple = FMath::Max(0.5f * ProjectionMatrix.M[0][0], 0.5f * ProjectionMatrix.M[1][1]);
    return 2.0f * ScreenMultiple * ObjectSphereRadius / FMath::Max(1.0f, Distance);
}

我们通过每次缩减一定百分比的屏幕尺寸来确定LOD之间的间隔,相比直接使用距离,会更加容易一些:

image-20241109121834793

可是,虽然使用屏幕尺寸确实能直观地设置LOD之间的间隔,但如果为不同尺寸的模型生成LOD链的距离间隔不统一,将会让视觉上产生的瑕疵更加明显。

假如我们采用屏幕尺寸固定百分比递减的方式生成LOD,可以看到不同模型的LOD链距离是不一致的(这里为了能看出瑕疵,刻意把LOD的简化幅度调得很大):

image-20241109133230072

LODTest0

对比一下使用固定的距离梯度来生成LOD:

image-20241109133923492

LODTest1

可以看到使用固定距离梯度LOD的画面变化会更加平缓一些:

image-20241109135329103

虽然使用屏幕尺寸确实可以让单个模型的LOD链组织得更加合理,但由于引擎中大量使用 距离 来限制某些视觉效果(比如阴影,距离场光照...),如果为了可以让视觉效果更加平滑地过渡,建议使用这样的策略来生成LOD:

  • 使用固定的LOD距离梯度
  • 根据包围球半径确定LOD的数量
  • 根据屏幕尺寸,顶点密度和模型特征来确定简化幅度

上述的一些关键参数会随着项目的迭代,复杂度的提升以及平台的性能要求发生变化,所以我们不应该直接手动去生成LOD,而是搭建一条LOD生成管线,这样可以不断迭代和优化这条管线。

常见的简模生成策略有三类:

  • 减面(Reduce) :根据重要性按照一定 简化幅度 进行删减原有模型的顶点数据上来生成简模。
  • 重构(Remesh) :通过体素化的模型算法,以一定 体素精度 来逼近源模型从而得到简模。
  • 替身(Impostor) :以一种取巧的方式来保证模型的视觉效果,通常它会打破原有模型的几何结构。

image-20240809151946858

v2-ac82f9df3eb6d0ab62edc448957c80e6_r

目前大部分的团队会把这条管线搭建在DDC与引擎之间,但目前来说,虚幻引擎在提供了非常优秀的模型处理接口,如果我们直接把管线搭建在引擎侧,可以更好的结合实际的游戏场景环境来生成更好的效果 。

比如之前植被章节所提到的 八面体替身 (Octahedron Impostor)

UE引擎原生虽然提供了一些模型简化算法,但有一些几何处理SDK,它们可以更大程度地在保证视觉效果的前提下,大幅度地缩减模型渲染的开销:

使用这些SDK提供的接口,可以轻易搭建一条LOD链生成管线。

这是一些制作LOD时可供参考的文档:

底层机制

创建自定义网格

UPackage* NewPackage = CreatePackage(TEXT("/Game/NewStaticMesh"));                                              // 创建新包
UStaticMesh* NewStaticMesh = NewObject<UStaticMesh>(NewPackage, "NewStaticMesh",  RF_Public | RF_Standalone);   // 创建新网格,指定Outer为新包,这样保存新包时,会将该对象序列化存储

{   // LOD0
    FStaticMeshSourceModel& NewSourceModel = NewStaticMesh->AddSourceModel();                                   // 创建新的源模型  
    FMeshDescription& NewMeshDescription = *NewStaticMesh->CreateMeshDescription(0);                            // 为该源模型创建网格描述
    FStaticMeshAttributes AttributeGetter = FStaticMeshAttributes(NewMeshDescription);                          // 创建网格描述的属性获取器
    AttributeGetter.Register();

    TPolygonGroupAttributesRef<FName> PolygonGroupNames = AttributeGetter.GetPolygonGroupMaterialSlotNames();   // 几何组名称列表,对应相应的MeshsSection
    TVertexAttributesRef<FVector3f> VertexPositions = AttributeGetter.GetVertexPositions();                     // 顶点数组
    TVertexInstanceAttributesRef<FVector3f> Normals = AttributeGetter.GetVertexInstanceNormals();               // 法线数组
    TVertexInstanceAttributesRef<FVector3f> Tangents = AttributeGetter.GetVertexInstanceTangents();             // 切线数组
    TVertexInstanceAttributesRef<float> BinormalSigns = AttributeGetter.GetVertexInstanceBinormalSigns();       
    TVertexInstanceAttributesRef<FVector4f> Colors = AttributeGetter.GetVertexInstanceColors();                 // 顶点色数组
    TVertexInstanceAttributesRef<FVector2f> UVs = AttributeGetter.GetVertexInstanceUVs();                       // 纹理坐标数组

    int32 VertexCount = 4;
    int32 VertexInstanceCount = 6;
    int32 PolygonCount = 2;
    NewMeshDescription.ReserveNewVertices(VertexCount);                                                         // 预分配内存
    NewMeshDescription.ReserveNewVertexInstances(VertexInstanceCount);
    NewMeshDescription.ReserveNewPolygons(PolygonCount);
    NewMeshDescription.ReserveNewEdges(PolygonCount * 2);
    UVs.SetNumChannels(PolygonCount);
    {
        const FVertexID VertexID0 = NewMeshDescription.CreateVertex();
        const FVertexID VertexID1 = NewMeshDescription.CreateVertex();
        const FVertexID VertexID2 = NewMeshDescription.CreateVertex();
        const FVertexID VertexID3 = NewMeshDescription.CreateVertex();

        VertexPositions[VertexID0] = FVector3f(0.0f, 0.0f, 0.0f);
        VertexPositions[VertexID1] = FVector3f(100.0f, 0.0f, 0.0f);
        VertexPositions[VertexID2] = FVector3f(100.0f, 0.0f, 100.0f);
        VertexPositions[VertexID3] = FVector3f(0.0f, 0.0f, 100.0f);

        const FVertexInstanceID VertexInstanceID0 = NewMeshDescription.CreateVertexInstance(VertexID0);
        const FVertexInstanceID VertexInstanceID1 = NewMeshDescription.CreateVertexInstance(VertexID1);
        const FVertexInstanceID VertexInstanceID2 = NewMeshDescription.CreateVertexInstance(VertexID2);

        const FVertexInstanceID VertexInstanceID3 = NewMeshDescription.CreateVertexInstance(VertexID0);
        const FVertexInstanceID VertexInstanceID4 = NewMeshDescription.CreateVertexInstance(VertexID2);
        const FVertexInstanceID VertexInstanceID5 = NewMeshDescription.CreateVertexInstance(VertexID3);

        UVs[VertexInstanceID0] = FVector2f(0.0f, 0.0f);
        UVs[VertexInstanceID1] = FVector2f(1.0f, 0.0f);
        UVs[VertexInstanceID2] = FVector2f(1.0f, 1.0f);
        UVs[VertexInstanceID3] = FVector2f(0.0f, 0.0f);
        UVs[VertexInstanceID4] = FVector2f(1.0f, 1.0f);
        UVs[VertexInstanceID5] = FVector2f(0.0f, 1.0f);

        Normals[VertexInstanceID0] 
            = Normals[VertexInstanceID1] 
            = Normals[VertexInstanceID2] 
            = Normals[VertexInstanceID3] 
            = Normals[VertexInstanceID4] 
            = Normals[VertexInstanceID5] 
            = FVector3f(0.0f, 1.0f, 0.0f);

        UMaterialInterface* Material = UMaterial::GetDefaultMaterial(MD_Surface);

        {   // Mesh Section 0
            FPolygonGroupID PolygonGroup0 = NewMeshDescription.CreatePolygonGroup();
            PolygonGroupNames[PolygonGroup0] = "LOD0_Section0";
            NewMeshDescription.CreatePolygon(PolygonGroup0,
                {
                    VertexInstanceID0,
                    VertexInstanceID1,
                    VertexInstanceID2,
                });
            NewStaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material));
        }

        {   // Mesh Section 1
            FPolygonGroupID PolygonGroup1 = NewMeshDescription.CreatePolygonGroup();
            PolygonGroupNames[PolygonGroup1] = "LOD0_Section1";
            NewMeshDescription.CreatePolygon(PolygonGroup1,
                {
                    VertexInstanceID3,
                    VertexInstanceID4,
                    VertexInstanceID5
                });
            NewStaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material));
        }
    }
    NewSourceModel.ScreenSize = 1;                                                      // 设置屏幕尺寸
    NewStaticMesh->CommitMeshDescription(0);                                            // 提交网格描述
}

{
    FStaticMeshSourceModel& NewSourceModel = NewStaticMesh->AddSourceModel();
    FMeshDescription& NewMeshDescription = *NewStaticMesh->CreateMeshDescription(1);
    FStaticMeshAttributes AttributeGetter = FStaticMeshAttributes(NewMeshDescription);
    AttributeGetter.Register();

    TPolygonGroupAttributesRef<FName> PolygonGroupNames = AttributeGetter.GetPolygonGroupMaterialSlotNames();
    TVertexAttributesRef<FVector3f> VertexPositions = AttributeGetter.GetVertexPositions();
    TVertexInstanceAttributesRef<FVector3f> Tangents = AttributeGetter.GetVertexInstanceTangents();
    TVertexInstanceAttributesRef<float> BinormalSigns = AttributeGetter.GetVertexInstanceBinormalSigns();
    TVertexInstanceAttributesRef<FVector3f> Normals = AttributeGetter.GetVertexInstanceNormals();
    TVertexInstanceAttributesRef<FVector4f> Colors = AttributeGetter.GetVertexInstanceColors();
    TVertexInstanceAttributesRef<FVector2f> UVs = AttributeGetter.GetVertexInstanceUVs();

    int32 VertexCount = 3;
    int32 VertexInstanceCount = 3;
    int32 PolygonCount = 1;
    NewMeshDescription.ReserveNewVertices(VertexCount);
    NewMeshDescription.ReserveNewVertexInstances(VertexInstanceCount);
    NewMeshDescription.ReserveNewPolygons(PolygonCount);
    NewMeshDescription.ReserveNewEdges(PolygonCount * 2);
    UVs.SetNumChannels(1);
    {
        const FVertexID VertexID0 = NewMeshDescription.CreateVertex();
        const FVertexID VertexID1 = NewMeshDescription.CreateVertex();
        const FVertexID VertexID2 = NewMeshDescription.CreateVertex();

        VertexPositions[VertexID0] = FVector3f(0.0f, 0.0f, 0.0f);
        VertexPositions[VertexID1] = FVector3f(100.0f, 0.0f, 0.0f);
        VertexPositions[VertexID2] = FVector3f(50.0f, 0.0f, 100.0f);

        const FVertexInstanceID VertexInstanceID0 = NewMeshDescription.CreateVertexInstance(VertexID0);
        const FVertexInstanceID VertexInstanceID1 = NewMeshDescription.CreateVertexInstance(VertexID1);
        const FVertexInstanceID VertexInstanceID2 = NewMeshDescription.CreateVertexInstance(VertexID2);

        UVs[VertexInstanceID0] = FVector2f(0.0f, 0.0f);
        UVs[VertexInstanceID1] = FVector2f(1.0f, 0.0f);
        UVs[VertexInstanceID2] = FVector2f(0.5f, 1.0f);

        Normals[VertexInstanceID0]
            = Normals[VertexInstanceID1]
            = Normals[VertexInstanceID2]
            = FVector3f(0.0f, 1.0f, 0.0f);

        UMaterialInterface* Material = UMaterial::GetDefaultMaterial(MD_Surface);

        {
            FPolygonGroupID PolygonGroup0 = NewMeshDescription.CreatePolygonGroup();
            PolygonGroupNames[PolygonGroup0] = "LOD1_Section0";
            NewMeshDescription.CreatePolygon(PolygonGroup0,
                {
                    VertexInstanceID0,
                    VertexInstanceID1,
                    VertexInstanceID2,
                });
            NewStaticMesh->GetStaticMaterials().Add(FStaticMaterial(Material));
        }
    }
    NewSourceModel.ScreenSize = 0.5f;
    NewStaticMesh->CommitMeshDescription(1);
}

NewStaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;           // 声明此次构建已是最新版本的数据,避免自动生成LOD顶替我们填充的模型数据
NewStaticMesh->Build();                                                         // 构建网格
NewStaticMesh->PostEditChange();
NewStaticMesh->MarkPackageDirty();
NewStaticMesh->WaitForPendingInitOrStreaming(true, true);

FStaticMeshCompilingManager::Get().FinishAllCompilation();
FAssetRegistryModule::AssetCreated(NewStaticMesh);                              

FPackagePath PackagePath = FPackagePath::FromPackageNameChecked(NewPackage->GetName());
FString PackageLocalPath = PackagePath.GetLocalFullPath();
UPackage::SavePackage(NewPackage, NewStaticMesh, RF_Public | RF_Standalone, *PackageLocalPath, GError, nullptr, false, true, SAVE_NoError);         // 保存新包

渲染资源提交时机

image-20241115222000975

渲染执行策略

image-20241115223942867

常用控制台变量

  • r.ForceLOD:强制锁定所有模型的LOD级别
  • r.ForceLODShadow:强制锁定用于提交给阴影Pass的LOD级别
  • r.StaticMesh.MinLodQualityLevel:设置模型的最小LOD级别
  • r.StaticMeshLODDistanceScale:LOD距离的缩放因子

材质贴图

对于材质而言,早期有一些项目,在生成模型LOD的同时,也会为对应的材质生成LOD,虽然理论意义上,简化材质的逻辑确实可以减少性能开销,但材质LOD无疑会增加材质集的大小,且带来的性能提升并不是特别明显,较复杂的管理方式也让这种做法在实际项目中很难推广。

目前更多的关注点是在贴图上面,而LOD的应用主要是为贴图生成 Mipmap

大家对 Mipmap(多级渐远纹理) 更多的认识应该是解决摩尔纹现象:

14568

除了解决摩尔纹, Mipmap目前在UE中还有其他用处:

  • 纹理流送(Texture Streaming) :为了缓解显存占用的压力,UE默认会分配一定大小的纹理流送池(使用r.Streaming.PoolSize配置),根据纹理的重要性(相机距离和纹理组优先级)来流入纹理的某个 MipLevel ,从而尽可能降低降低整体的显存占用,这里有一些关于纹理流送非常有价值的文章:
  • UI适配(UI Adaptation) :由于游戏可能要面临不同分辨率的运行环境,当缩小UI贴图的时候,由于 下采样 (Downsampling) 的纹素跨度过大,将会导致贴图出现锯齿边缘,对于动态调整位置的UI,还会出现明显的闪烁,此时就可以给UI贴图生成Mipmap,因为引擎会根据 DDX(UV)DDY(UV)来确定使用UI贴图的哪一个 MipLevel

image-20241115230952306

我们在这里可以单独特化某个贴图的Mipmap:

image-20241109194151209

但最好的方式还是将贴图进行分类到不同的 纹理组(TextureLODGroup) ,对每个纹理组采用统一的配置,这样在做多平台的性能调优时,会方便很多。

UE 5 中可以在此处统一设置不同平台下纹理组的配置:

image-20241109201815611

地形

UE5的地形运行机制非常完善,地形的LOD是通过调整曲面细分的精度,目前默认使用的是8级LOD,想要修改LOD,只需要调整这些参数:

image-20241109210110173

UE5.1之后支持了Nanite地形,由于地形拿到的是一种非常标准化的模型数据,开启Nanite之后无疑会拥有更合理的几何组织,相比LOD,它通常具有更少的三角形和更保真的视觉效果:

image-20241109211741424

虽然Nanite的调度算法相比曲面细分复杂一些,并且多了一份网格数据,但在镜头静止时,Nanite的渲染消耗明显低于LOD,并且Nanite对虚拟阴影是友好的,如果没有平台限制,Nanite地形会是更好的选择。

目前一些项目中会使用静态网格作为地形,借助世界分区的流送和HLOD可以得到更好地平衡性能且没有地形编辑的限制,但地形主要的优势在于使用几张高分辨率(贴图的分辨率远远小于地形分辨率)的Tile贴图就可以平铺出一个很大的区域,它所占用的磁盘大小要远远小于使用模型平铺出相同视觉精度的地形。

HLOD

上面提到的几种LOD类型主要的针对单个资产,而HLOD则是对资产合批之后再制作LOD。

目前HLOD在UE中存在两种使用方式:

  • 针对普通地图,可以根据该文档来利用工具对网格合并生成HLOD:
  • 针对开放世界地图,则是需要对场景物体进行分类,设置 HLODLayer ,来自动为世界分区的 网格单元(Grid Cell) 生成HLOD:

这里针对后面一种方式展开说下

因为 世界分区(World Partition) 会将场景网格划分成很多个单元,通过 流送(Streaming) 只加载关键的一部分区域(比如玩家周边),这样可以有效缓解程序执行的压力:

image-20231128183116324

但对于已经被卸载掉的单元,我们仍然希望它能够显示出来,但不想增加太大的性能开销,所以我们会对单元内的物体生成HLOD,这样可以进一步缩减流送的加载范围,因为HLOD能:

  • 减少Draw Call
  • 减少图元组件数量,提升剔除效率
  • 减少远景的几何和贴图的精度,提升渲染效率和降低内存压力

但它同样也存在问题:

  • 只能扩大一定的显示范围
  • 会增加磁盘空间大小

目前UE提供了四种基础的HLOD生成策略,它们 只针对静态网格体资产 ,分布是:

  • 实例化:将单元格内的物体合并为ISM
  • 合并:将单元格内的物体合并为一个网格
  • 简化:将单元格内的物体合并之后进行网格简化
  • 近似:将单元格内的物体合并之后进行网格近似

它们分布适用于不同的网格类别,假如使用了这样的分区网格:

GirdName Cell Size Loading Range Priority BlockOnSlowStreaming
MainGrid(Default) 12800 25600 0 false
DeferGrid 25600 25600 -9 true
AdvanceGrid 51200 51200 9 true
SmallGrid 6400 19200 -9 false

依此可以分以下几个大类:

  • 地形面片:主要是一些用来补充地表的静态网格面片(非地形组件),分布在AdvanceGrid,地形块的大小一般可以是25600,对于地形面片,它们会放在51200的Cell中,合并几何来生成HLOD,减少 DrawCall 和 图元组件数量。
  • 普通模型:分布在 MainGrid 中,放置在12800的Cell中,HLOD的生成方式是剔除掉小物件,合并后简化或近似生成HLOD简模,可以具有多个HLOD级别。
  • 植被:分布在AdvanceGrid中,使用 25600 的 HISM 组织 Partition,放置在51200的Cell中,HLOD策略是将 HISM 合并并退化为大小为51200 的ISM。
  • 其他:一些自定义或是动态的渲染组件(如粒子,水体...),尽可能避免在远处显示,通常这类物品在远处显示需要扩大加载距离,并做好LOD,或者定制专属的HLOD生成策略。

image-20241116120055164

特效

特效(Niagara) 是一种非常特殊的渲染组件,GPU粒子使用 Computer Shader 模拟粒子运动,模拟的数据将交由粒子渲染器执行 间接渲染 (Indirect Rendering)

粒子的优化主要在于减少模拟逻辑的复杂度,减少发射器,裁剪,优化材质和OverDraw。

LOD的主要应用在于Niagara提供了一些引擎内置变量:

image-20241116115756142

通常我们会根据LOD的距离去动态调整粒子的数量以及生命周期,来优化粒子的性能消耗。