Files
2025-05-18 13:04:45 +08:00

231 lines
7.7 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "BaseGizmos/GizmoElementArc.h"
#include "BaseGizmos/GizmoRenderingUtil.h"
#include "BaseGizmos/GizmoMath.h"
#include "DynamicMeshBuilder.h"
#include "InputState.h"
#include "Materials/MaterialInterface.h"
#include "PrimitiveDrawingUtils.h"
#include "VectorUtil.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GizmoElementArc)
static void DrawThickArc(FPrimitiveDrawInterface* PDI, const FVector& InCenter, const FVector& InAxis0, const FVector& InAxis1,
double InOuterRadius, double InInnerRadius, const double InStartAngle, const double InEndAngle, const int32 InNumSegments,
const FMaterialRenderProxy* MaterialRenderProxy, const FColor& InColor)
{
// Implementation copied from FWidget::DrawThickArc. This should eventually be moved to PrimDrawingUtils.h/cpp.
if (InColor.A == 0)
{
return;
}
const int32 NumPoints = FMath::TruncToInt32(InNumSegments * (InEndAngle - InStartAngle) / (UE_DOUBLE_PI / 2.0)) + 1;
FColor TriangleColor = InColor;
FColor RingColor = InColor;
RingColor.A = MAX_uint8;
FVector ZAxis = InAxis0 ^ InAxis1;
FVector LastWorldVertex;
FDynamicMeshBuilder MeshBuilder(PDI->View->GetFeatureLevel());
for (int32 RadiusIndex = 0; RadiusIndex < 2; ++RadiusIndex)
{
double Radius = (RadiusIndex == 0) ? InOuterRadius : InInnerRadius;
double TCRadius = Radius / (double)InOuterRadius;
//Compute vertices for base circle.
for (int32 VertexIndex = 0; VertexIndex <= NumPoints; VertexIndex++)
{
double Percent = VertexIndex / (double)NumPoints;
double Angle = FMath::Lerp(InStartAngle, InEndAngle, Percent);
double AngleDeg = FRotator::ClampAxis(Angle * 180.f / PI);
FVector VertexDir = InAxis0.RotateAngleAxis(AngleDeg, ZAxis);
VertexDir.Normalize();
double TCAngle = Percent * (PI / 2);
FVector2f TC(static_cast<float>(TCRadius * FMath::Cos(Angle)), static_cast<float>(TCRadius * FMath::Sin(Angle)));
// Keep the vertices in local space so that we don't lose precision when dealing with LWC
// The local-to-world transform is handled in the MeshBuilder.Draw() call at the end of this function
const FVector VertexPosition = VertexDir * Radius;
FVector Normal = VertexPosition;
Normal.Normalize();
FDynamicMeshVertex MeshVertex;
MeshVertex.Position = (FVector3f)VertexPosition;
MeshVertex.Color = TriangleColor;
MeshVertex.TextureCoordinate[0] = TC;
MeshVertex.SetTangents(
(FVector3f)-ZAxis,
FVector3f((-ZAxis) ^ Normal),
(FVector3f)Normal
);
MeshBuilder.AddVertex(MeshVertex); //Add bottom vertex
// Push out the arc line borders so they dont z-fight with the mesh arcs
// DrawLine needs vertices in world space, but this is fine because it takes FVectors and works with LWC well
FVector EndLinePos = VertexPosition + InCenter;
if (VertexIndex != 0)
{
PDI->DrawLine(LastWorldVertex, EndLinePos, RingColor, SDPG_Foreground);
}
LastWorldVertex = EndLinePos;
}
}
//Add top/bottom triangles, in the style of a fan.
int32 InnerVertexStartIndex = NumPoints + 1;
for (int32 VertexIndex = 0; VertexIndex < NumPoints; VertexIndex++)
{
MeshBuilder.AddTriangle(VertexIndex, VertexIndex + 1, InnerVertexStartIndex + VertexIndex);
MeshBuilder.AddTriangle(VertexIndex + 1, InnerVertexStartIndex + VertexIndex + 1, InnerVertexStartIndex + VertexIndex);
}
MeshBuilder.Draw(PDI, FTranslationMatrix(InCenter), MaterialRenderProxy, SDPG_Foreground, 0.f);
}
void UGizmoElementArc::Render(IToolsContextRenderAPI* RenderAPI, const FRenderTraversalState& RenderState)
{
FRenderTraversalState CurrentRenderState(RenderState);
bool bVisibleViewDependent = UpdateRenderState(RenderAPI, Center, CurrentRenderState);
if (!bVisibleViewDependent)
{
return;
}
const UMaterialInterface* UseMaterial = CurrentRenderState.GetCurrentMaterial();
if (!UseMaterial)
{
return;
}
const FVector WorldCenter = CurrentRenderState.LocalToWorldTransform.TransformPosition(FVector::ZeroVector);
const FVector WorldAxis0 = CurrentRenderState.LocalToWorldTransform.TransformVectorNoScale(Axis0);
const FVector WorldAxis1 = CurrentRenderState.LocalToWorldTransform.TransformVectorNoScale(Axis1);
const FVector WorldNormal = WorldAxis0 ^ WorldAxis1;
bool bPartial = IsPartial(RenderAPI->GetSceneView(), WorldCenter, WorldNormal);
double Start, End;
if (bPartial)
{
if (PartialEndAngle - PartialStartAngle <= 0)
{
return;
}
Start = PartialStartAngle;
End = PartialEndAngle;
}
else
{
Start = 0.0;
End = UE_DOUBLE_TWO_PI;
}
const double WorldOuterRadius = Radius * CurrentRenderState.LocalToWorldTransform.GetScale3D().X;
const double WorldInnerRadius = InnerRadius * CurrentRenderState.LocalToWorldTransform.GetScale3D().X;
const FColor VertexColor = CurrentRenderState.GetCurrentVertexColor().ToFColor(false);
FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface();
DrawThickArc(PDI, WorldCenter, WorldAxis0, WorldAxis1, WorldOuterRadius, WorldInnerRadius,
Start, End, NumSegments, UseMaterial->GetRenderProxy(), VertexColor);
}
FInputRayHit UGizmoElementArc::LineTrace(const UGizmoViewContext* ViewContext, const FLineTraceTraversalState& LineTraceState, const FVector& RayOrigin, const FVector& RayDirection)
{
FLineTraceTraversalState CurrentLineTraceState(LineTraceState);
bool bHittableViewDependent = UpdateLineTraceState(ViewContext, Center, CurrentLineTraceState);
if (bHittableViewDependent)
{
const double PixelHitThresholdAdjust = CurrentLineTraceState.PixelToWorldScale * PixelHitDistanceThreshold;
const FVector WorldCenter = CurrentLineTraceState.LocalToWorldTransform.TransformPosition(FVector::ZeroVector);
const FVector WorldAxis0 = CurrentLineTraceState.LocalToWorldTransform.TransformVectorNoScale(Axis0);
const FVector WorldAxis1 = CurrentLineTraceState.LocalToWorldTransform.TransformVectorNoScale(Axis1);
const FVector WorldNormal = WorldAxis0 ^ WorldAxis1;
double HitDepth = -1.0;
bool bPartial = IsPartial(ViewContext, WorldCenter, WorldNormal);
const double PartialAngle = PartialEndAngle - PartialStartAngle;
if (bPartial && PartialAngle <= 0.0)
{
return FInputRayHit();
}
// Intersect ray with plane in which arc lies.
FPlane Plane(WorldCenter, WorldNormal);
HitDepth = FMath::RayPlaneIntersectionParam(RayOrigin, RayDirection, Plane);
if (HitDepth < 0.0)
{
return FInputRayHit();
}
FVector HitPoint = RayOrigin + RayDirection * HitDepth;
FVector HitVec = HitPoint - WorldCenter;
// Determine whether hit point lies within the arc
const double WorldOuterRadius = Radius * CurrentLineTraceState.LocalToWorldTransform.GetScale3D().X;
const double WorldInnerRadius = InnerRadius * CurrentLineTraceState.LocalToWorldTransform.GetScale3D().X;
const double Distance = HitVec.Length();
const double HitBufferMax = WorldOuterRadius + PixelHitThresholdAdjust;
const double HitBufferMin = WorldInnerRadius - PixelHitThresholdAdjust;
if (Distance > HitBufferMax || Distance < HitBufferMin)
{
return FInputRayHit();
}
// Handle partial arc
if (bPartial)
{
// Compute projected angle
FVector WorldBeginAxis = WorldAxis0.RotateAngleAxis(PartialStartAngle, WorldNormal).GetSafeNormal();
FVector HitVecNormal = HitVec.GetSafeNormal();
double HitAngle = UE::Geometry::VectorUtil::PlaneAngleSignedR(WorldBeginAxis, HitVec, WorldNormal);
if (HitAngle < 0)
{
HitAngle = UE_DOUBLE_TWO_PI + HitAngle;
}
if (HitAngle > PartialAngle)
{
return FInputRayHit();
}
}
if (HitDepth >= 0.0)
{
FInputRayHit RayHit(static_cast<float>(HitDepth));
RayHit.SetHitObject(this);
RayHit.HitIdentifier = PartIdentifier;
return RayHit;
}
}
return FInputRayHit();
}
void UGizmoElementArc::SetInnerRadius(double InInnerRadius)
{
InnerRadius = InInnerRadius;
}
double UGizmoElementArc::GetInnerRadius() const
{
return InnerRadius;
}