快速结论
Mutable 解决的不是“把材质参数调成红色”这种浅层需求,而是“把网格、材质、贴图、morph、裁剪、贴花、子对象、表格数据编译成一套可运行时生成的组合规则”。运行时实例只保存参数;生成完成后,游戏拿到的是标准 UE 资源。
最小心智模型
UCustomizableObject 是图和编译数据,UCustomizableObjectInstance 是参数快照,UCustomizableObjectInstanceUsage 把实例接到 USkeletalMeshComponent。
最容易踩的点
改参数不等于网格立刻变。Mutable 更新是异步队列,要等回调、UpdatedDelegate 或组件更新完成后再读取生成结果。
编辑器里制作并编译 CustomizableObject。
运行时 CreateInstance(),得到默认参数实例。
设置 enum、float、color、projector 等参数。
异步更新后写入 Skeletal Mesh Component。
样例覆盖范围
这篇文章基于本机的 /Users/mchen/SyncShare/MutableSample。工程使用 Unreal Engine 5.7,开启了 Mutable、HairStrandsMutable、MutableClothing、RigLogic 等插件。C++ 模块里显式依赖 CustomizableObject 和 AnimationSharing。
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);
编辑器资产流程
- 准备输入资源。 准备参考骨架、基础 Skeletal Mesh、可替换部件、材质、贴图、DataTable。角色类项目要特别注意所有可组合部件的骨架、LOD、材质槽和贴图通道。
- 创建根 Customizable Object。 根对象负责定义最终对象的整体结构。样例里的
CO_Character是角色根对象,CO_Weapon是武器根对象。 - 设置 Mesh Component。 一个对象可以输出多个 mesh component。新代码尽量用组件名,而不是组件 index。
- 连接 Mesh / Material / Texture 节点。 基础图通常把 Skeletal Mesh 接到 Mesh Component,把材质和各纹理通道接到 Material 节点。
- 抽出参数。 把用户需要改的分支变成参数节点,例如 body、barrel、color、texture、projector。
- 用 Child Object 和 Group 管组合爆炸。 样例的武器拆成枪管、枪身、握把、弹匣、瞄具、枪托;角色也把外套、裤子、鞋、皮肤变化拆成子对象。
- 用 DataTable 管内容矩阵。
Content/Tutorials/TableNode展示了 Table Node。DataTable 适合维护“参数选项到资源集合”的关系。 - 设置 State 和 UI Metadata。 State 限制某个场景下哪些参数可改,UI metadata 给动态 UI 提供名称、分组、缩略图和显示策略。
- 编译并创建 COI。 编辑器里的
CustomizableObjectInstance资产可以保存一组参数预设。
武器样例里的参数很有代表性:Body、Barrels Types、Magazines Types、Grips Types、Stocks Types、Scopes Types、MainColor、SecondaryColor、TriggerColor、EmissiveColor、Decal Image 和 Projectors。这说明 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();
}
真实项目里还要保存 Instance 和 Usage 的引用,避免被 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 头文件里已经提示:如果要的是对象能生成的总组件数,优先从 UCustomizableObject 调 GetComponentCount();如果要的是实例更新完成后的实际组件名,用 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 管。 | 保存参数快照,不保存生成资源引用。 |
参考资料
- Mutable Quickstart Guide:官方使用场景和基本流程。
- Mutable FAQ:官方解释 Mutable 适合解决的问题、输出资源类型和功能边界。
- Mutable Resource Usage at Runtime:运行时 CPU、内存、磁盘流送和异步操作说明。
- Mutable Plugin API:插件模块、依赖和 API 入口。
- UCustomizableObject::CreateInstance:创建运行时实例。
- UCustomizableObjectInstance::GetComponentNames:实例生成后的组件名列表。
- UCustomizableObjectInstanceUsage::AttachTo:把 Usage 绑定到 Skeletal Mesh Component。