Files
UnrealEngine/Engine/Plugins/Experimental/MeshModelingToolsetExp/Source/MeshModelingToolsEditorOnlyExp/Private/MeshTangentsTool.cpp
2025-05-18 13:04:45 +08:00

557 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MeshTangentsTool.h"
#include "InteractiveToolManager.h"
#include "ToolBuilderUtil.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMeshToMeshDescription.h"
#include "MeshDescription.h"
#include "ToolSetupUtil.h"
#include "ToolDataVisualizer.h"
#include "AssetUtils/MeshDescriptionUtil.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "TargetInterfaces/MaterialProvider.h"
#include "TargetInterfaces/MeshDescriptionCommitter.h"
#include "TargetInterfaces/MeshDescriptionProvider.h"
#include "TargetInterfaces/PrimitiveComponentBackedTarget.h"
#include "TargetInterfaces/StaticMeshBackedTarget.h"
#include "ModelingToolTargetUtil.h"
#include "ToolTargetManager.h"
#include "PropertySets/GeometrySelectionVisualizationProperties.h"
#include "GroupTopology.h"
#include "DynamicMeshEditor.h"
#include "Selection/GeometrySelectionVisualization.h"
#include "Selection/StoredMeshSelectionUtil.h"
#include "Selections/GeometrySelectionUtil.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MeshTangentsTool)
using namespace UE::Geometry;
#define LOCTEXT_NAMESPACE "UMeshTangentsTool"
/*
* ToolBuilder
*/
const FToolTargetTypeRequirements& UMeshTangentsToolBuilder::GetTargetRequirements() const
{
static FToolTargetTypeRequirements TypeRequirements({
UMaterialProvider::StaticClass(),
UMeshDescriptionCommitter::StaticClass(),
UMeshDescriptionProvider::StaticClass(),
UPrimitiveComponentBackedTarget::StaticClass(),
});
return TypeRequirements;
}
USingleSelectionMeshEditingTool* UMeshTangentsToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const
{
return NewObject<UMeshTangentsTool>(SceneState.ToolManager);
}
void UMeshTangentsToolBuilder::InitializeNewTool(USingleSelectionMeshEditingTool* NewTool, const FToolBuilderState& SceneState) const
{
UToolTarget* Target = SceneState.TargetManager->BuildFirstSelectedTargetable(SceneState, GetTargetRequirements());
check(Target);
NewTool->SetTarget(Target);
NewTool->SetWorld(SceneState.World);
if (UMeshTangentsTool* TangentsTool = Cast<UMeshTangentsTool>(NewTool))
{
UE::Geometry::FGeometrySelection Selection;
if (UE::Geometry::GetCurrentGeometrySelectionForTarget(SceneState, Target, Selection))
{
TangentsTool->SetGeometrySelection(MoveTemp(Selection));
}
}
}
bool UMeshTangentsToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const
{
return USingleSelectionMeshEditingToolBuilder::CanBuildTool(SceneState) &&
SceneState.TargetManager->CountSelectedAndTargetableWithPredicate(SceneState, GetTargetRequirements(),
[](UActorComponent& Component) { return !ToolBuilderUtil::IsVolume(Component); }) >= 1;
}
/*
* Tool
*/
UMeshTangentsTool::UMeshTangentsTool()
{
}
void UMeshTangentsTool::Setup()
{
UInteractiveTool::Setup();
UE::ToolTarget::HideSourceObject(Target);
// make our preview mesh
AActor* TargetActor = UE::ToolTarget::GetTargetActor(Target);
PreviewMesh = NewObject<UPreviewMesh>(this);
PreviewMesh->bBuildSpatialDataStructure = false;
PreviewMesh->CreateInWorld(TargetActor->GetWorld(), FTransform::Identity);
PreviewMesh->SetTransform((FTransform)UE::ToolTarget::GetLocalToWorldTransform(Target));
ToolSetupUtil::ApplyRenderingConfigurationToPreview(PreviewMesh, nullptr);
// configure materials
FComponentMaterialSet MaterialSet = UE::ToolTarget::GetMaterialSet(Target);
PreviewMesh->SetMaterials(MaterialSet.Materials);
// configure mesh
PreviewMesh->SetTangentsMode(EDynamicMeshComponentTangentsMode::ExternallyProvided);
static FGetMeshParameters GetMeshParams;
GetMeshParams.bWantMeshTangents = true;
FDynamicMesh3 InputMeshWithTangents = UE::ToolTarget::GetDynamicMeshCopy(Target, GetMeshParams);
PreviewMesh->ReplaceMesh(MoveTemp(InputMeshWithTangents));
// make a copy of initialized mesh and tangents
PreviewMesh->ProcessMesh([&](const FDynamicMesh3& ReadMesh)
{
InputMesh = MakeShared<FDynamicMesh3, ESPMode::ThreadSafe>(ReadMesh);
InitialTangents = MakeShared<FMeshTangentsf, ESPMode::ThreadSafe>(InputMesh.Get());
InitialTangents->CopyTriVertexTangents(ReadMesh);
});
// initialize our properties
Settings = NewObject<UMeshTangentsToolProperties>(this);
Settings->RestoreProperties(this);
AddToolPropertySource(Settings);
Settings->WatchProperty(Settings->CalculationMethod, [this](EMeshTangentsType) { Compute->InvalidateResult(); });
Settings->WatchProperty(Settings->LineLength, [this](float) { bLengthDirty = true; });
Settings->WatchProperty(Settings->LineThickness, [this](float) { bThicknessDirty = true; });
Settings->WatchProperty(Settings->bShowTangents, [this](bool) { bVisibilityChanged = true; });
Settings->WatchProperty(Settings->bShowNormals, [this](bool) { bVisibilityChanged = true; });
Settings->WatchProperty(Settings->bCompareWithMikkt, [this](bool) { ComputeMikkTDeviations(ComputeDegenerateTris()); });
if (InputGeometrySelection.IsEmpty() == false)
{
GeometrySelectionVizProperties = NewObject<UGeometrySelectionVisualizationProperties>(this);
GeometrySelectionVizProperties->RestoreProperties(this);
AddToolPropertySource(GeometrySelectionVizProperties);
GeometrySelectionVizProperties->Initialize(this);
GeometrySelectionVizProperties->SelectionElementType = static_cast<EGeometrySelectionElementType>(InputGeometrySelection.ElementType);
GeometrySelectionVizProperties->SelectionTopologyType = static_cast<EGeometrySelectionTopologyType>(InputGeometrySelection.TopologyType);
GeometrySelectionVizProperties->bEnableShowEdgeSelectionVertices = true;
// TODO Enable this but note we need to compute a ROI which only includes triangles incident to the
// polygroup feature eg do not include all triangles in the groups incident to a polygroup edge
//GeometrySelectionVizProperties->bEnableShowTriangleROIBorder = true;
// Compute group topology if the selection has Polygroup topology, and do nothing otherwise
// Currently it is only possible to make a polygroup geometry selection using polygroup set stored directly in the mesh
FGroupTopology GroupTopology(InputMesh.Get(), InputGeometrySelection.TopologyType == EGeometryTopologyType::Polygroup);
// Compute the overlay selection and a proxy triangle vertex selection used to make edge selections behave like
// vertex selections. See :EdgeSelectionsBehaveLikeVertexSelections
if (InputGeometrySelection.TopologyType == EGeometryTopologyType::Polygroup)
{
ConvertPolygroupSelectionToIncidentOverlaySelection(
*InputMesh,
GroupTopology,
InputGeometrySelection,
EditTriangles,
EditVertices,
&TriangleVertexGeometrySelection);
}
else
{
ConvertTriangleSelectionToOverlaySelection(
*InputMesh,
InputGeometrySelection,
EditTriangles,
EditVertices,
&TriangleVertexGeometrySelection);
}
// Setup input geometry selection visualization
FTransform ApplyTransform = UE::ToolTarget::GetLocalToWorldTransform(Target);
GeometrySelectionViz = NewObject<UPreviewGeometry>(this);
GeometrySelectionViz->CreateInWorld(GetTargetWorld(), ApplyTransform);
InitializeGeometrySelectionVisualization(
GeometrySelectionViz,
GeometrySelectionVizProperties,
*InputMesh,
InputGeometrySelection,
&GroupTopology,
!TriangleVertexGeometrySelection.IsEmpty() ? &TriangleVertexGeometrySelection : nullptr);
}
PreviewGeometry = NewObject<UPreviewGeometry>(this);
PreviewGeometry->CreateInWorld(TargetActor->GetWorld(), PreviewMesh->GetTransform());
Compute = MakeUnique<TGenericDataBackgroundCompute<FMeshTangentsd>>();
Compute->Setup(this);
Compute->OnOpCompleted.AddLambda([this](const UE::Geometry::TGenericDataOperator<UE::Geometry::FMeshTangentsd>* Op)
{
if (((FCalculateTangentsOp*)(Op))->bNoAttributesError)
{
bHasDisplayedNoAttributeError = true;
GetToolManager()->DisplayMessage(
LOCTEXT("TangentsNoAttributesError", "Error: Source mesh did not have tangents."),
EToolMessageLevel::UserWarning);
}
else if (bHasDisplayedNoAttributeError)
{
bHasDisplayedNoAttributeError = false;
GetToolManager()->DisplayMessage(
FText(),
EToolMessageLevel::UserWarning);
}
});
Compute->OnResultUpdated.AddLambda( [this](const TUniquePtr<FMeshTangentsd>& NewResult) { OnTangentsUpdated(NewResult); } );
Compute->InvalidateResult();
SetToolDisplayName(LOCTEXT("ToolName", "Edit Tangents"));
GetToolManager()->DisplayMessage(
LOCTEXT("OnStartTool", "Configure or Recalculate Tangents on a Static Mesh Asset (disables autogenerated Tangents and Normals)"),
EToolMessageLevel::UserNotification);
}
void UMeshTangentsTool::SetGeometrySelection(UE::Geometry::FGeometrySelection&& SelectionIn)
{
InputGeometrySelection = MoveTemp(SelectionIn);
}
void UMeshTangentsTool::OnShutdown(EToolShutdownType ShutdownType)
{
PreviewGeometry->Disconnect();
PreviewMesh->Disconnect();
Settings->SaveProperties(this);
if (GeometrySelectionViz)
{
GeometrySelectionViz->Disconnect();
}
if (GeometrySelectionVizProperties)
{
GeometrySelectionVizProperties->SaveProperties(this);
}
// Restore (unhide) the source meshes
UE::ToolTarget::ShowSourceObject(Target);
TUniquePtr<FMeshTangentsd> Tangents = Compute->Shutdown();
if (ShutdownType == EToolShutdownType::Accept)
{
GetToolManager()->BeginUndoTransaction(LOCTEXT("UpdateTangents", "Update Tangents"));
UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(UE::ToolTarget::GetTargetComponent(Target));
if (StaticMeshComponent != nullptr)
{
UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh();
if (ensure(StaticMesh != nullptr))
{
StaticMesh->Modify();
// disable auto-generated normals and tangents build settings
UE::MeshDescription::FStaticMeshBuildSettingChange SettingsChange;
SettingsChange.AutoGeneratedNormals = UE::MeshDescription::EBuildSettingBoolChange::Disable;
SettingsChange.AutoGeneratedTangents = UE::MeshDescription::EBuildSettingBoolChange::Disable;
UE::MeshDescription::ConfigureBuildSettings(StaticMesh, 0, SettingsChange);
}
}
CopyToOverlays(*Tangents, *InputMesh);
FConversionToMeshDescriptionOptions Options;
Options.bUpdatePositions = false;
Options.bUpdateNormals = true;
Options.bUpdateTangents = true;
UE::ToolTarget::CommitDynamicMeshUpdate(Target, *InputMesh, false, Options);
GetToolManager()->EndUndoTransaction();
}
}
void UMeshTangentsTool::OnTick(float DeltaTime)
{
Compute->Tick(DeltaTime);
if (bThicknessDirty || bLengthDirty || bVisibilityChanged)
{
UpdateVisualization(bThicknessDirty, bLengthDirty);
bThicknessDirty = bLengthDirty = bVisibilityChanged = false;
}
if (GeometrySelectionViz)
{
UpdateGeometrySelectionVisualization(GeometrySelectionViz, GeometrySelectionVizProperties);
}
}
void UMeshTangentsTool::Render(IToolsContextRenderAPI* RenderAPI)
{
if (Settings->bCompareWithMikkt && Deviations.Num() > 0)
{
FToolDataVisualizer Visualizer;
Visualizer.BeginFrame(RenderAPI);
Visualizer.SetTransform(PreviewMesh->GetTransform());
for (const FMikktDeviation& ErrorPt : Deviations)
{
if (ErrorPt.MaxAngleDeg > Settings->CompareWithMikktThreshold)
{
Visualizer.DrawPoint(ErrorPt.VertexPos, FLinearColor(0.95f, 0.05f, 0.05f), 4.0f * Settings->LineThickness, false);
Visualizer.DrawLine<FVector3f>(ErrorPt.VertexPos, ErrorPt.VertexPos + Settings->LineLength * ErrorPt.MikktTangent, FLinearColor(0.95f, 0.05f, 0.05f), 2.0f * Settings->LineThickness, false);
Visualizer.DrawLine<FVector3f>(ErrorPt.VertexPos, ErrorPt.VertexPos + Settings->LineLength * ErrorPt.MikktBitangent, FLinearColor(0.05f, 0.95f, 0.05f), 2.0f * Settings->LineThickness, false);
Visualizer.DrawLine<FVector3f>(ErrorPt.VertexPos, ErrorPt.VertexPos + (1.1f * Settings->LineLength) * ErrorPt.OtherTangent, FLinearColor(0.95f, 0.50f, 0.05f), Settings->LineThickness, false);
Visualizer.DrawLine<FVector3f>(ErrorPt.VertexPos, ErrorPt.VertexPos + (1.1f * Settings->LineLength) * ErrorPt.OtherBitangent, FLinearColor(0.05f, 0.95f, 0.95f), Settings->LineThickness, false);
}
}
Visualizer.EndFrame();
}
}
bool UMeshTangentsTool::CanAccept() const
{
return Super::CanAccept() && Compute->HaveValidResult();
}
TUniquePtr<TGenericDataOperator<FMeshTangentsd>> UMeshTangentsTool::MakeNewOperator()
{
TUniquePtr<FCalculateTangentsOp> TangentsOp = MakeUnique<FCalculateTangentsOp>();
TangentsOp->SourceMesh = InputMesh;
TangentsOp->SourceTangents = InitialTangents;
TangentsOp->CalculationMethod = Settings->CalculationMethod;
return TangentsOp;
}
void UMeshTangentsTool::UpdateVisualization(bool bThicknessChanged, bool bLengthChanged)
{
ULineSetComponent* TangentLines = PreviewGeometry->FindLineSet(TEXT("Tangents"));
ULineSetComponent* NormalLines = PreviewGeometry->FindLineSet(TEXT("Normals"));
if (TangentLines == nullptr || NormalLines == nullptr)
{
return;
}
if (bThicknessChanged)
{
float Thickness = Settings->LineThickness;
TangentLines->SetAllLinesThickness(Thickness);
NormalLines->SetAllLinesThickness(Thickness);
}
if (bLengthChanged)
{
float LineLength = Settings->LineLength;
TangentLines->SetAllLinesLength(LineLength);
NormalLines->SetAllLinesLength(LineLength);
}
PreviewGeometry->SetLineSetVisibility(TEXT("Tangents"), Settings->bShowTangents);
PreviewGeometry->SetLineSetVisibility(TEXT("Normals"), Settings->bShowNormals);
}
void UMeshTangentsTool::OnTangentsUpdated(const TUniquePtr<FMeshTangentsd>& NewResult)
{
const float LineLength = Settings->LineLength;
const float Thickness = Settings->LineThickness;
const TSet<int32> DegenerateTris = ComputeDegenerateTris();
// update Tangents rendering line set
PreviewGeometry->CreateOrUpdateLineSet(TEXT("Tangents"), InputMesh->MaxTriangleID(),
[&](int32 Index, TArray<FRenderableLine>& Lines)
{
bool bValid = InputMesh->IsTriangle(Index) && !DegenerateTris.Contains(Index);
bool bIncludedTriangle = EditTriangles.IsEmpty() || EditTriangles.Contains(Index);
if (bValid && bIncludedTriangle)
{
FIndex3i Vids = InputMesh->GetTriangle(Index);
for (int j = 0; j < 3; ++j)
{
bool bIncludedVertex = EditVertices.IsEmpty() || EditVertices.Contains(Vids[j]);
if (bIncludedVertex)
{
FVector3d Origin = InputMesh->GetVertex(Vids[j]);
FVector3d Tangent, Bitangent;
NewResult->GetPerTriangleTangent(Index, j, Tangent, Bitangent);
Lines.Add(FRenderableLine((FVector)Origin, (FVector)Origin + LineLength * (FVector)Tangent, FColor(240,15,15), Thickness));
Lines.Add(FRenderableLine((FVector)Origin, (FVector)Origin + LineLength * (FVector)Bitangent, FColor(15,240,15), Thickness));
}
}
}
}, 6);
// update Normals rendering line set
const FDynamicMeshNormalOverlay* NormalOverlay = InputMesh->Attributes()->PrimaryNormals();
PreviewGeometry->CreateOrUpdateLineSet(TEXT("Normals"), NormalOverlay->MaxElementID(),
[&](int32 Index, TArray<FRenderableLine>& Lines)
{
if (NormalOverlay->IsElement(Index))
{
int32 ParentVtx = NormalOverlay->GetParentVertex(Index);
bool bIncluded = InputGeometrySelection.IsEmpty() || EditVertices.Contains(ParentVtx);
if (bIncluded)
{
FVector3f Normal = NormalOverlay->GetElement(Index);
FVector3f Origin = (FVector3f)InputMesh->GetVertex(ParentVtx);
Lines.Add(FRenderableLine((FVector)Origin, (FVector)Origin + LineLength * (FVector)Normal, FColor(15,15,240), Thickness));
}
}
}, 1);
ComputeMikkTDeviations(DegenerateTris);
PreviewMesh->DeferredEditMesh([&](FDynamicMesh3& EditMesh)
{
CopyToOverlays(*NewResult, EditMesh);
}, false);
PreviewMesh->NotifyDeferredEditCompleted(UPreviewMesh::ERenderUpdateMode::FastUpdate, EMeshRenderAttributeFlags::VertexNormals, false);
UpdateVisualization(false, false);
}
void UMeshTangentsTool::CopyToOverlays(const FMeshTangentsd& Tangents, FDynamicMesh3& Mesh)
{
if (ensure(Mesh.HasAttributes()) == false || ensure(Mesh.Attributes()->NumNormalLayers() == 3) == false)
{
return;
}
if (InputGeometrySelection.IsEmpty())
{
Tangents.CopyToOverlays(Mesh);
}
else
{
FDynamicMesh3 MeshCopy;
{
bool bCopyNormals = false;
bool bCopyColors = false;
bool bCopyUVs = false;
bool bCopyAttributes = true;
MeshCopy.Copy(Mesh, bCopyNormals, bCopyColors, bCopyUVs, bCopyAttributes);
}
// Copy all tangents to the MeshCopy overlays
Tangents.CopyToOverlays(MeshCopy);
// Copy the subset of tangents corresponding to the geometry selection to the Mesh overlays
FDynamicMeshEditor Editor(&Mesh);
Editor.AppendElementSubset(
&MeshCopy,
EditTriangles,
EditVertices,
MeshCopy.Attributes()->PrimaryTangents(),
Mesh.Attributes()->PrimaryTangents());
Editor.AppendElementSubset(
&MeshCopy,
EditTriangles,
EditVertices,
MeshCopy.Attributes()->PrimaryBiTangents(),
Mesh.Attributes()->PrimaryBiTangents());
}
}
TSet<int32> UMeshTangentsTool::ComputeDegenerateTris() const
{
TSet<int32> DegenerateTris;
if (!Settings->bShowDegenerates || (Settings->bCompareWithMikkt && Settings->CalculationMethod != EMeshTangentsType::MikkTSpace))
{
FMeshTangentsd DegenTangents(InputMesh.Get());
DegenTangents.ComputeTriangleTangents(InputMesh->Attributes()->GetUVLayer(0));
DegenerateTris = TSet<int32>(DegenTangents.GetDegenerateTris());
}
return DegenerateTris;
}
void UMeshTangentsTool::ComputeMikkTDeviations(const TSet<int32>& DegenerateTris)
{
// calculate deviation between what we have and MikkT, if necessary
Deviations.Reset();
if (Settings->bCompareWithMikkt && Settings->CalculationMethod == EMeshTangentsType::FastMikkTSpace)
{
FProgressCancel TmpCancel;
FCalculateTangentsOp MikktOp;
MikktOp.SourceMesh = InputMesh;
MikktOp.CalculationMethod = EMeshTangentsType::MikkTSpace;
MikktOp.CalculateResult(&TmpCancel);
TUniquePtr<FMeshTangentsd> MikktTangents = MikktOp.ExtractResult();
FCalculateTangentsOp NewOp;
NewOp.SourceMesh = InputMesh;
NewOp.CalculationMethod = EMeshTangentsType::FastMikkTSpace;
NewOp.CalculateResult(&TmpCancel);
TUniquePtr<FMeshTangentsd> NewTangents = NewOp.ExtractResult();
for (int32 Index : InputMesh->TriangleIndicesItr())
{
bool bValid = !DegenerateTris.Contains(Index);
bool bIncludedTriangle = EditTriangles.IsEmpty() || EditTriangles.Contains(Index);
if (bValid && bIncludedTriangle)
{
FIndex3i Vids = InputMesh->GetTriangle(Index);
for (int j = 0; j < 3; ++j)
{
bool bIncludedVertex = EditVertices.IsEmpty() || EditVertices.Contains(Vids[j]);
if (bIncludedVertex)
{
FVector3f TangentMikkt, BitangentMikkt;
MikktTangents->GetPerTriangleTangent<FVector3f, float>(Index, j, TangentMikkt, BitangentMikkt);
UE::Geometry::Normalize(TangentMikkt);
UE::Geometry::Normalize(BitangentMikkt);
ensure(UE::Geometry::IsNormalized(TangentMikkt));
ensure(UE::Geometry::IsNormalized(BitangentMikkt));
FVector3f TangentNew, BitangentNew;
NewTangents->GetPerTriangleTangent<FVector3f, float>(Index, j, TangentNew, BitangentNew);
UE::Geometry::Normalize(TangentNew);
UE::Geometry::Normalize(BitangentNew);
ensure(UE::Geometry::IsNormalized(TangentNew));
ensure(UE::Geometry::IsNormalized(BitangentNew));
float TangentAngleDeg = UE::Geometry::AngleD(TangentMikkt, TangentNew);
float BiTangentAngleDeg = UE::Geometry::AngleD(BitangentMikkt, BitangentNew);
float MaxAngleDeg = FMathf::Max(TangentAngleDeg, BiTangentAngleDeg);
if (MaxAngleDeg > 0.5f)
{
FMikktDeviation Deviation;
Deviation.MaxAngleDeg = MaxAngleDeg;
Deviation.TriangleID = Index;
Deviation.TriVertIndex = j;
Deviation.VertexPos = (FVector3f)InputMesh->GetVertex(Vids[j]);
Deviation.MikktTangent = TangentMikkt;
Deviation.MikktBitangent = BitangentMikkt;
Deviation.OtherTangent = TangentNew;
Deviation.OtherBitangent = BitangentNew;
Deviations.Add(Deviation);
}
}
}
}
}
}
}
#undef LOCTEXT_NAMESPACE