UE4 InteractiveToolsFramework研究

UE Runtime下如何实现交付编辑

· UE技术分享

        开发三维图形工具软件,开发者往往需要在Runtime模式下进行几何对象的交互编辑,如果完全从头开发难度非常巨大,我们可以通过UE4.26提供的InteractiveToolsFramework插件很方便地实现几何对象自由场景编辑,该插件可以在\Engine\Source\Runtime\Experimental下找到。  

一、框架总体结构 

broken image
broken image

​   

为了便于理解,我们暂且将系统分解为交互框架、工具系统、输入接口、小部和对象管理五部分。 

1) 交互框架(InteractiveToolsFramework) 

        交互工具框架InteractiveToolsFramework,简称(ITF),最核心的类为UInteractiveToolsContext,该类是交互工具框架的最顶层,该类聚合了UInteractiveToolManager、UInteractiveGizmoManager、UInputRouter等交互编辑核心管理和流程定义类,这些对象的API本质上提供了一种三维场景对象交互编辑的抽象——交互编辑的流程和规则。 

        同时交互框架通过UInteractiveToolsContext集成了IToolsContextQueriesAPI、IToolsContextTransactionsAPI、IToolsContextAssetAPI的调用入口,便于在处理交付编辑时相关模块方便地获得上下文信息。  

 ​     IToolsContextQueriesAPI提供从编辑器容器查询状态信息的功能。 最关键的是 GetCurrentSelectionState(),ToolManager 将使用它来确定将哪些选定的 actor 和组件传递给 ToolBuilder。 在使用 ITF 时,您可能需要对此进行自定义实现。 许多工具和 TRS Gizmos 也需要 GetCurrentViewState() 才能正常工作,因为它提供 3D 相机/视图信息。

​​​​​​class IToolsContextQueriesAPI{    void GetCurrentSelectionState(FToolBuilderState& StateOut);    void GetCurrentViewState(FViewCameraState& StateOut);    EToolContextCoordinateSystem GetCurrentCoordinateSystem();    bool ExecuteSceneSnapQuery(const FSceneSnapQueryRequest& Request, TArray& Results );    UMaterialInterface* GetStandardMaterial(EStandardToolContextMaterials MaterialType); }

broken image

undefined

       IToolsContextTransactionsAPI 主要用于将数据发送回编辑器容器, DisplayMessage() 由带有各种用户信息消息、错误和状态消息等的工具调用,PostInvalidation() 用于指示需要重绘,在引擎以最大/固定帧速率持续重绘的运行时上下文中; RequestSelectionChange() 是某些工具提供的提示,通常在它们创建新对象时提供;AppendChange() 由想要发出 FCommandChange 记录(实际上是 FToolCommandChange 子类)的工具调用,这是 ITF 撤消/重做方法的核心组件;BeginUndoTransaction() 和 EndUndoTransaction() 是相关的函数,用于标记应该分组的一组 Change 记录的开始和结束 - 通常 AppendChange() 将在其间被调用一次或多次。  

​​​​class IToolsContextTransactionsAPI{    

void DisplayMessage(const FText& Message, EToolMessageLevel Level);  

 void PostInvalidation();    bool RequestSelectionChange(const FSelectedOjectsChangeList& SelectionChange);   

 void BeginUndoTransaction(const FText& Description);    void AppendChange(UObject* TargetObject, TUniquePtr Change, const FText& Description);    void EndUndoTransaction(); }

broken image

broken image

        IToolsContextRenderAPI 传递给 UInteractiveTool::Render() 和 UInteractiveGizmo::Render() 以提供常见渲染任务所需的信息。 GetPrimitiveDrawInterface() 返回一个抽象的 FPrimitiveDrawInterface API 的实现,它是一个标准的 UE 接口,提供线和点绘制功能(通常缩写为 PDI)。各种工具使用 PDI 来绘制基本的线反馈,例如在绘制多边形工具中绘制当前多边形的边缘。

​​class IToolsContextRenderAPI{   

 FPrimitiveDrawInterface* GetPrimitiveDrawInterface();    

FViewCameraState GetCameraState();    

const FSceneView* GetSceneView();    

EViewInteractionState GetViewInteractionState();

}

broken image

broken image

        ITooslContextAssetAPI 定义了创建新的网格对象的接口, 编辑器建模工具使用 GenerateStaticMeshActor() 函数来生成新的静态网格物体资产/组件/Actor,例如在绘制多边形工具中,该函数使用挤出的多边形(AssetConfig 参数的一部分)调用以创建资产 。

​​class IToolsContextAssetAPI{   

 AActor* GenerateStaticMeshActor(  UWorld* TargetWorld,        FTransform Transform,        FString ObjectBaseName,        FGeneratedStaticMeshAssetConfig&& AssetConfig);

}

broken image

broken image

2)工具系统(Tools, ToolBuilders, ToolManager)        

        工具系统定义了如何构建、扩展和管理各类编辑工具建立一种可以非常灵活扩展的场景编辑工具体系,开发者可以 根据需求开发、定制、扩展出五花八门的编辑工具,各类工具主要支持对三维场景中的几何对象进行交互绘制、修改、运算,如:UDrawPolygonTool定义了多边形绘制工具、USingleSelectionTool定义了单选对象处理工具、USingleClickTool定义了单击鼠标输出工具。  

 

         UInteractiveToolManager 的使命为全程管理各类编辑工具。一般工具通过RegisterToolType() 将字符串标识符与 ToolBuilder 实现相关联。 然后应用程序使用 SelectActiveToolType() 设置一个活动的 Builder,然后使用 ActivateTool() 创建一个新的 UInteractiveTool 实例。 有 getter 来访问活动的 Tool,但实际上很少有人频繁调用它。 Render() 和 Tick() 函数必须由应用程序在每一帧调用,然后应用程序调用活动工具的关联函数。 最后,DeactiveTool() 用于终止活动工具。 

UCLASS()class UInteractiveToolManager : public UObject, public IToolContextTransactionProvider{    

void RegisterToolType(const FString& Identifier, UInteractiveToolBuilder* Builder);    

bool SelectActiveToolType(const FString& Identifier);   

 bool ActivateTool();    void Tick(float DeltaTime);    void Render(IToolsContextRenderAPI* RenderAPI);    void DeactivateTool(EToolShutdownType ShutdownType); };

broken image
broken image
broken image

        UInteractiveTool 是所有编辑工具的基类,定义了编辑工具的创建、绘制、关闭流程和状态,UInteractiveTool 不是一个独立的对象,并不能简单地New一个工具, 要使其发挥作用,必须调用 Setup/Render/Tick/Shutdown,并将 IToolsContextRenderAPI相关接口在初始化时传递给工具。开发者必须在Setup() 中初始化工具;在 Shutdown() 中执行终结和清理,此时也是执行“应用”操作真正地保存编辑的对象的重要时机。

​​UCLASS()class UInteractiveTool : public UObject, public IInputBehaviorSource{    

void Setup();    

void Shutdown(EToolShutdownType ShutdownType);   

void Render(IToolsContextRenderAPI* RenderAPI);    

void OnTick(float DeltaTime);    bool HasAccept();    bool CanAccept(); };

broken image

broken image

        Shutdown函数有个类型为EToolShutdownType的参数,定义了工具关闭时是完成、接受编辑操作还是取消操作,相关类型可以通过 HasAccept() 和 CanAccept() 函数进行定义。       

​​enum class EToolShutdownType{

/** Tool cleans up and exits. Pass this to tools that do not have Accept/Cancel options. */

Completed = 0,

/** Tool commits current preview to scene */

Accept = 1,

/** Tool discards current preview without modifying scene */ Cancel = 2

};

broken image

broken image

        最后,每个工具实现各自的Render接口来实现工具相关辅助线、辅助点绘制或交互编辑信息的屏幕输出。

        每个编辑工具都可以通过重载::OnTick实现自己的 ::Tick() 函数,如:多UDrawPolygonTool就在OnTick中实现了编辑对象的控制点捕捉计算功能。  

 

         UInteractiveToolBuilder是创建工具的工厂模式类的基类,所有工具都必须有对应TooBuilder工厂类。在编辑工具框架中,所有工具的真正实例化都需要要向UInteractiveToolManager 发出请求,因此,我们必须让 ToolManager具备随时构建任意合法类型的工具。在此,我们采用的办法是将为每类工具实现UInteractiveToolBuilder的扩展类,开发者在初始化编辑系统时通过ToolManager的RegisterToolType接口向 ToolManager 注册一个  键值对。  

UCLASS()class UInteractiveToolBuilder : public UObject{    bool CanBuildTool(const FToolBuilderState& SceneState);    UInteractiveTool* BuildTool(const FToolBuilderState& SceneState);};

broken image
broken image

 3)输入接口(Input Behavior System)

       在交互编辑中对操作行为进行逻辑判断(准确的说时基于各种输入的先后关系来判断当编辑工具的操作模式,比如:判断鼠标点击的click事件、添加多边形时的顺序输入事件、拖动节点的拖拽事件Drag、悬停时间Hover)是一件相当繁琐的事情。同时对于实现交互编辑逻辑来说也是相当重要的,为了避免用户扩展时陷入到这些繁琐的逻辑判断中,ITF实现对交互输入逻辑的底层模型,并采用订阅机制暴露给上层工具,相关工具只要实现特定的IModifierToggleBehaviorTarget接口便可以合理地处理相关交互事件。

         ITF 中,类似OnMouseMove、OnMouseDown、OnMouseUp等各类输入事件处理状态机都通过 UInputBehavior 来实现,便于正在众多编辑工具之间共享。事实上,一些简单的行为,如 USingleClickInputBehavior、UClickDragBehavior 和 UHoverBehavior,可以处理大多数鼠标驱动交互的情况,编辑工具(UInteractiveTool)或小部件(Gizmo)通过实现的各种BehaviorTarget接口就可以获得相应事件的处理权。例如,USingleClickInputBehavior 实现了IClickBehaviorTarget 接口,通过IsHitByClick()方法判断是否可以触发点击事件 ,如果可以触发则触发 OnClicked()来处理单击鼠标逻辑。在InpBehavior初始化时需要传入相应的BehaviorTarget。

   ​UCLASS()class UInputBehavior : public UObject{    

FInputCapturePriority GetPriority();   

 EInputDevices GetSupportedDevices();    FInputCaptureRequest WantsCapture(const FInputDeviceState& InputState);    FInputCaptureUpdate BeginCapture(const FInputDeviceState& InputState);    FInputCaptureUpdate UpdateCapture(const FInputDeviceState& InputState);    void ForceEndCapture(const FInputCaptureData& CaptureData);    // ... hover support... }

broken image

        采用输入接口(Input Behavior System) 一个十分重要的作用就是将编辑工具( Tools)和输入路由(UInputRouter)解耦。系列编辑工具只需要将希望激活的 UInputBehavior通过AddInputBehavior方法添加到其InputBehaviors中(通常会在其::Setup()中),并在激活编辑工具时(UInteractiveToolManager::ActivateTool)将其InputBehaviors列表注册(RegisterBehavior)到UInputRouter的ActiveInputBehaviors。

class UInteractiveTool : public UObject, public IInputBehaviorSource{    // ...previous functions...    void AddInputBehavior(UInputBehavior* Behavior);    const UInputBehaviorSet* GetInputBehaviors();};

broken image

         相关输入由UInputRouter进行集中处理,它首先会通过CollectWantsCapture收集哪些InputBehavior需要Capture对应的Input。

void UInputBehaviorSet::CollectWantsCapture(const FInputDeviceState& input, TArray& result){

    for (auto b : Behaviors) {

// only call WantsCapture if the Behavior supports the current input device

          if (SupportsInputType(b.Behavior, input)) {

FInputCaptureRequest request = b.Behavior->WantsCapture(input); if ( request.Type != EInputCaptureRequestType::Ignore ) { request.Owner = b.Source; result.Add(request); } } } }

broken image
broken image
broken image

         然后,根据收集到的CaptureRequests对Behaviors进行进行消息分发,并设定当前的ActiveLeftCapture。

for (int i = 0; i < CaptureRequests.Num() && bAccepted == false; ++i) {

    FInputCaptureUpdate Result = CaptureRequests[i].Source->BeginCapture(Input, EInputCaptureSide::Left);

if (Result.State == EInputCaptureState::Begin) {

        // end outstanding hover TerminateHover(EInputCaptureSide::Left); ActiveLeftCapture = Result.Source; ActiveLeftCaptureOwner = CaptureRequests[i].Owner; ActiveLeftCaptureData = Result.Data; bAccepted = true; } }

broken image

        完成以上一系列操作后,UInputRouter会便会在适当的时机不断地发出(Post)相关事件,由对应的实现了相关IModifierToggleBehaviorTarget的工具进行消息处理。

// if we are in camera control we don't send any events

bool bInCameraControl = (ContextActor->GetCurrentInteractionMode() != EToolActorInteractionMode::NoInteraction);

if (bInCameraControl) {

ensure(bPendingMouseStateChange == false);

ensure(ToolsContext->InputRouter->HasActiveMouseCapture() == false); //ToolsContext->InputRouter->PostHoverInputEvent(InputState);

} else if (bPendingMouseStateChange || ToolsContext->InputRouter->HasActiveMouseCapture())

{ ToolsContext->InputRouter->PostInputEvent(InputState); } else { ToolsContext->InputRouter->PostHoverInputEvent(InputState); }

broken image

4)小部件(Gizmos)

           “Gizmo”指的是在 2D 和 3D 场景中创建/编辑几何对象时所用到的辅助图形化对象,用户通过操作Gizmo可以按照特定限制(将变化限制到特定方向、维度和局部)来改变选中并关联到Gizmo的几何对象,最厂家你的Gizmo就是标准的平移/旋转/缩放 Gizmo,如下图所示:

​​

broken image

​ TRS Giszmo 

        在 Interactive Tools Framework 中,所有Gizmos 都是 UInteractiveGizmo 的子类,它与 UInteractiveTool 有一定的相似性,Gizmo 实例都由 UInteractiveGizmoBuilder 工厂创建,并由 UInteractiveGizmoManager 管理,Gizmos 和UInteractiveTool一样通过添加关联特定UInputBehavior 来接受各类输入事件,在事件中再同步修改关联的几何对象。 

UCLASS()class UInteractiveGizmo : public UObject, public IInputBehaviorSource{    

void Setup();   

 void Shutdown();   

 void Render(IToolsContextRenderAPI* RenderAPI);    

void Tick(float DeltaTime);    void AddInputBehavior(UInputBehavior* Behavior);    const UInputBehaviorSet* GetInputBehaviors(); }

broken image

        上图中的 TRS Gizmo 实际上是由一组不同功能的部件组成,包括了多个 UAxisPositionGizmo用于控制位置,多个UAxisAngleGizmo用于控制旋转方向,我们也可以通过GizmoManager根据需要来创建、管理UTransformGizmo。

/** * UInteractiveGizmoManager allows users of the Tools framework to create and operate Gizmo instances. * For each Gizmo, a (string,GizmoBuilder) pair is registered with the GizmoManager. * Gizmos can then be activated via the string identifier. *  */

UCLASS(Transient) class INTERACTIVETOOLSFRAMEWORK_API UInteractiveGizmoManager : public UObject, public IToolContextTransactionProvider {    ... public: virtual void RegisterGizmoType(const FString& BuilderIdentifier, UInteractiveGizmoBuilder* Builder); virtual bool DeregisterGizmoType(const FString& BuilderIdentifier); virtual UInteractiveGizmo* CreateGizmo(const FString& BuilderIdentifier, const FString& InstanceIdentifier = FString(), void* Owner = nullptr); template GizmoType* CreateGizmo(const FString& BuilderIdentifier, const FString& InstanceIdentifier = FString(), void* Owner = nullptr) { UInteractiveGizmo* NewGizmo = CreateGizmo(BuilderIdentifier, InstanceIdentifier, Owner); return CastChecked(NewGizmo); } virtual bool DestroyGizmo(UInteractiveGizmo* Gizmo); virtual void DestroyAllGizmosOfType(const FString& BuilderIdentifier); virtual void DestroyAllGizmosByOwner(void* Owner);        ... }

broken image
broken image

        Gizmo最主要的职责就是辅助对几何对象进行编辑,这需要通过构建转换代理(UTransformPoxy)来实现。用户在场景中通过鼠标点击或触控选中对象后,触发相应的事件来创建GizmoTargets并激活TransformGizmo。为了将几何对象的变化与Gizmo进行关联,首先必须创建TransformProxy,创建后立即要将当前需要编辑修改的对象作为组件添加到TransformProxy上,并在当前TransformGizmo中将TransformProxy设置为ActiveTarget。这样操作后,一旦用户操作Gizmo,相应的修改就会被转换为对当前几何对象的位置、方向、形状的修改。

TransformProxy = NewObject(this);for (URuntimeMeshSceneObject* SceneObject : SelectedObjects){    TransformProxy->AddComponent(SO->GetMeshComponent());}TransformGizmo = GizmoManager->CreateCustomTransformGizmo(ETransformGizmoSubElements::TranslateRotateUniformScale, this); TransformGizmo->SetActiveTarget(TransformProxy);

broken image

5)对象管理(SceneObjectsManager)

        用户需要实现自己的场景对象管理和存储逻辑,新加对象需要挂载到特定的Actor才能在三维场景中被加载和显示。需要实现FindNearestHitObject() ,采用类似于 LineTrace方式来拾取编辑对象,并实现可编辑网格提的管理和绘制。 

        在对象编辑工具中提供了FPrimitiveComponentTarget类用于统一抽象几何对象的编辑逻辑,主要原因是在 Unreal 中有许多不同的网格类型,包括FMeshDescription(由UStaticMesh使用)、USkeletalMesh、FRawMesh、Cloth Meshes、Geometry Collections等等,要对这些网格分别进行编辑逻辑的编码非常麻烦,同时可能面临渲染性能的挑战(当您修改 UStaticMesh 内的 FMeshDescription 时,需要一个“构建”步骤来重新生成渲染数据,这在大型网格上可能需要几秒钟),因此,建模模式工具不能直接编辑各类网格格式,而是在ToolBuilder中包装一个 FPrimitiveComponentTarget 子类,有该类提供一个 API 来读取和写入它的内部网格生成 FMeshDescription,然后将该 FMeshDescription 转换为 FDynamicMesh3 进行实际编辑,并创建一个新的 USimpleDynamicMeshComponent 以进行快速预览,当 Tool Accept 时写回更新的 FMeshDescription。 

二、总体工作流程 

1)初始化交互工具框架系统(ITF),初始化框架上下文,包括初始化;IToolsContextQueriesAPI、IToolsContextTransactionsAPI、IToolsContextAssetAPI接口 

2)初始化对象存储管理系统,并与ITF建立连接; 

3)将各类ToolBulder注册到ToolManager,等待用户调用,此时各类工具还会配置号需要的InputBehaviors; 

4)ToolManager 调用 Tool Setup()进行相关工具的安装; 

6)用户展开各类输入操作,Tool在其Tick中处理输入,并根据操作进行渲染(Render); 

7)用户通过鼠标操作完成编辑或通过按钮、热键结束本次编辑,触发ToolManager中特定工具的Shutdown(),或者取消本次编辑,则恢复之前的编辑对象,或者接受编辑,则需要将新对象添加到存储系统中或更新存储系统中的几何对象。 

三、编辑工具绘制 

        编辑工具绘制包括三类绘制,即预览网格对象绘制、编辑工具绘制和Gizmos绘制。 

        预览网格对象在初始化的时会生成一个APreviewMeshActor,在ToolManager时会调用特定工 

具的Render(),调用特定工具的UpdateLivePreview()。 

void UPreviewMesh::CreateInWorld(UWorld* World, const FTransform& WithTransform){

FRotator Rotation(0.0f, 0.0f, 0.0f);

FActorSpawnParameters SpawnInfo; TemporaryParentActor = World->SpawnActor(FVector::ZeroVector, Rotation, SpawnInfo); DynamicMeshComponent = NewObject(TemporaryParentActor);

TemporaryParentActor->SetRootComponent(DynamicMeshComponent);

//DynamicMeshComponent->SetupAttachment(TemporaryParentActor->GetRootComponent());

DynamicMeshComponent->RegisterComponent(); TemporaryParentActor->SetActorTransform(WithTransform); //Builder.NewMeshComponent->SetWorldTransform(PlaneFrame.ToFTransform()); } void UPreviewMesh::UpdatePreview(const FDynamicMesh3* Mesh) { DynamicMeshComponent->SetDrawOnTop(this->bDrawOnTop); DynamicMeshComponent->GetMesh()->Copy(*Mesh); DynamicMeshComponent->NotifyMeshUpdated(); if (bBuildSpatialDataStructure) { MeshAABBTree.SetMesh(DynamicMeshComponent->GetMesh(), true); } }

broken image

         编辑工具对象绘制需要通过IToolsContextRenderAPI接口或的FPrimitiveDrawInterface(PDI),在通过PDI进行辅助线、控制点的绘制。

// Draw the polygon contour preview //

if (PolygonVertices.Num() > 0) {

FColor UseColor = bIsClosed ? ClosedPolygonColor : bHaveSelfIntersection ? ErrorColor : OpenPolygonColor; FColor LastSegmentColor = bIsClosed ? ClosedPolygonColor : bHaveSelfIntersection ? ErrorColor : PreviewColor; FVector3d UseLastVertex = bIsClosed ? PolygonVertices[0] : PreviewVertex; float UseThickness = bHaveSelfIntersection ? SelfIntersectThickness : LineThickness; auto DrawVertices = [&PDI, &UseColor](const TArray& Vertices, ESceneDepthPriorityGroup Group, float Thickness) { for (int lasti = Vertices.Num() - 1, i = 0, NumVertices = Vertices.Num(); i < NumVertices; lasti = i++) { PDI->DrawLine((FVector)Vertices[lasti], (FVector)Vertices[i], UseColor, Group, Thickness, 0.0f, true); } }; // draw thin no-depth (x-ray draw) //DrawVertices(PolygonVertices, SDPG_Foreground, HiddenLineThickness); for (int i = 0; i < NumVerts - 1; ++i) { PDI->DrawLine((FVector)PolygonVertices[i], (FVector)PolygonVertices[i + 1], UseColor, SDPG_Foreground, HiddenLineThickness, 0.0f, true); } PDI->DrawLine((FVector)PolygonVertices[NumVerts - 1], (FVector)UseLastVertex, LastSegmentColor, SDPG_Foreground, HiddenLineThickness, 0.0f, true); for (int HoleIdx = 0; HoleIdx < PolygonHolesVertices.Num(); HoleIdx++) { DrawVertices(PolygonHolesVertices[HoleIdx], SDPG_Foreground, HiddenLineThickness); } // draw thick depth-tested //DrawVertices(PolygonVertices, SDPG_World, LineThickness); for (int i = 0; i < NumVerts - 1; ++i) { PDI->DrawLine((FVector)PolygonVertices[i], (FVector)PolygonVertices[i + 1], UseColor, SDPG_World, LineThickness, 0.0f, true); } PDI->DrawLine((FVector)PolygonVertices[NumVerts - 1], (FVector)UseLastVertex, LastSegmentColor, SDPG_World, LineThickness, 0.0f, true); for (int HoleIdx = 0; HoleIdx < PolygonHolesVertices.Num(); HoleIdx++) { DrawVertices(PolygonHolesVertices[HoleIdx], SDPG_World, LineThickness); } // Intersection point if (bHaveSelfIntersection && !bInInteractiveExtrude) { PDI->DrawPoint((FVector)SelfIntersectionPoint, SnapHighlightColor, 12*PDIScale, SDPG_Foreground); } }

broken image

         Gizmos绘制有相应的Gizmo类实现,由GizmoManager->Render(&RenderAPI)进行调用。

void UInteractiveGizmoManager::Render(IToolsContextRenderAPI* RenderAPI){

    for (FActiveGizmo& ActiveGizmo : ActiveGizmos) {       

           ActiveGizmo.Gizmo->Render(RenderAPI); } }

broken image

 ​