243 lines
9.0 KiB
C++
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);
|
|
}
|