Files

243 lines
9.0 KiB
C++

// 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<float>(ScreenWidth));
MinScreenY = FMath::Clamp(MinScreenY, 0.0f, static_cast<float>(ScreenHeight));
MaxScreenX = FMath::Clamp(MaxScreenX, 0.0f, static_cast<float>(ScreenWidth));
MaxScreenY = FMath::Clamp(MaxScreenY, 0.0f, static_cast<float>(ScreenHeight));
const float ScreenArea = static_cast<float>(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);
}