Files
UnrealEngine/Engine/Source/Editor/ClothPainter/Private/ClothPainter.cpp
2025-05-18 13:04:45 +08:00

617 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ClothPainter.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "ClothLODData.h"
#include "ClothMeshAdapter.h"
#include "ClothPaintSettings.h"
#include "ClothPaintToolBase.h"
#include "ClothPaintTools.h"
#include "ClothingAsset.h"
#include "ClothingAssetBase.h"
#include "CollisionQueryParams.h"
#include "ComponentReregisterContext.h"
#include "Delegates/Delegate.h"
#include "EditorViewportClient.h"
#include "Engine/EngineTypes.h"
#include "Engine/NetSerialization.h"
#include "Engine/SkeletalMesh.h"
#include "EngineDefines.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UICommandList.h"
#include "HAL/PlatformCrt.h"
#include "IMeshPaintGeometryAdapter.h"
#include "Internationalization/Internationalization.h"
#include "Math/Color.h"
#include "Math/Matrix.h"
#include "Math/NumericLimits.h"
#include "Math/Transform.h"
#include "Math/Vector.h"
#include "MeshPaintHelpers.h"
#include "MeshPaintSettings.h"
#include "Misc/Guid.h"
#include "PointWeightMap.h"
#include "SClothPaintWidget.h"
#include "Stats/Stats.h"
#include "Templates/Casts.h"
#include "Templates/Tuple.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
#include "UnrealClient.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
class FPrimitiveDrawInterface;
class FSceneView;
class UMeshComponent;
#define LOCTEXT_NAMESPACE "ClothPainter"
namespace ClothPaintConstants
{
const float HoverQueryRadius = 50.0f;
}
FClothPainter::FClothPainter()
: IMeshPainter()
, SkeletalMeshComponent(nullptr)
, PaintSettings(nullptr)
, BrushSettings(nullptr)
{
VertexPointColor = FLinearColor::White;
WidgetLineThickness = .0f;
bShouldSimulate = false;
bShowHiddenVerts = false;
}
FClothPainter::~FClothPainter()
{
SkeletalMeshComponent->SetMeshSectionVisibilityForCloth(SkeletalMeshComponent->SelectedClothingGuidForPainting, true);
// Cancel rendering of the paint proxy
SkeletalMeshComponent->SelectedClothingGuidForPainting = FGuid();
}
void FClothPainter::Init()
{
// Brush settings share their values by getting/setting per-property values in the config via Constructor/PostEditChange.
BrushSettings = NewObject<UPaintBrushSettings>(GetTransientPackage());
BrushSettings->AddToRoot();
BrushSettings->bOnlyFrontFacingTriangles = false;
PaintSettings = GetMutableDefault<UClothPainterSettings>();
PaintSettings->LoadConfig();
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);
}
bool FClothPainter::PaintInternal(const FVector& InCameraOrigin, const TArrayView<TPair<FVector, FVector>>& Rays, EMeshPaintAction PaintAction, float PaintStrength)
{
bool bApplied = false;
if(SkeletalMeshComponent->SelectedClothingGuidForPainting.IsValid() && !bShouldSimulate)
{
USkeletalMesh* SkelMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
for (const TPair<FVector, FVector>& Ray : Rays)
{
const FVector& InRayOrigin = Ray.Key;
const FVector& InRayDirection = Ray.Value;
const FHitResult& HitResult = GetHitResult(InRayOrigin, InRayDirection);
if (HitResult.bBlockingHit)
{
// Generic per-vertex painting operations
if (!IsPainting())
{
BeginTransaction(LOCTEXT("MeshPaint", "Painting Cloth Property Values"));
bArePainting = true;
Adapter->PreEdit();
}
const FMeshPaintParameters Parameters = CreatePaintParameters(HitResult, InCameraOrigin, InRayOrigin, InRayDirection, PaintStrength);
FPerVertexPaintActionArgs Args;
Args.Adapter = Adapter.Get();
Args.CameraPosition = InCameraOrigin;
Args.HitResult = HitResult;
Args.BrushSettings = GetBrushSettings();
Args.Action = PaintAction;
if (SelectedTool->IsPerVertex())
{
bApplied |= MeshPaintHelpers::ApplyPerVertexPaintAction(Args, GetPaintAction(Parameters));
}
else
{
bApplied = true;
GetPaintAction(Parameters).ExecuteIfBound(Args, INDEX_NONE);
}
}
}
}
return bApplied;
}
FPerVertexPaintAction FClothPainter::GetPaintAction(const FMeshPaintParameters& InPaintParams)
{
if(SelectedTool.IsValid())
{
return SelectedTool->GetPaintAction(InPaintParams, PaintSettings);
}
return FPerVertexPaintAction();
}
void FClothPainter::SetTool(TSharedPtr<FClothPaintToolBase> InTool)
{
if(InTool.IsValid() && Tools.Contains(InTool))
{
if(SelectedTool.IsValid())
{
SelectedTool->Deactivate(CommandList);
}
SelectedTool = InTool;
SelectedTool->Activate(CommandList);
}
}
void FClothPainter::SetSkeletalMeshComponent(UDebugSkelMeshComponent* InSkeletalMeshComponent)
{
TSharedPtr<FClothMeshPaintAdapter> Result = MakeShareable(new FClothMeshPaintAdapter());
Result->Construct(InSkeletalMeshComponent, 0);
Adapter = Result;
SkeletalMeshComponent = InSkeletalMeshComponent;
RefreshClothingAssets();
if (Widget.IsValid())
{
Widget->OnRefresh();
}
}
USkeletalMesh* FClothPainter::GetSkeletalMesh() const
{
if(SkeletalMeshComponent)
{
return SkeletalMeshComponent->GetSkeletalMeshAsset();
}
return nullptr;
}
void FClothPainter::RefreshClothingAssets()
{
if(!PaintSettings || !SkeletalMeshComponent)
{
return;
}
PaintSettings->ClothingAssets.Reset();
if(USkeletalMesh* Mesh = SkeletalMeshComponent->GetSkeletalMeshAsset())
{
for(UClothingAssetBase* BaseClothingAsset : Mesh->GetMeshClothingAssets())
{
if(UClothingAssetCommon* ActualAsset = Cast<UClothingAssetCommon>(BaseClothingAsset))
{
PaintSettings->ClothingAssets.AddUnique(ActualAsset);
}
}
}
}
void FClothPainter::EnterPaintMode()
{
Reset();
if(SkeletalMeshComponent)
{
HoveredTextCallbackHandle = SkeletalMeshComponent->RegisterExtendedViewportTextDelegate(FGetExtendedViewportText::CreateSP(this, &FClothPainter::GetViewportText));
}
}
void FClothPainter::ExitPaintMode()
{
if(SkeletalMeshComponent)
{
SkeletalMeshComponent->UnregisterExtendedViewportTextDelegate(HoveredTextCallbackHandle);
}
// Remove reference to asset so it can be GC if necessary
if (PaintSettings)
{
PaintSettings->ClothingAssets.Reset();
PaintSettings->SaveConfig();
}
}
void FClothPainter::RecalculateAutoViewRange()
{
if(!Adapter.IsValid())
{
return;
}
TSharedPtr<FClothMeshPaintAdapter> ClothAdapter = StaticCastSharedPtr<FClothMeshPaintAdapter>(Adapter);
FPointWeightMap* CurrentMask = ClothAdapter->GetCurrentMask();
if(UClothPainterSettings* PainterSettings = Cast<UClothPainterSettings>(GetPainterSettings()))
{
if(PainterSettings->bAutoViewRange && CurrentMask)
{
float MinValue = MAX_flt;
float MaxValue = -MinValue;
CurrentMask->CalcRanges(MinValue, MaxValue);
PainterSettings->AutoCalculatedViewMin = MinValue;
PainterSettings->AutoCalculatedViewMax = MaxValue;
}
else
{
PainterSettings->AutoCalculatedViewMin = 0.0f;
PainterSettings->AutoCalculatedViewMax = 0.0f;
}
}
}
void FClothPainter::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
{
IMeshPainter::Tick(ViewportClient, DeltaTime);
SkeletalMeshComponent->MinClothPropertyView = PaintSettings->GetViewMin();
SkeletalMeshComponent->MaxClothPropertyView = PaintSettings->GetViewMax();
if(SelectedTool.IsValid() && PaintSettings->bAutoViewRange)
{
if(SelectedTool->HasValueRange())
{
float ToolMinRange = SkeletalMeshComponent->MinClothPropertyView;
float ToolMaxRange = SkeletalMeshComponent->MaxClothPropertyView;
SelectedTool->GetValueRange(ToolMinRange, ToolMaxRange);
SkeletalMeshComponent->MinClothPropertyView = FMath::Min(SkeletalMeshComponent->MinClothPropertyView, ToolMinRange);
SkeletalMeshComponent->MaxClothPropertyView = FMath::Max(SkeletalMeshComponent->MaxClothPropertyView, ToolMaxRange);
}
}
SkeletalMeshComponent->bClothFlipNormal = PaintSettings->bFlipNormal;
SkeletalMeshComponent->bClothCullBackface = PaintSettings->bCullBackface;
SkeletalMeshComponent->ClothMeshOpacity = PaintSettings->Opacity;
if ((bShouldSimulate && SkeletalMeshComponent->bDisableClothSimulation) || (!bShouldSimulate && !SkeletalMeshComponent->bDisableClothSimulation))
{
if(bShouldSimulate)
{
// Need to re-apply our masks here, as they have likely been edited
for(UClothingAssetCommon* Asset : PaintSettings->ClothingAssets)
{
if(Asset)
{
constexpr bool bUpdateFixedVertData = true; // There's Currently no way of telling whether the MaxDistance mask has been edited
constexpr bool bInvalidateDerivedDataCache = false; // No need to rebuild the DDC while previewing
Asset->ApplyParameterMasks(bUpdateFixedVertData, bInvalidateDerivedDataCache);
}
}
}
FComponentReregisterContext ReregisterContext(SkeletalMeshComponent);
SkeletalMeshComponent->bDisableClothSimulation = !bShouldSimulate;
SkeletalMeshComponent->bShowClothData = !bShouldSimulate;
SkeletalMeshComponent->SetMeshSectionVisibilityForCloth(SkeletalMeshComponent->SelectedClothingGuidForPainting, bShouldSimulate);
ViewportClient->Invalidate();
}
// We always want up to date CPU skinned verts, so each tick we reinitialize the adapter
if(Adapter.IsValid())
{
Adapter->Initialize();
}
}
void FClothPainter::FinishPainting()
{
if (IsPainting())
{
EndTransaction();
Adapter->PostEdit();
/** If necessary, recalculate view ranges when set to auto mode */
RecalculateAutoViewRange();
}
bArePainting = false;
}
void FClothPainter::Reset()
{
if(Widget.IsValid())
{
Widget->Reset();
}
bArePainting = false;
SkeletalMeshComponent->SetMeshSectionVisibilityForCloth(SkeletalMeshComponent->SelectedClothingGuidForPainting, true);
SkeletalMeshComponent->SelectedClothingGuidForPainting = FGuid();
bShouldSimulate = false;
}
TSharedPtr<IMeshPaintGeometryAdapter> FClothPainter::GetMeshAdapterForComponent(const UMeshComponent* Component)
{
if (Component == SkeletalMeshComponent)
{
return Adapter;
}
return nullptr;
}
void FClothPainter::AddReferencedObjects(FReferenceCollector& Collector)
{
Collector.AddReferencedObject(SkeletalMeshComponent);
Collector.AddReferencedObject(BrushSettings);
Collector.AddReferencedObject(PaintSettings);
}
UPaintBrushSettings* FClothPainter::GetBrushSettings()
{
return BrushSettings;
}
UMeshPaintSettings* FClothPainter::GetPainterSettings()
{
return PaintSettings;
}
TSharedPtr<class SWidget> FClothPainter::GetWidget()
{
return Widget;
}
const FHitResult FClothPainter::GetHitResult(const FVector& Origin, const FVector& Direction)
{
FHitResult HitResult(1.0f);
const FVector TraceStart(Origin);
const FVector TraceEnd(Origin + Direction * HALF_WORLD_MAX);
if (Adapter.IsValid())
{
Adapter->LineTraceComponent(HitResult, TraceStart, TraceEnd, FCollisionQueryParams(SCENE_QUERY_STAT(FClothPainter_GetHitResult), true));
}
return HitResult;
}
void FClothPainter::Refresh()
{
RefreshClothingAssets();
if(Widget.IsValid())
{
Widget->OnRefresh();
}
}
void FClothPainter::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
if(SelectedTool.IsValid() && SelectedTool->ShouldRenderInteractors() && !bShouldSimulate)
{
RenderInteractors(View, Viewport, PDI, true, SDPG_Foreground);
}
if(Adapter.IsValid() && SkeletalMeshComponent)
{
TSharedPtr<FClothMeshPaintAdapter> ClothAdapter = StaticCastSharedPtr<FClothMeshPaintAdapter>(Adapter);
TArray<MeshPaintHelpers::FPaintRay> PaintRays;
MeshPaintHelpers::RetrieveViewportPaintRays(View, Viewport, PDI, PaintRays);
bool bFoundValue = false;
float Value = 0.0f;
for(const MeshPaintHelpers::FPaintRay& PaintRay : PaintRays)
{
const FHitResult& HitResult = GetHitResult(PaintRay.RayStart, PaintRay.RayDirection);
if(HitResult.Component == SkeletalMeshComponent.Get())
{
const FMatrix ComponentToWorldMatrix = SkeletalMeshComponent->GetComponentTransform().ToMatrixWithScale();
const FVector ComponentSpaceCameraPosition(ComponentToWorldMatrix.InverseTransformPosition(PaintRay.CameraLocation));
const FVector ComponentSpaceBrushPosition(ComponentToWorldMatrix.InverseTransformPosition(HitResult.Location));
const float ComponentSpaceBrushRadius = ClothPaintConstants::HoverQueryRadius;
const float ComponentSpaceSquaredBrushRadius = ComponentSpaceBrushRadius * ComponentSpaceBrushRadius;
TArray<TPair<int32, FVector>> VertexData;
ClothAdapter->GetInfluencedVertexData(ComponentSpaceSquaredBrushRadius, ComponentSpaceBrushPosition, ComponentSpaceCameraPosition, BrushSettings->bOnlyFrontFacingTriangles, VertexData);
FPointWeightMap* CurrentMask = ClothAdapter->GetCurrentMask();
if(CurrentMask && VertexData.Num() > 0)
{
int32 ClosestIndex = INDEX_NONE;
float ClosestDistanceSq = MAX_flt;
int32 NumVertsFound = VertexData.Num();
for(int32 CurrIndex = 0; CurrIndex < NumVertsFound; ++CurrIndex)
{
const float DistSq = (VertexData[CurrIndex].Value - ComponentSpaceBrushPosition).SizeSquared();
if(DistSq < ClosestDistanceSq)
{
ClosestDistanceSq = DistSq;
ClosestIndex = CurrIndex;
}
}
if(ClosestIndex != INDEX_NONE)
{
TPair<int32, FVector>& Nearest = VertexData[ClosestIndex];
bFoundValue = true;
Value = CurrentMask->GetValue(Nearest.Key);
break;
}
}
}
}
if(PaintRays.Num() > 0)
{
FText BaseText = LOCTEXT("ClothPaintViewportValueText", "Cloth Value: {0}");
if(bFoundValue)
{
FNumberFormattingOptions Options;
Options.MinimumFractionalDigits = 3;
Options.MaximumFractionalDigits = 3;
CachedHoveredClothValueText = FText::Format(BaseText, FText::AsNumber(Value, &Options));
}
else
{
CachedHoveredClothValueText = FText::Format(BaseText, LOCTEXT("ClothPaintViewportNotApplicable", "N/A"));
}
}
}
const ESceneDepthPriorityGroup DepthPriority = bShowHiddenVerts ? SDPG_Foreground : SDPG_World;
// Render simulation mesh vertices if not simulating
if(SkeletalMeshComponent)
{
if(!bShouldSimulate)
{
if(SelectedTool.IsValid())
{
SelectedTool->Render(SkeletalMeshComponent, Adapter.Get(), View, Viewport, PDI);
}
}
}
bShouldSimulate = Viewport->KeyState(EKeys::H);
bShowHiddenVerts = Viewport->KeyState(EKeys::J);
}
bool FClothPainter::InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent)
{
bool bHandled = IMeshPainter::InputKey(InViewportClient, InViewport, InKey, InEvent);
if(SelectedTool.IsValid())
{
if(CommandList->ProcessCommandBindings(InKey, FSlateApplication::Get().GetModifierKeys(), InEvent == IE_Repeat))
{
bHandled = true;
}
else
{
// Handle non-action based key actions (holds etc.)
bHandled |= SelectedTool->InputKey(Adapter.Get(), InViewportClient, InViewport, InKey, InEvent);
}
}
return bHandled;
}
FMeshPaintParameters FClothPainter::CreatePaintParameters(const FHitResult& HitResult, const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, float PaintStrength)
{
const float BrushStrength = BrushSettings->BrushStrength * BrushSettings->BrushStrength * PaintStrength;
const float BrushRadius = BrushSettings->GetBrushRadius();
const float BrushDepth = BrushRadius * .5f;
FVector BrushXAxis, BrushYAxis;
HitResult.Normal.FindBestAxisVectors(BrushXAxis, BrushYAxis);
// Display settings
const float VisualBiasDistance = 0.15f;
const FVector BrushVisualPosition = HitResult.Location + HitResult.Normal * VisualBiasDistance;
FMeshPaintParameters Params;
{
Params.BrushPosition = HitResult.Location;
Params.SquaredBrushRadius = BrushRadius * BrushRadius;
Params.BrushRadialFalloffRange = BrushSettings->BrushFalloffAmount * BrushRadius;
Params.InnerBrushRadius = BrushRadius - Params.BrushRadialFalloffRange;
Params.BrushDepth = BrushDepth;
Params.BrushDepthFalloffRange = BrushSettings->BrushFalloffAmount * BrushDepth;
Params.InnerBrushDepth = BrushDepth - Params.BrushDepthFalloffRange;
Params.BrushStrength = BrushStrength;
Params.BrushNormal = HitResult.Normal;
Params.BrushToWorldMatrix = FMatrix(BrushXAxis, BrushYAxis, Params.BrushNormal, Params.BrushPosition);
Params.InverseBrushToWorldMatrix = Params.BrushToWorldMatrix.InverseFast();
}
return Params;
}
float FClothPainter::GetPropertyValue(int32 VertexIndex)
{
FClothMeshPaintAdapter* ClothAdapter = (FClothMeshPaintAdapter*)Adapter.Get();
if(FPointWeightMap* Mask = ClothAdapter->GetCurrentMask())
{
return Mask->GetValue(VertexIndex);
}
return 0.0f;
}
void FClothPainter::SetPropertyValue(int32 VertexIndex, const float Value)
{
FClothMeshPaintAdapter* ClothAdapter = (FClothMeshPaintAdapter*)Adapter.Get();
if(FPointWeightMap* Mask = ClothAdapter->GetCurrentMask())
{
Mask->SetValue(VertexIndex, Value);
}
}
void FClothPainter::OnAssetSelectionChanged(UClothingAssetCommon* InNewSelectedAsset, int32 InAssetLod, int32 InMaskIndex)
{
TSharedPtr<FClothMeshPaintAdapter> ClothAdapter = StaticCastSharedPtr<FClothMeshPaintAdapter>(Adapter);
if(ClothAdapter.IsValid() && InNewSelectedAsset && InNewSelectedAsset->IsValidLod(InAssetLod))
{
// Validate the incoming parameters, to make sure we only set a selection if we're going
// to get a valid paintable surface
if(InNewSelectedAsset->LodData.IsValidIndex(InAssetLod) &&
InNewSelectedAsset->LodData[InAssetLod].PointWeightMaps.IsValidIndex(InMaskIndex))
{
const FGuid NewGuid = InNewSelectedAsset->GetAssetGuid();
SkeletalMeshComponent->SetMeshSectionVisibilityForCloth(SkeletalMeshComponent->SelectedClothingGuidForPainting, true);
SkeletalMeshComponent->SetMeshSectionVisibilityForCloth(NewGuid, false);
SkeletalMeshComponent->bDisableClothSimulation = true;
SkeletalMeshComponent->bShowClothData = true;
SkeletalMeshComponent->SelectedClothingGuidForPainting = NewGuid;
SkeletalMeshComponent->SelectedClothingLodForPainting = InAssetLod;
SkeletalMeshComponent->SelectedClothingLodMaskForPainting = InMaskIndex;
SkeletalMeshComponent->RefreshSelectedClothingSkinnedPositions();
ClothAdapter->SetSelectedClothingAsset(NewGuid, InAssetLod, InMaskIndex);
for(TSharedPtr<FClothPaintToolBase> Tool : Tools)
{
Tool->OnMeshChanged();
}
}
}
}
#undef LOCTEXT_NAMESPACE // "ClothPainter"