// Copyright Epic Games, Inc. All Rights Reserved. #include "BaseGizmos/GizmoElementTorus.h" #include "GizmoPrivateUtil.h" #include "BaseGizmos/GizmoMath.h" #include "InputState.h" #include "Materials/MaterialInterface.h" #include "PrimitiveDrawingUtils.h" #include "VectorUtil.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GizmoElementTorus) void UGizmoElementTorus::Render(IToolsContextRenderAPI* RenderAPI, const FRenderTraversalState& RenderState) { FRenderTraversalState CurrentRenderState(RenderState); bool bVisibleViewDependent = UpdateRenderState(RenderAPI, Center, CurrentRenderState); if (bVisibleViewDependent) { if (const UMaterialInterface* UseMaterial = CurrentRenderState.GetCurrentMaterial()) { const FVector WorldCenter = CurrentRenderState.LocalToWorldTransform.GetLocation(); const FVector WorldAxis0 = CurrentRenderState.LocalToWorldTransform.TransformVectorNoScale(Axis0); const FVector Normal = Axis0 ^ Axis1; const FVector WorldNormal = CurrentRenderState.LocalToWorldTransform.TransformVectorNoScale(Normal); const bool bPartial = IsPartial(RenderAPI->GetSceneView(), WorldCenter, WorldNormal); FVector BeginAxis = bPartial ? Axis0.RotateAngleAxis(PartialStartAngle, Normal).GetSafeNormal() : Axis0; const float PartialAngle = static_cast(PartialEndAngle - PartialStartAngle); if (PartialAngle <= 0.0f) { return; } FPrimitiveDrawInterface* PDI = RenderAPI->GetPrimitiveDrawInterface(); FVector TorusSideAxis = Normal ^ BeginAxis; TorusSideAxis.Normalize(); DrawTorus(PDI, CurrentRenderState.LocalToWorldTransform.ToMatrixWithScale(), BeginAxis, TorusSideAxis, Radius, InnerRadius, NumSegments, NumInnerSlices, UseMaterial->GetRenderProxy(), SDPG_Foreground, bPartial, PartialAngle, bEndCaps); } } } // // This method approximates ray-torus intersection by intersecting the ray with the plane in which the torus // lies, then determining a hit point closest to the linear circle defined by torus center and torus outer radius. // // If the torus lies at a glancing angle, the ray-torus intersection is performed against cylinders approximating // the shape of the torus. // FInputRayHit UGizmoElementTorus::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 double WorldOuterRadius = Radius * CurrentLineTraceState.LocalToWorldTransform.GetScale3D().X; const double WorldInnerRadius = InnerRadius * CurrentLineTraceState.LocalToWorldTransform.GetScale3D().X; const FVector WorldCenter = CurrentLineTraceState.LocalToWorldTransform.GetLocation(); 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); FVector WorldBeginAxis; const double PartialAngle = PartialEndAngle - PartialStartAngle; if (bPartial) { if (PartialAngle <= 0) { return FInputRayHit(); } WorldBeginAxis = WorldAxis0.RotateAngleAxis(PartialStartAngle, WorldNormal).GetSafeNormal(); } else { WorldBeginAxis = WorldAxis0; } if (IsGlancingAngle(ViewContext, WorldCenter, WorldNormal)) { // If the torus lies at a glancing angle, the ray-torus intersection is performed against cylinders approximating // the shape of the torus. static constexpr int32 NumFullTorusCylinders = 16; static constexpr double AngleDelta = UE_DOUBLE_TWO_PI / static_cast(NumFullTorusCylinders); const int32 NumCylinders = bPartial ? FMath::CeilToInt32(NumFullTorusCylinders * PartialAngle / UE_DOUBLE_TWO_PI) : NumFullTorusCylinders; const FVector ViewDirection = ViewContext->GetViewDirection(); FVector VectorA = WorldBeginAxis; FVector VectorB = VectorA.RotateAngleAxisRad(AngleDelta, WorldNormal); const double CylinderRadius = WorldInnerRadius + PixelHitThresholdAdjust; double CylinderHeight = (VectorB - VectorA).Length() * WorldOuterRadius; if (FMath::IsNearlyZero(CylinderHeight)) { return FInputRayHit(); } // due to numerical imprecision, the ray origin needs to be clamped in ortho views // (cf. UEditorInteractiveToolsContext::GetRayFromMousePos) FVector ClampedRayOrigin(RayOrigin); const double DepthBias = UE::GizmoUtil::ClampRayOrigin(ViewContext, ClampedRayOrigin, RayDirection); bool bDidIntersect = false; // Line trace against a set of cylinders approximating the shape of the torus for (int32 i = 0; i < NumCylinders; i++) { if (i > 0) { VectorA = VectorB; VectorB = VectorA.RotateAngleAxisRad(AngleDelta, WorldNormal); } if (i == NumCylinders - 1) { CylinderHeight = bPartial ? FMath::Fmod(PartialAngle, AngleDelta) * CylinderHeight : CylinderHeight; } const FVector CylinderDirection = (VectorB - VectorA).GetSafeNormal(); const FVector CylinderCenter = WorldCenter + (VectorA * WorldOuterRadius) + CylinderDirection * CylinderHeight * 0.5; bool bIntersects = false; double RayParam = -1.0; GizmoMath::RayCylinderIntersection( CylinderCenter, CylinderDirection, CylinderRadius, CylinderHeight, ClampedRayOrigin, RayDirection, bIntersects, RayParam); bDidIntersect |= bIntersects; // Update with closest hit depth if (bIntersects && (HitDepth < 0 || HitDepth > RayParam)) { HitDepth = RayParam; } } if (bDidIntersect) { // add the depth bias if any HitDepth += DepthBias; } } else { // Intersect ray with plane in which torus lies. FPlane Plane(WorldCenter, WorldNormal); HitDepth = FMath::RayPlaneIntersectionParam(RayOrigin, RayDirection, Plane); if (HitDepth < 0.0) { return FInputRayHit(); } FVector HitPoint = RayOrigin + RayDirection * HitDepth; // Find the closest point on the circle to the intersection point FVector NearestCirclePos; GizmoMath::ClosetPointOnCircle(HitPoint, WorldCenter, WorldNormal, static_cast(WorldOuterRadius), NearestCirclePos); // Find the closest point on the ray to the circle and determine if it is within the torus FRay Ray(RayOrigin, RayDirection, true); FVector NearestRayPos = Ray.ClosestPoint(NearestCirclePos); const double HitBuffer = PixelHitThresholdAdjust + WorldInnerRadius; double Distance = FVector::Distance(NearestCirclePos, NearestRayPos); if (Distance > HitBuffer) { return FInputRayHit(); } // Handle partial torus if (bPartial) { // Compute projected angle FVector HitVec = (NearestCirclePos - WorldCenter).GetSafeNormal(); double HitAngle = UE::Geometry::VectorUtil::PlaneAngleSignedR(WorldBeginAxis, HitVec, WorldNormal); if (HitAngle < 0) { HitAngle = UE_DOUBLE_TWO_PI + HitAngle; } if (HitAngle > PartialAngle) { return FInputRayHit(); } } // Adjust hit depth to return the closest hit point's depth HitDepth = (NearestRayPos - RayOrigin).Length(); } if (HitDepth >= 0.0) { FInputRayHit RayHit(static_cast(HitDepth)); RayHit.SetHitObject(this); RayHit.HitIdentifier = PartIdentifier; return RayHit; } } return FInputRayHit(); } bool UGizmoElementTorus::IsGlancingAngle(const UGizmoViewContext* View, const FVector& InWorldCenter, const FVector& InWorldNormal) { check(View); FVector ViewToCircleBaseCenter; if (View->IsPerspectiveProjection()) { ViewToCircleBaseCenter = (InWorldCenter - View->ViewLocation).GetSafeNormal(); } else { ViewToCircleBaseCenter = View->GetViewDirection(); } // Determine if the ray direction is at a glancing angle by using a minimum cos angle based on the // angle between the vector from arc center to ring center and the vector from arc center to ring edge. double MinCosAngle = FMath::Max(InnerRadius * 1.5 / FMath::Sqrt(Radius * Radius + InnerRadius * InnerRadius), DefaultViewDependentPlanarMinCosAngleTol); double DotP = FMath::Abs(FVector::DotProduct(InWorldNormal, ViewToCircleBaseCenter)); return (DotP <= MinCosAngle); } void UGizmoElementTorus::SetInnerRadius(double InInnerRadius) { InnerRadius = InInnerRadius; } double UGizmoElementTorus::GetInnerRadius() const { return InnerRadius; } void UGizmoElementTorus::SetNumInnerSlices(int32 InNumInnerSlices) { NumInnerSlices = InNumInnerSlices; } int32 UGizmoElementTorus::GetNumInnerSlices() const { return NumInnerSlices; } void UGizmoElementTorus::SetEndCaps(bool InEndCaps) { bEndCaps = InEndCaps; } bool UGizmoElementTorus::GetEndCaps() const { return bEndCaps; }