Add Planar Capture System implementation checklist and developer reference
- Created a comprehensive implementation checklist for the Planar Capture System (Systems 136-147) detailing tasks across multiple phases including C++ core, material foundation, Blueprint actors, data assets, integration, and performance testing. - Added a developer reference document outlining the architecture, data flow, state machine, budget enforcement, render target pooling, horror features, integration points, multiplayer networking, performance characteristics, debugging methods, and build order for the capture systems. - Introduced examples of capture surface usage in the Project Void horror game, including specific implementations for mirrors, monitors, portals, and fake windows, along with a checklist for integration tasks.
This commit is contained in:
240
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal file
240
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright Ngonart OU. All Rights Reserved.
|
||||
// UE5 Modular Game Framework — PlanarCaptureCameraUtils implementation
|
||||
|
||||
#include "Capture/PlanarCaptureCameraUtils.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;
|
||||
|
||||
FRotator ReflectedRotation = UKismetMathLibrary::MakeRotFromXZ(ReflectedForward, ReflectedUp);
|
||||
|
||||
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 FTransform ViewTransform = SurfaceTransform.Inverse();
|
||||
const FPlane ViewSpaceClipPlane = ClipPlane.TransformBy(ViewTransform);
|
||||
|
||||
// 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 Scale = 2.0f / FVector4::DotProduct(ClipPlaneVec, Q);
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user