// Copyright Ngonart OU. All Rights Reserved. // UE5 Modular Game Framework — PlanarCaptureCameraUtils implementation #include "Capture/PlanarCaptureCameraUtils.h" #include "Kismet/KismetMathLibrary.h" FTransform UPlanarCaptureCameraUtils::ComputeMirroredTransform( const FTransform& ViewerCameraTransform, const FTransform& MirrorPlaneTransform) { // Mirror plane normal (local Z axis of the mirror surface) const FVector MirrorNormal = MirrorPlaneTransform.GetUnitAxis(EAxis::Z); const FVector MirrorLocation = MirrorPlaneTransform.GetLocation(); // Reflect viewer position across the mirror plane const FVector ViewerPos = ViewerCameraTransform.GetLocation(); const FVector ToViewer = ViewerPos - MirrorLocation; const float DistanceToPlane = FVector::DotProduct(ToViewer, MirrorNormal); const FVector ReflectedPosition = ViewerPos - 2.0f * DistanceToPlane * MirrorNormal; // Reflect viewer rotation across the mirror plane const FVector ViewerForward = ViewerCameraTransform.GetUnitAxis(EAxis::X); const FVector ReflectedForward = ViewerForward - 2.0f * FVector::DotProduct(ViewerForward, MirrorNormal) * MirrorNormal; const FVector ViewerUp = ViewerCameraTransform.GetUnitAxis(EAxis::Z); const FVector ReflectedUp = ViewerUp - 2.0f * FVector::DotProduct(ViewerUp, MirrorNormal) * MirrorNormal; const FRotator ReflectedRotation = FRotationMatrix::MakeFromXZ(ReflectedForward, ReflectedUp).Rotator(); return FTransform(ReflectedRotation, ReflectedPosition, ViewerCameraTransform.GetScale3D()); } FTransform UPlanarCaptureCameraUtils::ComputePortalTransform( const FTransform& ViewerCameraTransform, const FTransform& SourceSurfaceTransform, const FTransform& TargetSurfaceTransform) { // Compute viewer position relative to source surface const FVector ViewerPos = ViewerCameraTransform.GetLocation(); const FVector RelativePos = SourceSurfaceTransform.InverseTransformPosition(ViewerPos); // Compute viewer rotation relative to source surface const FQuat ViewerRot = ViewerCameraTransform.GetRotation(); const FQuat SourceRotInv = SourceSurfaceTransform.GetRotation().Inverse(); const FQuat RelativeRot = SourceRotInv * ViewerRot; // Apply relative transform to target surface const FVector TargetPos = TargetSurfaceTransform.TransformPosition(RelativePos); const FQuat TargetRot = TargetSurfaceTransform.GetRotation() * RelativeRot; return FTransform(TargetRot, TargetPos, ViewerCameraTransform.GetScale3D()); } FMatrix UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix( float FOV, float AspectRatio, float NearPlane, float FarPlane, const FPlane& ClipPlane, const FTransform& SurfaceTransform) { // Build standard perspective projection matrix FMatrix ProjectionMatrix; const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f); const float YScale = 1.0f / FMath::Tan(HalfFOVRad); const float XScale = YScale / AspectRatio; ProjectionMatrix.SetIdentity(); ProjectionMatrix.M[0][0] = XScale; ProjectionMatrix.M[1][1] = YScale; ProjectionMatrix.M[2][2] = (NearPlane == FarPlane) ? 0.0f : FarPlane / (FarPlane - NearPlane); ProjectionMatrix.M[2][3] = 1.0f; ProjectionMatrix.M[3][2] = (NearPlane == FarPlane) ? NearPlane : -NearPlane * FarPlane / (FarPlane - NearPlane); ProjectionMatrix.M[3][3] = 0.0f; // Transform clip plane into view space (inverse of surface transform) const FMatrix ViewMatrix = SurfaceTransform.ToMatrixNoScale().Inverse(); const FPlane ViewSpaceClipPlane = ClipPlane.TransformBy(ViewMatrix); // Compute oblique near-plane using the standard technique: // Calculate the clip-space corner that maximizes the dot product with the plane normal. // Then modify the third row of the projection matrix to make the near plane pass through the clip plane. FVector4 ClipPlaneVec(ViewSpaceClipPlane.X, ViewSpaceClipPlane.Y, ViewSpaceClipPlane.Z, ViewSpaceClipPlane.W); // Find the vertex of the view frustum in clip space that is closest to the plane FVector4 Q; Q.X = (FMath::Sign(ClipPlaneVec.X) + ProjectionMatrix.M[0][2]) / ProjectionMatrix.M[0][0]; Q.Y = (FMath::Sign(ClipPlaneVec.Y) + ProjectionMatrix.M[1][2]) / ProjectionMatrix.M[1][1]; Q.Z = 1.0f; Q.W = (1.0f + ProjectionMatrix.M[2][2]) / ProjectionMatrix.M[3][2]; // Scale the clip plane so its distance from the origin matches the Q vertex const float DotVal = ClipPlaneVec.X * Q.X + ClipPlaneVec.Y * Q.Y + ClipPlaneVec.Z * Q.Z + ClipPlaneVec.W * Q.W; const float Scale = 2.0f / DotVal; ClipPlaneVec *= Scale; // Replace the third row of the projection matrix with the clip plane ProjectionMatrix.M[2][0] = ClipPlaneVec.X; ProjectionMatrix.M[2][1] = ClipPlaneVec.Y; ProjectionMatrix.M[2][2] = ClipPlaneVec.Z; ProjectionMatrix.M[2][3] = ClipPlaneVec.W; return ProjectionMatrix; } float UPlanarCaptureCameraUtils::ComputeScreenCoverage( const FBox& SurfaceBounds, const FTransform& ViewerTransform, float ViewerFOV, int32 ScreenWidth, int32 ScreenHeight) { if (!SurfaceBounds.IsValid || ScreenWidth <= 0 || ScreenHeight <= 0) { return 0.0f; } const float HalfFOVRad = FMath::DegreesToRadians(ViewerFOV * 0.5f); const FVector ViewerPos = ViewerTransform.GetLocation(); const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X); // Project all 8 corners of the bounding box to screen space const FVector Corners[8] = { FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z), FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z), FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z), FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z), FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z), FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z), FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z), FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z), }; float MinScreenX = FLT_MAX, MinScreenY = FLT_MAX; float MaxScreenX = -FLT_MAX, MaxScreenY = -FLT_MAX; int32 BehindCameraCount = 0; for (const FVector& Corner : Corners) { const FVector ToCorner = Corner - ViewerPos; const float DotForward = FVector::DotProduct(ToCorner, ViewerForward); if (DotForward <= 0.0f) { BehindCameraCount++; continue; } const float Distance = ToCorner.Size(); const FVector Right = ViewerTransform.GetUnitAxis(EAxis::Y); const FVector Up = ViewerTransform.GetUnitAxis(EAxis::Z); const float DotRight = FVector::DotProduct(ToCorner.GetSafeNormal(), Right); const float DotUp = FVector::DotProduct(ToCorner.GetSafeNormal(), Up); const float AngleX = FMath::Atan2(DotRight, DotForward); const float AngleY = FMath::Atan2(DotUp, DotForward); const float ScreenX = (AngleX / HalfFOVRad) * (ScreenWidth * 0.5f) + (ScreenWidth * 0.5f); const float ScreenY = (ScreenHeight * 0.5f) - (AngleY / HalfFOVRad) * (ScreenHeight * 0.5f); MinScreenX = FMath::Min(MinScreenX, ScreenX); MinScreenY = FMath::Min(MinScreenY, ScreenY); MaxScreenX = FMath::Max(MaxScreenX, ScreenX); MaxScreenY = FMath::Max(MaxScreenY, ScreenY); } if (BehindCameraCount >= 8) { return 0.0f; } // Clamp to screen bounds MinScreenX = FMath::Clamp(MinScreenX, 0.0f, static_cast(ScreenWidth)); MinScreenY = FMath::Clamp(MinScreenY, 0.0f, static_cast(ScreenHeight)); MaxScreenX = FMath::Clamp(MaxScreenX, 0.0f, static_cast(ScreenWidth)); MaxScreenY = FMath::Clamp(MaxScreenY, 0.0f, static_cast(ScreenHeight)); const float ScreenArea = static_cast(ScreenWidth * ScreenHeight); const float BoundingArea = (MaxScreenX - MinScreenX) * (MaxScreenY - MinScreenY); return FMath::Clamp(BoundingArea / ScreenArea, 0.0f, 1.0f); } bool UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer( const FBox& SurfaceBounds, const FTransform& ViewerTransform, float ViewerFOV, float ViewerAspectRatio, float ViewerNearPlane, float ViewerFarPlane) { if (!SurfaceBounds.IsValid) { return false; } // Quick dot-product check: is the surface center roughly in front of the viewer? const FVector SurfaceCenter = SurfaceBounds.GetCenter(); const FVector ViewerPos = ViewerTransform.GetLocation(); const FVector ToSurface = SurfaceCenter - ViewerPos; const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X); if (FVector::DotProduct(ToSurface.GetSafeNormal(), ViewerForward) < 0.0f) { return false; } // Distance check if (ToSurface.Size() > ViewerFarPlane) { return false; } if (ToSurface.Size() < ViewerNearPlane) { return false; } // Screen coverage check — if coverage is above 0, it's visible const float Coverage = ComputeScreenCoverage(SurfaceBounds, ViewerTransform, ViewerFOV, 1920, 1080); return Coverage > 0.001f; } float UPlanarCaptureCameraUtils::ComputeCompositeScore( float ScreenCoverage, float FacingAngle, float DistanceToViewer, float MaxDistance, float ScriptedPriority) { const float DistanceFactor = (MaxDistance > 0.0f) ? FMath::Clamp(1.0f - (DistanceToViewer / MaxDistance), 0.0f, 1.0f) : 1.0f; const float Score = (ScreenCoverage * 0.5f) + (FMath::Abs(FacingAngle) * 0.3f) + (DistanceFactor * 0.1f) + (ScriptedPriority * 0.1f); return FMath::Clamp(Score, 0.0f, 1.0f); }