Unreal Engine Mutable 笔记

UE Mutable 详细用法剖析

基于 MutableSample 拆解 Mutable 的完整工作方式:从 CustomizableObject 资产图、参数和子对象,到 C++ 运行时实例生成、多组件绑定、异步更新、材质恢复、LOD 管理和生产踩坑。

快速结论

Mutable 解决的不是“把材质参数调成红色”这种浅层需求,而是“把网格、材质、贴图、morph、裁剪、贴花、子对象、表格数据编译成一套可运行时生成的组合规则”。运行时实例只保存参数;生成完成后,游戏拿到的是标准 UE 资源。

最小心智模型

UCustomizableObject 是图和编译数据,UCustomizableObjectInstance 是参数快照,UCustomizableObjectInstanceUsage 把实例接到 USkeletalMeshComponent

最容易踩的点

改参数不等于网格立刻变。Mutable 更新是异步队列,要等回调、UpdatedDelegate 或组件更新完成后再读取生成结果。

1

编辑器里制作并编译 CustomizableObject

2

运行时 CreateInstance(),得到默认参数实例。

3

设置 enum、float、color、projector 等参数。

4

异步更新后写入 Skeletal Mesh Component。

样例覆盖范围

这篇文章基于本机的 /Users/mchen/SyncShare/MutableSample。工程使用 Unreal Engine 5.7,开启了 MutableHairStrandsMutableMutableClothingRigLogic 等插件。C++ 模块里显式依赖 CustomizableObjectAnimationSharing

PrivateDependencyModuleNames.AddRange(new string[] {
    "CustomizableObject",
    "AnimationSharing"
});
目录 作用
Content/Tutorials 最小教程,覆盖基础对象、参数、组件、表格节点、颜色、patch、remove unseen、shape variations。
Content/Character 角色主对象 CO_Character、实例 COI_Character,以及衣服、鞋、头盔、皮肤贴花、机械臂等子对象。
Content/Weapon 武器主对象 CO_Weapon,以及枪管、弹匣、握把、枪托、瞄具等 child objects 和 DataTable。
Content/UI/DynamicallyGeneratedGUI_DGGUI 根据 Mutable 参数元数据动态生成 UI,覆盖 bool、list、float、int、color、projector、layers、state。
Content/Infinite 大量随机 Mutable 实例的性能/LOD 场景,对应 C++ AInfinite

核心概念

Customizable Object

UCustomizableObject 是 Mutable 的根资产。它保存图编辑器里的节点和编译后的运行时数据。它可以查询组件、参数、默认值、枚举选项、UI metadata,也可以通过 CreateInstance() 创建运行时实例。

int32 ComponentCount = Object->GetComponentCount();
FName ComponentName = Object->GetComponentName(0);

int32 ParameterCount = Object->GetParameterCount();
const FString& ParameterName = Object->GetParameterName(0);

UCustomizableObjectInstance* Instance = Object->CreateInstance();

Customizable Object Instance

UCustomizableObjectInstance 是一组参数值加生成状态。它不等于最终网格。最终网格是在更新完成后写到 Skeletal Mesh Component 上的标准 UE 资源。

Instance->SetEnumParameterSelectedOption(TEXT("Body"), TEXT("Body_02"));
Instance->SetBoolParameterSelectedOption(TEXT("HasSticker"), true);
Instance->SetFloatParameterSelectedOption(TEXT("Roughness"), 0.35f);
Instance->SetColorParameterSelectedOption(TEXT("MainColor"), FLinearColor::Red);
Instance->SetTextureParameterSelectedOption(TEXT("Decal Image"), DecalTexture);
Instance->SetProjectorPosition(TEXT("DecalProjector"), FVector(0, 12, 80));

Instance->UpdateSkeletalMeshAsync();

UE 5.7 里枚举参数推荐使用 SetEnumParameterSelectedOption()。旧资料常见的 SetIntParameterSelectedOption() 仍能看到,但在 5.7 头文件里已经指向 enum 命名。

Instance Usage

UCustomizableObjectInstanceUsage 是样例里最值得注意的运行时类。它是一个轻量 UObject,用来把某个实例绑定到一个 USkeletalMeshComponent。官方头文件说明它可以替代组件路径,尤其适合非蓝图项目。

UCustomizableObjectInstanceUsage* Usage =
    NewObject<UCustomizableObjectInstanceUsage>(Actor);

Usage->AttachTo(SkeletalMeshComponent);
Usage->SetCustomizableObjectInstance(Instance);
Usage->SetComponentName(ComponentName);

编辑器资产流程

  1. 准备输入资源。 准备参考骨架、基础 Skeletal Mesh、可替换部件、材质、贴图、DataTable。角色类项目要特别注意所有可组合部件的骨架、LOD、材质槽和贴图通道。
  2. 创建根 Customizable Object。 根对象负责定义最终对象的整体结构。样例里的 CO_Character 是角色根对象,CO_Weapon 是武器根对象。
  3. 设置 Mesh Component。 一个对象可以输出多个 mesh component。新代码尽量用组件名,而不是组件 index。
  4. 连接 Mesh / Material / Texture 节点。 基础图通常把 Skeletal Mesh 接到 Mesh Component,把材质和各纹理通道接到 Material 节点。
  5. 抽出参数。 把用户需要改的分支变成参数节点,例如 body、barrel、color、texture、projector。
  6. 用 Child Object 和 Group 管组合爆炸。 样例的武器拆成枪管、枪身、握把、弹匣、瞄具、枪托;角色也把外套、裤子、鞋、皮肤变化拆成子对象。
  7. 用 DataTable 管内容矩阵。 Content/Tutorials/TableNode 展示了 Table Node。DataTable 适合维护“参数选项到资源集合”的关系。
  8. 设置 State 和 UI Metadata。 State 限制某个场景下哪些参数可改,UI metadata 给动态 UI 提供名称、分组、缩略图和显示策略。
  9. 编译并创建 COI。 编辑器里的 CustomizableObjectInstance 资产可以保存一组参数预设。

武器样例里的参数很有代表性:BodyBarrels TypesMagazines TypesGrips TypesStocks TypesScopes TypesMainColorSecondaryColorTriggerColorEmissiveColorDecal ImageProjectors。这说明 Mutable 的参数不只是材质颜色,也可以决定网格、贴花、表格行和投射区域。

C++ 运行时

最小路径

#include "MuCO/CustomizableObject.h"
#include "MuCO/CustomizableObjectInstance.h"
#include "MuCO/CustomizableObjectInstanceUsage.h"

void SpawnMutable(
    AActor* Owner,
    UCustomizableObject* Object,
    USkeletalMeshComponent* MeshComponent)
{
    if (!Owner || !Object || !MeshComponent)
    {
        return;
    }

    UCustomizableObjectInstance* Instance = Object->CreateInstance();
    if (!Instance)
    {
        return;
    }

    Instance->SetEnumParameterSelectedOption(TEXT("Body"), TEXT("Body_02"));
    Instance->SetColorParameterSelectedOption(TEXT("MainColor"), FLinearColor(0.9f, 0.1f, 0.05f));

    const FName ComponentName = Object->GetComponentName(0);

    UCustomizableObjectInstanceUsage* Usage =
        NewObject<UCustomizableObjectInstanceUsage>(Owner);

    Usage->AttachTo(MeshComponent);
    Usage->SetCustomizableObjectInstance(Instance);
    Usage->SetComponentName(ComponentName);

    Instance->UpdateSkeletalMeshAsync();
}

真实项目里还要保存 InstanceUsage 的引用,避免被 GC 回收。样例的 AInstanceActor 把它们都做成 UPROPERTY(),这是正确习惯。

UPROPERTY()
TArray<TObjectPtr<UCustomizableObjectInstanceUsage>> Usages;

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Default)
TObjectPtr<UCustomizableObjectInstance> CustomizableObjectInstance = nullptr;

多组件绑定

AInfinite::SpawnInstances 的运行时流程是:随机选 Actor 蓝图类,从类默认对象读取 CustomizableObject,创建实例,随机化参数,按组件数量创建多个 USkeletalMeshComponent,再给每个组件创建一个 UCustomizableObjectInstanceUsage

UCustomizableObjectInstance* Instance =
    InstanceActor->CustomizableObject->CreateInstance();

Instance->SetRandomValuesFromStream(RandomStream);
Actor->CustomizableObjectInstance = Instance;

for (int32 ComponentIndex = 0;
     ComponentIndex < Instance->GetNumComponents();
     ++ComponentIndex)
{
    USkeletalMeshComponent* Component = /* root or NewObject */;
    const FName& ComponentName = Object->GetComponentName(ComponentIndex);

    UCustomizableObjectInstanceUsage* Usage =
        NewObject<UCustomizableObjectInstanceUsage>(Actor);

    Usage->AttachTo(Component);
    Usage->SetCustomizableObjectInstance(Instance);
    Usage->SetComponentName(ComponentName);

    Actor->Usages.Add(Usage);
}

GetNumComponents() 在 5.7 头文件里已经提示:如果要的是对象能生成的总组件数,优先从 UCustomizableObjectGetComponentCount();如果要的是实例更新完成后的实际组件名,用 GetComponentNames()。新代码尽量走 name-based API。

异步更新

Mutable 的生成和更新会排队异步执行。参数设置只是改了实例描述,最终 mesh 何时可用要等更新完成。

FInstanceUpdateDelegate Callback;
Callback.BindDynamic(this, &UMyCustomizer::OnMutableUpdated);

Instance->SetEnumParameterSelectedOption(TEXT("Jacket"), TEXT("Jacket_B"));
Instance->UpdateSkeletalMeshAsyncResult(Callback);
UFUNCTION()
void OnMutableUpdated(const FUpdateContext& Context)
{
    if (!UCustomizableObjectSystem::IsUpdateResultValid(Context.UpdateResult))
    {
        return;
    }

    UCustomizableObjectInstance* UpdatedInstance = Context.Instance;
}

材质覆盖和恢复

AMutableSampleCustomizableActor 继承自 ACustomizableSkeletalMeshActor,提供“替换成展示/调试材质,再恢复原材质”的能力。它在启用时绑定实例更新委托,确保每次 Mutable 重新生成后仍能重新应用展示材质。

COInstance->UpdatedDelegate.AddUniqueDynamic(
    this,
    &AMutableSampleCustomizableActor::SwitchComponentMaterials);

替换时,它先从实例拿组件名,再通过 GetSkeletalMeshComponent(ComponentName) 找到 Actor 上对应的组件。恢复时还有一个细节:如果启用了 mesh cache,原始材质可能来自实例保存的 override materials,而不是组件默认材质。

const TArray<UMaterialInterface*> OverrideMaterials =
    COInstance->GetSkeletalMeshComponentOverrideMaterials(
        ActorSkeletalMeshComponents[Index]->GetFName());

const bool bUseOverrideMaterials =
    COInstance->GetCustomizableObject()->bEnableMeshCache &&
    CustomizableObjectSystem->IsMeshCacheEnabled();

这个细节很实用:Mutable 生成的 Skeletal Mesh、材质、贴图可能由系统缓存和复用,不要简单假设 EmptyOverrideMaterials() 永远能还原正确结果。

海量实例与 LOD

AInfinite 把世界划分为区域,围绕玩家所在区域按环形顺序生成地面、遮挡物和 Mutable 实例。RegionWidthInstances 控制实例生成范围,InstancesNum 控制每个区域数量,MaxInstancesPerTick 用来分帧生成。

UCustomizableObjectSystem* System = UCustomizableObjectSystem::GetInstance();
System->SetReplaceDiscardedWithReferenceMeshEnabled(true);

IConsoleVariable* CVar =
    IConsoleManager::Get().FindConsoleVariable(
        TEXT("b.OnlyUpdateCloseCustomizableObjects"));
CVar->Set(true);

if (UCustomizableInstanceLODManagement* LODManagement =
    Cast<UCustomizableInstanceLODManagement>(
        System->GetInstanceLODManagement()))
{
    LODManagement->SetCustomizableObjectsUpdateDistance(UpdateDistance);
}

Tick 里还会把玩家或 benchmark camera 作为 LOD 视点:

LODManagement->ClearViewCenters();
LODManagement->AddViewCenter(PlayerActor);

这对大规模 NPC、装备预览和开放世界人群很关键。Mutable 生成成本比普通换材质高得多;生产项目里通常要把“马上能看到的”“后台预热的”“远处可降级的”分成不同优先级。

配置、内存和打包

样例的 DefaultGame.ini 给 Mutable 较大的工作内存,并保留更新后的工作缓存:

[ConsoleVariables]
mutable.WorkingMemory=262144
mutable.ClearWorkingMemoryOnUpdateEnd=0

mutable.WorkingMemory 的单位是 KB。5.7 头文件说明它不是硬限制,但系统会尽量通过刷新内部缓存控制工作内存。内存给得多,一般能减少构建时间;内存给得少,更适合资源紧张平台。

[Features]
CustomizableObjectNumBoneInfluences=Eight

这个设置和骨骼权重数量有关。样例还开启了 r.GPUSkin.Support16BitBoneIndex=True,但关闭了 r.GPUSkin.UnlimitedBoneInfluences。角色资产如果使用复杂蒙皮、MetaHuman、装备合并或 cloth,需要提前规划骨骼索引和 influence 数量。

什么时候该用 Mutable

适合

  • 角色、武器、载具有大量可组合部件。
  • 需要运行时烘焙出优化后的 Skeletal Mesh。
  • 需要裁剪被衣服遮住的身体,减少穿模和无效渲染。
  • 需要把贴花、颜色、材质、mesh、morph、groom、cloth 放进统一管线。

不一定适合

  • 只是几套固定皮肤。
  • 只是换材质颜色或贴图。
  • 部件数量少,多个 SkeletalMeshComponent 组合已经够用。
  • 项目不能接受异步生成、内存峰值、cook 数据或插件 Beta/Experimental 风险。

保存、网络和生产建议

Mutable 实例本质上是一组参数。保存和联网时,不要保存生成出来的 USkeletalMesh 或运行时贴图引用,而应该保存参数快照。

USTRUCT(BlueprintType)
struct FMutableAppearanceSave
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FString State;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TMap<FString, FString> EnumOptions;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TMap<FString, float> FloatValues;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TMap<FString, FLinearColor> ColorValues;
};

加载时重新找到同一个 UCustomizableObject,创建实例,应用参数,绑定组件,再触发异步更新。网络同步也类似:服务器同步参数值、随机种子和版本号,客户端本地生成。要额外注意版本兼容;一旦 CO 图、参数名或 DataTable 选项改名,旧存档和旧网络包就需要迁移层。

常见坑

问题 原因 建议
改完参数模型没变 只改了实例参数,没有触发更新。 UpdateSkeletalMeshAsync() 或通过 Usage/组件路径触发更新。
同一帧读取 Skeletal Mesh 为空 Mutable 更新是异步队列。 用回调或 UpdatedDelegate
实例被 GC Instance / Usage 没有 UPROPERTY 引用。 像样例的 AInstanceActor 一样保存引用。
多组件错绑 用 index 绑定,图变化后 index 不稳定。 优先用 ComponentName,更新后用 GetComponentNames()
材质恢复不对 Mutable 生成资源可能有 override materials 或 mesh cache。 参考样例,使用 GetSkeletalMeshComponentOverrideMaterials()
大量角色卡顿 一帧创建太多实例或更新太多参数。 分帧生成,限制更新距离,设置 LOD 管理和工作内存。
保存生成资源 运行时 mesh/texture 是生成结果,生命周期归 Mutable/UE 管。 保存参数快照,不保存生成资源引用。

参考资料