ClothPainter 模块全面系统分析
目录
1 模块概述
1.1 基本信息
| 属性 | 值 |
|---|---|
| 模块名称 | ClothPainter |
| 类型 | Editor |
| 描述 | 编辑器模块,用于在骨骼网格编辑器中绘制布料(Cloth)属性遮罩 |
| 位置 | Engine/Source/Editor/ClothPainter |
整个模块包含约 30 个文件,代码量适中,涵盖模块注册、编辑模式、绘制工具、几何适配器、UI 组件和属性定制等多个方面。它展示了一套完整的"编辑器内顶点绘制"框架,非常适合作为学习 UE5 编辑器工具开发与 Mesh Paint 系统集成的案例。
1.2 文件结构
ClothPainter/
├── ClothPainter.Build.cs # 模块构建配置
├── Public/
│ ├── ClothPaintingModule.h # 模块接口头文件
│ └── ... # 其他公开接口
├── Private/
│ ├── ClothPaintingModule.cpp # 模块注册、编辑模式注册、工具栏扩展
│ ├── ClothPainter.h/.cpp # 核心绘制器(IMeshPainter实现)
│ ├── ClothPaintToolBase.h/.cpp # 画笔工具基类
│ ├── ClothPaintTools.h/.cpp # 四种具体工具实现
│ ├── ClothMeshAdapter.h/.cpp # 布料网格几何适配器
│ ├── ClothPaintSettings.h # 绘制设置
│ ├── ClothPaintSettingsCustomization.h/.cpp # 设置面板定制
│ ├── ClothPaintToolCommands.h/.cpp # 工具命令注册
│ ├── ClothPainterCommands.h/.cpp # 绘制器命令注册
│ ├── ClothingPaintEditMode.h/.cpp # 编辑模式
│ ├── SClothPaintTab.h/.cpp # 绘制Tab主界面
│ ├── SClothPaintWidget.h/.cpp # 绘制Widget(属性面板)
│ ├── SClothAssetSelector.h/.cpp # 布料资产和遮罩选择器
│ └── ... # 其他辅助文件
1.3 模块依赖
该模块的 Build.cs 文件中列出了所有私有依赖模块,核心依赖如下:
| 依赖模块 | 用途 |
|---|---|
MeshPaint |
提供 IMeshPainter、IMeshPaintEdMode 等绘制基础设施 |
SkeletalMeshEditor |
骨骼网格编辑器,ClothPainter 作为其扩展嵌入 |
ClothingSystemRuntimeCommon |
布料运行时数据结构(FClothLODDataCommon、FPointWeightMap 等) |
ClothingSystemRuntimeNv / ClothingSystemRuntimeInterface |
布料模拟接口 |
ClothingSystemEditorInterface |
布料编辑器接口 |
EditorFramework / UnrealEd |
编辑器框架 |
PropertyEditor |
属性面板定制 |
ToolMenus / EditorWidgets |
工具栏与编辑器Widget |
AdvancedPreviewScene |
预览场景 |
2 必须知道的问题
2.1 为什么会有 ClothPainter 这个模块?它解决了什么问题?
核心定位
ClothPainter 是一个编辑器专用模块 ,它的存在是为了解决 UE5 中布料资产(Cloth Asset)的参数遮罩绘制问题。
在 UE 的布料系统中,每块布料(如一件披风、一面旗帜)都由 UClothingAssetCommon 资产描述。其中存储了物理网格数据,并为布料模拟提供了逐顶点的参数遮罩(Point Weight Maps),比如:
- MaxDistance:布料顶点在模拟中可以偏离原始位置的最大距离
- BackstopDistances / BackstopRadius:限制布料穿透角色身体的"背止"参数
这些参数直接影响布料在游戏中的表现。举个例子,肩甲区域的布料需要限制移动范围,而裙摆则应该自由飘动------这就需要逐顶点地"涂刷"不同的参数值。
如果没有这个模块会怎样?
如果手写,需要做这些事情:
- 在编辑器里找到布料资产的底层数据,对着几百上千个顶点逐个设置浮点值
- 或者通过外部工具(如 DCC 软件)绘制顶点颜色再导入,流程繁琐且难以迭代
- 没有渐变、平滑、填充等高级工具,只能暴力地逐点赋值
- 每次修改参数后,必须重新导出/导入数据才能看到效果
ClothPainter 把这些工作封装成了一个完整的编辑器内绘制工作流:在 Persona(骨骼网格编辑器)里打开布料绘制模式,选一个遮罩,像刷颜色一样用画笔涂下去,立刻在预览窗口中看到模拟效果的变化。
2.2 它和普通的 Mesh Paint(顶点绘制)是什么关系?
ClothPainter 是建立在 UE5 的 Mesh Paint 框架 之上的。它通过实现 IMeshPainter 接口接入引擎已有的绘制系统,复用了笔刷设置、射线检测、视口交互等基础设施。
但是它不是 用来做纹理绘制或顶点颜色绘制的------它专门服务于布料资产的 FPointWeightMap 数据。在 FClothMeshPaintAdapter 中可以看到,顶点颜色相关的接口(GetVertexColor、SetVertexColor)都被 checkf(false, ...) 保护,明确禁止了普通顶点色绘制。
2.3 这个模块在什么时候会被激活?
触发条件很简单:
- 在 Persona(骨骼网格编辑器)中打开一个包含布料资产的骨骼网格
- 在工具栏上点击 "Activate Cloth Paint" 按钮
- 编辑器进入
FClothingPaintEditMode,左侧会出现 Clothing 面板,包含资产选择器、遮罩列表和绘制属性面板
退出时点击 "Deactivate Cloth Paint" 或切换回默认模式即可。
3 模块整体架构解析
3.1 架构图
┌─────────────────────────────────────────────────────────────────────────┐
│ 骨骼网格编辑器 (SkeletalMeshEditor) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FClothPaintingModule (模块入口) │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ StartupModule() → SetupMode() → 注册FClothingPaintEditMode │ │ │
│ │ │ → RegisterMenus() → 注册工具栏按钮 │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FClothingPaintEditMode (编辑器模式) │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 继承自 IMeshPaintEdMode │ │ │
│ │ │ 持有 FClothPainter (核心绘制器) │ │ │
│ │ │ 负责 Enter() / Exit() 时的场景初始化和清理 │ │ │
│ │ │ 协调 SClothPaintTab (UI) 和 FClothPainter (逻辑) │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │SClothPaintTab│ │ FClothPainter │ │ FClothMeshPaintAdapter│ │
│ │ (UI主界面) │ │ (核心绘制器) │ │ (几何适配器) │ │
│ └──────────────┘ └──────────────────┘ └──────────────────────┘ │
│ │ │ │ │ │ │
│ │ ┌───────┴───────┐ │ ┌────────┴────────┐ │ 访问 FPointWeightMap │
│ │ ▼ ▼ │ ▼ ▼ │ │
│ │SClothAsset S │FClothPaintTool_Brush│ UClothingAssetCommon │
│ │Selector Cloth│FClothPaintTool_Grad │ ─ LODData │
│ │(资产/遮罩选择) Paint│FClothPaintTool_Smooth│ ─ PhysicalMeshData │
│ │ Widget │FClothPaintTool_Fill │ ─ PointWeightMaps │
│ └──────────────────────┴────────────────────┴────────────────────────┘
│ │
└─────────────────────────────────────────────────────────────────────────┘
3.2 模块划分
1. 模块入口层 ------ FClothPaintingModule
职责:
- 在
StartupModule()中注册编辑模式(FClothingPaintEditMode) - 注册工具栏按钮和菜单项(
RegisterMenus) - 向骨骼网格编辑器注入 Cloth Painting Tab(通过
FWorkflowApplicationModeExtender) - 在
ShutdownModule()中反注册所有扩展
特点:
- 使用
IMPLEMENT_MODULE宏注册为标准编辑器模块 - 工具栏扩展通过
ISkeletalMeshEditorModule的FSkeletalMeshEditorToolbarExtender实现 - 工作流模式扩展通过
FWorkflowCentricApplication::GetModeExtenderList()实现
2. 编辑模式层 ------ FClothingPaintEditMode
职责:
- 创建并持有
FClothPainter实例 - 负责进入/退出绘制模式时的环境管理:禁用高级渲染特性、设置预览网格、恢复布料模拟状态
- 作为 IMeshPaintEdMode 的子类,将输入事件路由给
FClothPainter
特点:
- 构造函数中创建
FClothPainter并调用Init() Enter()中调用SetSkeletalMeshComponent和EnterPaintMode()Exit()中恢复布料模拟、重新注册组件、恢复渲染特性
3. 核心绘制层 ------ FClothPainter
职责:
- 实现
IMeshPainter接口,连接引擎的 Mesh Paint 框架 - 管理绘制工具(Tool)的注册、切换和生命周期
- 处理射线检测、笔刷渲染、顶点属性读写
- 管理画刷设置(
UPaintBrushSettings)和绘制设置(UClothPainterSettings) - 协调模拟状态的启用/禁用
特点:
- 同时实现
TSharedFromThis<FClothPainter>,允许工具通过弱引用安全访问 - 在
Init()中注册 4 种工具、创建 UI Widget - 通过
SetSkeletalMeshComponent()建立骨骼网格组件的连接
4. 工具系统层 ------ FClothPaintToolBase 及子类
职责:
- 定义统一的工具接口(获取绘制操作、输入处理、视口渲染、设置对象等)
- 四种具体工具分别实现不同的绘制策略
四种工具:
| 工具 | 类名 | 功能 |
|---|---|---|
| Brush | FClothPaintTool_Brush |
标准笔刷,逐顶点将 PaintValue 值涂到遮罩上 |
| Gradient | FClothPaintTool_Gradient |
渐变,选择起点和终点顶点,计算加权插值应用到全局 |
| Smooth | FClothPaintTool_Smooth |
平滑,对笔刷区域内的顶点值做邻域平均模糊 |
| Fill | FClothPaintTool_Fill |
填充,以点击顶点为中心,用 BFS 向外扩展填充到阈值边界 |
每种工具都对应一个 UObject 子类作为其设置对象,通过 Details Panel 暴露可配置属性。
5. 几何适配器层 ------ FClothMeshPaintAdapter
职责:
- 继承自
FBaseMeshPaintGeometryAdapter,将布料网格数据适配为 Mesh Paint 框架可用的格式 - 管理选中的布料资产、LOD 和遮罩索引
- 提供顶点位置、邻居关系、射线相交检测等功能
- 构建和维护顶点邻居映射(用于 Smooth 和 Fill 工具)
特点:
- 不使用骨骼网格本身的顶点数据,而是使用
UDebugSkelMeshComponent::SkinnedSelectedClothingPositions(蒙皮后的布料顶点位置) - 在
InitializeVertexData()中从三角面片重建邻居映射 - 通过
FClothMeshPaintAdapterFactory工厂类创建
6. UI 层 ------ Slate Widget 系统
这一层由以下组件构成:
| 组件 | 职责 |
|---|---|
SClothPaintTab |
Clothing Tab 主界面,包含资产选择器和属性面板 |
SClothAssetSelector |
布料资产列表、遮罩列表、LOD 选择、Mesh Skinning 参数 |
SClothPaintWidget |
绘制时显示的属性面板(画刷设置 + 工具设置 + 绘制设置) |
FClothPaintSettingsCustomization |
为 UClothPainterSettings 添加工具选择下拉框 |
FClothPaintBrushSettingsCustomization |
隐藏笔刷的颜色视图模式(布料绘制不需要颜色) |
3.3 模块间依赖关系
FClothPaintingModule (入口)
↓ 注册
FClothingPaintEditMode (编辑模式)
↓ 持有
FClothPainter (核心绘制器)
├─→ FClothMeshPaintAdapter (数据适配) → UClothingAssetCommon / FPointWeightMap
├─→ FClothPaintToolBase × 4 (绘制工具)
│ └─→ 各自持有 UObject 设置对象
└─→ SClothPaintWidget (UI)
└─→ IDetailsView → 属性定制类
SClothPaintTab (Tab UI)
├─→ SClothAssetSelector (资产/遮罩/蒙皮参数)
├─→ SClothPaintWidget (绘制属性面板)
└─→ IDetailsView (布料资产详情)
3.4 数据流走向
正常绘制流程:
1. 用户在视口中按下鼠标左键
↓
2. IMeshPaintEdMode 将输入路由给 FClothPainter::PaintInternal()
↓
3. PaintInternal() 发射射线 → GetHitResult() → FClothMeshPaintAdapter::LineTraceComponent()
↓
4. 射线命中布料网格,获取 HitResult
↓
5. 根据 SelectedTool 判断是否逐顶点操作
├─ 逐顶点(Brush/Gradient):遍历笔刷范围内的顶点,逐个调用 GetPaintAction()
└─ 非逐顶点(Smooth/Fill):直接调用 GetPaintAction() 一次
↓
6. PaintAction 内部:
- 通过 FClothMeshPaintAdapter 获取顶点数据
- 通过 FClothPainter::GetPropertyValue() / SetPropertyValue() 读写遮罩值
- 应用工具特定逻辑(笔刷衰减/邻域平均/BFS填充)
↓
7. 调用 Adapter->PreEdit() / PostEdit() 通知资产修改
↓
8. FinishPainting() 结束事务,更新自动视图范围
资产选择切换流程:
1. 用户在 SClothAssetSelector 中选择布料资产/遮罩
↓
2. OnSelectionChanged 委托触发 → SClothPaintTab::OnAssetSelectionChanged()
↓
3. 调用 FClothPainter::OnAssetSelectionChanged(Asset, Lod, MaskIndex)
↓
4. 更新 DebugSkelMeshComponent 的可见性和显示标志
↓
5. 调用 FClothMeshPaintAdapter::SetSelectedClothingAsset() 更新适配器
↓
6. 通知所有工具 OnMeshChanged()(清空渐变起点/终点等缓存)
4 类级代码详解
4.1 FClothPaintingModule ------ 模块入口
cpp
class FClothPaintingModule : public IModuleInterface
{
virtual void StartupModule() override;
virtual void ShutdownModule() override;
void SetupMode();
void ShutdownMode();
// ...
};
关键方法分析:
StartupModule()
cpp
void FClothPaintingModule::StartupModule()
{
SetupMode();
ClothPaintToolCommands::RegisterClothPaintToolCommands();
FClothPainterCommands::Register();
UToolMenus::RegisterStartupCallback(...);
// 向 SkeletalMeshEditor 注册工具栏扩展器
ISkeletalMeshEditorModule& SkelMeshEditorModule = ...;
Extenders.Add(ISkeletalMeshEditorModule::FSkeletalMeshEditorToolbarExtender::CreateRaw(
this, &FClothPaintingModule::ExtendSkelMeshEditorToolbar));
}
启动时做了四件事:
- 注册编辑模式和 Tab 扩展
- 注册命令(工具命令和绘制器命令)
- 注册菜单(延迟到 UToolMenus 启动完成后)
- 向骨骼网格编辑器注入工具栏按钮
SetupMode()
cpp
void FClothPaintingModule::SetupMode()
{
Extender = FWorkflowApplicationModeExtender::CreateRaw(
this, &FClothPaintingModule::ExtendApplicationMode);
FWorkflowCentricApplication::GetModeExtenderList().Add(Extender);
FEditorModeRegistry::Get().RegisterMode<FClothingPaintEditMode>(
PaintModeID, LOCTEXT("ClothPaintEditMode", "Cloth Painting"), FSlateIcon(), false);
}
这里的 FWorkflowApplicationModeExtender 是关键------它允许模块向 SkeletalMesh Editor 的应用模式中动态注入 Tab 工厂。当编辑器进入 "SkeletalMeshEditorMode" 时,会自动调用 ExtendApplicationMode 添加 Clothing Tab 的工厂方法。
OnToggleMode()
cpp
void FClothPaintingModule::OnToggleMode(TWeakPtr<ISkeletalMeshEditor> InSkeletalMeshEditor) const
{
FEditorModeTools& ModeManager = SkeletalMeshEditor->GetEditorModeManager();
if (!IsPaintModeActive(InSkeletalMeshEditor))
{
ModeManager.ActivateMode(PaintModeID, true);
// 设置 PersonaToolkit 和 ClothPaintTab
}
else
{
ModeManager.DeactivateMode(PaintModeID);
}
}
4.2 FClothingPaintEditMode ------ 编辑模式
cpp
class FClothingPaintEditMode : public IMeshPaintEdMode
{
TSharedPtr<FClothPainter> ClothPainter;
TWeakPtr<IPersonaToolkit> PersonaToolkit;
TWeakPtr<SClothPaintTab> ClothPaintTab;
};
核心流程:
Initialize()
cpp
void FClothingPaintEditMode::Initialize()
{
ClothPainter = MakeShared<FClothPainter>();
MeshPainter = ClothPainter.Get(); // 设置基类的MeshPainter指针
ClothPainter->Init();
}
注意这里 MeshPainter 是 IMeshPaintEdMode 的成员指针。所有来自引擎绘制框架的回调(InputKey、Tick、Render 等)都通过这个指针路由给 FClothPainter。
Enter() / Exit()
进入时:禁用视口高级渲染特性(后处理等),获取预览场景的骨骼网格组件,传递给 ClothPainter。
退出时最关键的工作是恢复布料模拟状态:
- 重新启用布料模拟(
bDisableClothSimulation = false) - 调用
ApplyParameterMasks将所有遮罩数据应用到布料模拟参数 - 恢复网格区域可见性
- 遍历所有引用该骨骼网格的组件,重新注册以触发模拟重建
4.3 FClothPainter ------ 核心绘制器
该类实现了 IMeshPainter 接口,代码约 600 行,是模块的核心大脑。
关键成员变量:
cpp
TSharedPtr<IMeshPaintGeometryAdapter> Adapter; // 几何适配器
TObjectPtr<UDebugSkelMeshComponent> SkeletalMeshComponent; // 目标骨骼网格
TSharedPtr<SClothPaintWidget> Widget; // UI Widget
TObjectPtr<UClothPainterSettings> PaintSettings; // 绘制设置
TObjectPtr<UPaintBrushSettings> BrushSettings; // 笔刷设置
TSharedPtr<FClothPaintToolBase> SelectedTool; // 当前选中工具
TArray<TSharedPtr<FClothPaintToolBase>> Tools; // 所有已注册工具
Init() ------ 工具注册
cpp
void FClothPainter::Init()
{
BrushSettings = DuplicateObject<UPaintBrushSettings>(...);
PaintSettings = DuplicateObject<UClothPainterSettings>(...);
CommandList = MakeShareable(new FUICommandList);
Tools.Add(MakeShared<FClothPaintTool_Brush>(AsShared()));
Tools.Add(MakeShared<FClothPaintTool_Gradient>(AsShared()));
Tools.Add(MakeShared<FClothPaintTool_Smooth>(AsShared()));
Tools.Add(MakeShared<FClothPaintTool_Fill>(AsShared()));
SelectedTool = Tools[0];
SelectedTool->Activate(CommandList);
Widget = SNew(SClothPaintWidget, this);
}
这里使用 DuplicateObject 复制 CDO 作为设置对象,并 AddToRoot() 防止被 GC。工具注册采用简单的数组方式,新增工具只需在 Tools.Add() 后再加一行。默认选中第一个工具(Brush)。
PaintInternal() ------ 绘制核心循环
cpp
bool FClothPainter::PaintInternal(const FVector& InCameraOrigin,
const TArrayView<TPair<FVector, FVector>>& Rays,
EMeshPaintAction PaintAction, float PaintStrength)
这是每次鼠标操作时被引擎调用的入口。流程如下:
- 检查是否有有效的布料资产选中(
SelectedClothingGuidForPainting.IsValid()) - 检查是否在模拟模式下(模拟时不允许绘制)
- 遍历所有射线(支持多点触摸)
- 每条射线做一次
GetHitResult()射线检测 - 首次命中时开始事务(
BeginTransaction)和预编辑(Adapter->PreEdit()) - 根据工具类型选择逐顶点或非逐顶点模式执行绘制动作
- 返回是否实际应用了绘制
Tick() ------ 每帧更新
cpp
void FClothPainter::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
{
// 同步视图范围设定到骨骼网格组件
SkeletalMeshComponent->MinClothPropertyView = PaintSettings->GetViewMin();
SkeletalMeshComponent->MaxClothPropertyView = PaintSettings->GetViewMax();
// 自动视图范围时,纳入工具的值范围
// 同步法线翻转、背面剔除、透明度等渲染设置
// 处理模拟状态切换(按H键)
if ((bShouldSimulate && SkeletalMeshComponent->bDisableClothSimulation) || ...)
{
// 进入模拟:应用遮罩到布料参数
// 退出模拟:重新注册组件
}
// 每帧重新初始化适配器,保证蒙皮顶点位置最新
if(Adapter.IsValid())
Adapter->Initialize();
}
值得注意的交互设计:按 H 键 切换布料模拟开关,按 J 键切换隐藏顶点可见性。这让开发者可以立即预览绘制结果在布料模拟中的实际效果。
OnAssetSelectionChanged() ------ 资产切换处理
cpp
void FClothPainter::OnAssetSelectionChanged(UClothingAssetCommon* InNewSelectedAsset,
int32 InAssetLod, int32 InMaskIndex)
这个方法负责在用户切换布料资产/遮罩时同步所有相关状态:
- 隐藏旧资产对应的网格区域,显示新资产
- 更新
SelectedClothingGuidForPainting、LOD 和 Mask 索引 - 调用
RefreshSelectedClothingSkinnedPositions()刷新蒙皮顶点缓存 - 重置适配器的选中资产
- 通知所有工具
OnMeshChanged()清空缓存数据
4.4 FClothMeshPaintAdapter ------ 几何适配器
这是连接绘制系统和布料数据的桥梁,继承自 FBaseMeshPaintGeometryAdapter。
核心数据结构:
cpp
struct FClothAssetInfo
{
int32 VertexStart, VertexEnd; // 该资产在缓冲区中的顶点范围
int32 IndexStart, IndexEnd; // 该资产在缓冲区中的索引范围
TArray<TArray<int32>> NeighborMap; // 顶点邻居映射
UClothingAssetCommon* Asset;
};
InitializeVertexData() ------ 构建顶点和邻居数据
这是适配器最核心的方法。它从 UDebugSkelMeshComponent::SkinnedSelectedClothingPositions 获取经过蒙皮的布料顶点位置(注意这里不是物理网格的原始位置,而是经过骨骼动画变形后的位置)。然后用物理网格的索引构建三角形,并从中推导每个顶点的邻居列表。
邻居映射的构建方式:遍历所有三角形 (I0, I1, I2),对每个顶点,将另外两个顶点加入其邻居列表(用 AddUnique 去重)。
LineTraceComponent() ------ 射线相交检测
由于布料网格是与骨骼网格分离的独立网格,标准的碰撞检测不适用。这里实现了自定义的三角形-线段相交检测:
cpp
bool ClothSegmentTriangleIntersection(const FVector& StartPoint, const FVector& EndPoint,
const FVector& A, const FVector& B, const FVector& C,
FVector& OutIntersectPoint, FVector& OutTriangleNormal)
遍历适配器中的所有三角形,找到距离射线起点最近的交点。这是布料绘制的核心交互方式------鼠标光标通过射线检测找到布料表面上的精确位置。
5 工具系统深度剖析
5.1 工具基类 ------ FClothPaintToolBase
cpp
class FClothPaintToolBase
{
explicit FClothPaintToolBase(TWeakPtr<FClothPainter> InPainter)
: Painter(InPainter) {}
virtual FPerVertexPaintAction GetPaintAction(...); // 获取绘制操作委托
virtual bool InputKey(...); // 处理按键输入
virtual bool ShouldRenderInteractors() const; // 是否渲染笔刷交互器
virtual FText GetDisplayName() const = 0; // UI显示名称
virtual bool IsPerVertex() const; // 逐顶点 or 一次性操作
virtual void Render(...); // 渲染自定义视口数据
virtual void Activate(TWeakPtr<FUICommandList>); // 工具激活
virtual void Deactivate(TWeakPtr<FUICommandList>); // 工具停用
virtual void OnMeshChanged(); // 网格变更通知
virtual bool HasValueRange(); // 是否提供值范围
virtual void GetValueRange(float&, float&); // 获取值范围
virtual UObject* GetSettingsObject(); // 获取设置UObject
virtual void RegisterSettingsObjectCustomizations(IDetailsView*); // 注册属性定制
};
这套接口设计遵循了清晰的**"工具模式"**(Tool Pattern)。每个工具都是一个独立的状态机,通过 Painter 的弱引用访问绘制器提供的服务:
GetPropertyValue(VertexIndex)------ 读取当前遮罩值SetPropertyValue(VertexIndex, Value)------ 写入遮罩值GetBrushSettings()------ 获取笔刷半径、强度、衰减等GetAdapter()------ 获取几何适配器
工具的 GetSettingsObject() 返回一个 UObject 子类,该对象会被放入 Details Panel,让用户可以在 UI 中调整工具特定参数。
5.2 Brush 工具 ------ 标准笔刷绘制
cpp
class FClothPaintTool_Brush : public FClothPaintToolBase
{
UClothPaintTool_BrushSettings* Settings;
void PaintAction(FPerVertexPaintActionArgs& InArgs, int32 VertexIndex, FMatrix InverseBrushMatrix)
{
// 获取顶点世界空间位置
// 调用 MeshPaintHelpers::ApplyBrushToVertex() 计算笔刷衰减
// 将 Settings->PaintValue 混合到当前顶点值
}
};
Brush 是最基础的工具,使用 MeshPaintHelpers::ApplyBrushToVertex() 进行标准的笔刷衰减计算。该函数根据顶点到笔刷中心的距离、笔刷半径和衰减参数,将 PaintValue 加权混合到当前顶点值中。
IsPerVertex() 返回 true ------在 PaintInternal() 中引擎会为笔刷范围内的每个顶点调用一次 PaintAction()。
5.3 Gradient 工具 ------ 线性渐变
cpp
class FClothPaintTool_Gradient : public FClothPaintToolBase
{
bool bSelectingBeginPoints;
TArray<int32> GradientStartIndices;
TArray<int32> GradientEndIndices;
};
Gradient 工具的实现非常有"编辑器操作流程"的味道。它的工作分为两个阶段:
-
选择阶段 :默认选择起始点(绿色),按住 Ctrl 切换到选择结束点(红色)。每点一次鼠标,将笔刷范围内的顶点加入对应的列表。
IsPerVertex()返回false------PaintAction 不执行逐顶点绘制,而是执行顶点选择逻辑。 -
应用阶段 :按 Enter 键调用
ApplyGradient()。该函数遍历网格的所有顶点,计算每个顶点到最近起始点和最近结束点的距离比,用FMath::LerpStable()插值出目标值。计算公式:Value = Lerp(GradientStartValue, GradientEndValue, DistToStart² / (DistToStart² + DistToEnd²))
代码注释里留了一个 TODO:"Look into surface distance instead of 3D distance"。这说明开发者意识到在某些复杂几何形状上,三维欧几里得距离可能不如曲面距离合理。
5.4 Smooth 工具 ------ 邻域平滑
cpp
class FClothPaintTool_Smooth : public FClothPaintToolBase
{
void SmoothVertices(const TSet<int32>& InfluencedVertices, TSharedPtr<FClothPainter> SharedPainter)
{
// 第一遍:对每个受影响的顶点,计算邻居平均值
for(const int32 Index : InfluencedVertices)
{
const TArray<int32>* Neighbors = Adapter->GetVertexNeighbors(Index);
float Accumulator = 0.0f;
for(int32 NeighborIndex : (*Neighbors))
Accumulator += SharedPainter->GetPropertyValue(NeighborIndex);
NewValues[i] = Accumulator / Neighbors->Num();
}
// 第二遍:用 Strength 参数插值到新值
for(int32 Index : InfluencedVertices)
{
float Current = SharedPainter->GetPropertyValue(Index);
SharedPainter->SetPropertyValue(Index, Current + Difference * Settings->Strength);
}
}
};
Smooth 实现了一个标准的"箱式模糊"(Box Blur)。它特别地用两遍处理:第一遍计算所有邻居平均值,第二遍才写入。这样可以避免"边平滑边读取已修改的邻居值"导致的偏差。
Settings->Strength 参数(0.0~1.0)控制平滑强度,0 表示不变,1 表示完全用平均值替换。
Smooth 工具还通过 FSmoothToolCustomization 添加了一个"一键平滑整网"按钮,让开发者无需手动刷选就可以对整个网格做平滑操作。
5.5 Fill 工具 ------ 区域填充
cpp
class FClothPaintTool_Fill : public FClothPaintToolBase
{
float QueryRadius = 20.0f;
void PaintAction(FPerVertexPaintActionArgs& InArgs, int32 VertexIndex, FMatrix InverseBrushMatrix)
{
// 找到最近的顶点作为种子
int32 ChosenIndex = Verts[0].Key;
// 获取种子顶点的当前值,确定阈值范围
const float QueryValue = SharedPainter->GetPropertyValue(ChosenIndex);
const float MinQueryValue = QueryValue - Settings->Threshold;
const float MaxQueryValue = QueryValue + Settings->Threshold;
// 用BFS队列从种子向外扩展
TQueue<int32> VertQueue;
SharedPainter->SetPropertyValue(ChosenIndex, Settings->FillValue);
VertQueue.Enqueue(ChosenIndex);
while(!VertQueue.IsEmpty())
{
VertQueue.Dequeue(CurrIndex);
const TArray<int32>* Neighbors = Adapter->GetVertexNeighbors(CurrIndex);
for(int32 NeighborIndex : (*Neighbors))
{
const float NeighborValue = SharedPainter->GetPropertyValue(NeighborIndex);
if(NeighborValue != Settings->FillValue &&
NeighborValue >= MinQueryValue && NeighborValue <= MaxQueryValue)
{
SharedPainter->SetPropertyValue(NeighborIndex, Settings->FillValue);
VertQueue.Enqueue(NeighborIndex);
}
}
}
}
};
Fill 工具实现了一个经典的 BFS 洪水填充(Flood Fill)算法:
- 在笔刷查询范围内找到距离最近的顶点作为种子点
- 记录种子点的当前值,用
Settings->Threshold确定允许填充的值范围 - 将种子点加入 BFS 队列
- 循环处理:弹出当前顶点,遍历其邻居;如果邻居的当前值在阈值范围内且不等于目标值,则设置为目标值并加入队列
这个算法特别适合快速将一块连续的相似值区域统一改为指定值------比如把整片"最大距离 50"的区域一键改成"最大距离 80"。
6 UI 系统分析
6.1 SClothPaintTab ------ Tab 主界面
SClothPaintTab 是 Clothing Tab 的顶层容器。它的 UI 结构如下:
SClothPaintTab
├── SScrollBox (ContentBox)
│ ├── SClothAssetSelector (资产选择器)
│ │ ├── 布料资产列表 (SAssetList)
│ │ └── 遮罩列表 (SMaskList)
│ ├── IDetailsView (布料资产属性)
│ └── SClothPaintWidget (绘制属性面板,EnterPaintMode时添加)
EnterPaintMode() 和 ExitPaintMode() 方法控制 SClothPaintWidget 的添加和移除。这种设计使得非绘制模式下的 Tab 内容更简洁。
6.2 SClothAssetSelector ------ 资产与遮罩选择器
这是 UI 中最复杂的组件,代码超过 1500 行。它提供:
- 布料资产列表:用 ListView 展示骨骼网格上的所有布料资产,支持重命名(SInlineEditableTextBlock)、删除、导出和 LOD 参数重建
- 遮罩列表:三列(启用/名称/目标),支持添加、删除、重命名、设置遮罩目标类型
- LOD 选择:通过 ComboButton 下拉菜单切换当前编辑的 LOD
- Mesh Skinning 面板:控制多重影响(Multiple Influences)、内核半径(Kernel Radius)、平滑过渡(Smooth Transition)
- 从其他骨骼网格复制布料:通过 Content Browser 的 Asset Picker 选择源骨骼网格
遮罩列表的右键菜单提供了"Set Target"子菜单,允许将遮罩分配给不同的模拟参数目标(MaxDistance、BackstopDistances 等),以及"Copy From Vertex Color"功能,可以从顶点颜色通道复制数据到遮罩。
6.3 SClothPaintWidget ------ 绘制属性面板
cpp
void SClothPaintWidget::Construct(const FArguments& InArgs, FClothPainter* InPainter)
{
Objects.Add(Painter->GetBrushSettings());
Objects.Add(Painter->GetPainterSettings());
UObject* ToolSettings = Painter->GetSelectedTool()->GetSettingsObject();
if(ToolSettings)
Objects.Add(ToolSettings);
CreateDetailsView(InPainter);
}
该组件使用 IDetailsView 显示三个对象的属性:
UPaintBrushSettings------ 笔刷半径、强度、衰减UClothPainterSettings------ 视图范围、法线翻转、背面剔除、透明度- 当前工具的设置对象(如
UClothPaintTool_BrushSettings的 PaintValue)
通过 FClothPaintSettingsCustomization 在 UClothPainterSettings 的属性面板顶部插入了一个工具选择下拉框,允许用户在 Brush / Gradient / Smooth / Fill 之间切换。
FClothPaintBrushSettingsCustomization 则隐藏了 ColorViewMode 属性,因为布料绘制不需要颜色查看模式。
7 总结与最佳实践
7.1 架构设计优势
- 清晰的分层架构:Module → EditMode → Painter → Tool/Adapter,每层职责明确
- 可扩展的工具系统 :通过继承
FClothPaintToolBase并注册到Tools数组即可添加新工具 - 与引擎框架深度集成:复用了 Mesh Paint 的射线检测、笔刷渲染、事务管理等基础设施
- 分离的设置模型:通用设置(笔刷)和工具特定设置(PaintValue、Threshold 等)分离管理
- 实时预览:H 键切换布料模拟,立即看到绘制结果在模拟中的实际效果
7.2 潜在改进点
- 曲面距离的渐变计算:Gradient 工具的代码中已标注 TODO,考虑使用曲面距离代替三维欧几里得距离
- 撤销系统完善 :当前资产的修改通过
Modify()和FScopedTransaction处理,但某些工具(如 Smooth)在 PaintAction 中临时设置bArePainting标志,可能导致事务状态不完整 - 工具自定义优先级 :所有资源加载走
FStreamableManager的高优先级通道,对于不需要即时响应的后台操作可以考虑低优先级 - Gradient 工具被覆盖时的处理 :如果渐变工具选择了起始/结束点后用户切换了工具再切回来,这些点会被
OnMeshChanged()清空但没有提示
7.3 作为一个编辑器工具开发案例的价值
ClothPainter 模块是学习 UE5 编辑器工具开发 的绝佳案例,原因如下:
- 完整的生命周期管理:展示了从模块注册、编辑模式创建、进入/退出清理到 UI 装配的完整流程
- Slate UI 的实战应用:SCompoundWidget、SListView、Details Panel 定制、工具栏扩展等技术都在这里有实际应用
- 工具模式设计:工具基类的接口设计可以作为"编辑器工具系统"的参考模式
- Proxy/Adapter 模式:FClothMeshPaintAdapter 展示了如何将领域数据适配为通用框架可消费的格式
- 射线检测的替代实现:当引擎的物理碰撞系统不适用于自定义网格时的手动三角形相交检测
7.4 几个值得记住的点
- 进入编辑模式后,别忘了在退出时恢复一切 。
FClothingPaintEditMode::Exit()中对模拟状态、组件注册、渲染特性的恢复是一个很好的参考 - 设置对象用
DuplicateObject而非直接修改 CDO 。FClothPainter::Init()中对UPaintBrushSettings和UClothPainterSettings的处理方式,避免了修改类默认对象 - 工具通过 Painter 的弱引用而非直接持有数据 。
TWeakPtr<FClothPainter>的设计避免了循环引用,也让工具在 Painter 销毁后不会悬空 - 邻居映射的预计算 。
FClothMeshPaintAdapter::InitializeVertexData()在网格初始化时一次性构建邻居关系,Smooth 和 Fill 工具直接查询即可,无需每次操作都重新计算 - BFS 洪水填充的模式值得记住。Fill 工具的实现是一个干净的图遍历应用,可以迁移到很多需要"连通区域操作"的场景
希望这篇文章能帮助大家理解 ClothPainter 模块的设计思想。它不复杂,但该有的都有了------清晰的分层、可扩展的工具架构、与引擎框架的良好集成------是一个"小而美"的编辑器模块范例。