// This isn't necessary for now as the orthographic camera is always an infinite distance away from the scene.
//#define ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z
//#define ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
// If not defined, orthographic near/far fitting will use Mesh.Split.
// If defined, use the same method as perspective near/far fitting. Represent the AABB as solid shapes instead of faces.
//#define USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING
using MatterHackers.Agg;
using MatterHackers.DataConverters3D;
using MatterHackers.PolygonMesh;
using MatterHackers.VectorMath;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MatterHackers
{
// For "zoom to selection" and dynamic near/far.
internal static class CameraFittingUtil
{
///
/// This proportion, scaled by the smaller dimension of the viewport, is subtracted from each side of the viewport for fitting.
/// Exposed for testing.
///
public const double MarginScale = 0.1;
public enum EPerspectiveFittingAlgorithm
{
///
/// Has a margin, but does not use MarginScale.
///
TrialAndError,
///
/// Fit the camera to the AABB's bounding sphere.
/// Guarantees a centered AABB center, but will not center the screenspace AABB.
///
Sphere,
///
/// Place the camera at the center of the AABB, then push the camera back until all points are visible.
/// Guarantees a centered AABB center, but will not center the screenspace AABB.
///
CenterOnWorldspaceAABB,
///
/// Fit the camera to the viewspace AABB, which will tend to be larger than the worldspace AABB.
/// Guarantees a centered AABB center, but will not center the screenspace AABB.
///
CenterOnViewspaceAABB,
///
/// Take the perspective side planes and fit them around the AABB.
/// There will be two intersection lines and the camera will be placed on the one further back.
/// Either X or Y will be restrained to one value, and the other will take the value closest to the center of the AABB.
/// https://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene/66113254#66113254
///
IntersectionOfBoundingPlanesWithApproxCentering,
///
/// This is the only one that guarantees perfect screenspace centering.
/// A solver is used to center the screenspace AABB on the non-restrained axis.
/// But this means that the viewspace center will not always be at the center of the screen, so the object can sometimes look off-centered.
///
IntersectionOfBoundingPlanesWithPerfectCentering,
}
// Exposed for testing.
// "static readonly" to silence unreachable code warnings.
public static readonly EPerspectiveFittingAlgorithm PerspectiveFittingAlgorithm = EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB;
#if ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z
// Scaled by the box's Z size in viewspace.
// If camera Z translation is needed and far - near > threshold, the camera will be placed close to the object.
const double OrthographicLargeZRangeScaledThreshold = 3.0;
const double OrthographicLargeZRangeScaledDistanceBetweenNearAndObject = 1.0;
#endif
public struct FitResult
{
public Vector3 CameraPosition;
public double OrthographicViewspaceHeight;
}
public static FitResult ComputeOrthographicCameraFit(WorldView world, double centerOffsetX, double zNear, double zFar, AxisAlignedBoundingBox worldspaceAABB)
{
Vector3[] worldspacePoints = worldspaceAABB.GetCorners();
Vector3[] viewspacePoints = worldspacePoints.Select(x => x.TransformPosition(world.ModelviewMatrix)).ToArray();
Vector3 viewspaceCenter = worldspaceAABB.Center.TransformPosition(world.ModelviewMatrix);
AxisAlignedBoundingBox viewspaceAABB = new AxisAlignedBoundingBox(viewspaceCenter, viewspaceCenter);
foreach (Vector3 point in viewspacePoints)
{
viewspaceAABB.ExpandToInclude(point);
}
// Take the viewport with margins subtracted, then fit the viewspace AABB to it.
Vector2 viewportSize = new Vector2(world.Width + centerOffsetX, world.Height);
double baseDim = Math.Min(viewportSize.X, viewportSize.Y);
double absTotalMargin = baseDim * MarginScale * 2;
Vector2 reducedViewportSize = viewportSize - new Vector2(absTotalMargin, absTotalMargin);
double unitsPerPixelX = viewspaceAABB.XSize / reducedViewportSize.X;
double unitsPerPixelY = viewspaceAABB.YSize / reducedViewportSize.Y;
double unitsPerPixel = Math.Max(unitsPerPixelX, unitsPerPixelY);
Vector2 targetViewspaceSize = viewportSize * unitsPerPixel;
Vector3 viewspaceNearCenter = new Vector3(viewspaceAABB.Center.Xy, viewspaceAABB.MaxXYZ.Z);
Vector3 viewspaceCameraPosition = viewspaceNearCenter;
#if ENABLE_ORTHOGRAPHIC_CAMERA_POSITIONING_ALONG_Z
Vector3 viewspaceFarCenter = new Vector3(viewspaceAABB.Center.Xy, viewspaceAABB.MinXYZ.Z);
if (-viewspaceNearCenter.Z >= zNear && -viewspaceFarCenter.Z <= zFar)
{
// The object fits in the Z range without translating along Z.
viewspaceCameraPosition.Z = 0;
}
else if (viewspaceAABB.ZSize * OrthographicLargeZRangeScaledThreshold < zFar - zNear)
{
// There's lots of Z range.
// Place the camera close to the object such that there's a reasonable amount of Z space behind and in front of the object.
viewspaceCameraPosition.Z += zNear + viewspaceAABB.ZSize * OrthographicLargeZRangeScaledDistanceBetweenNearAndObject;
}
else if (viewspaceAABB.ZSize < zFar - zNear)
{
// There's not much Z range, but enough to contain the object.
// Place the camera such that the object is in the middle of the Z range.
viewspaceCameraPosition.Z = viewspaceAABB.Center.Z + (zFar - zNear) * 0.5;
}
else
{
// The object is too big to fit in the Z range.
// Place the camera at the near side of the object.
viewspaceCameraPosition.Z = viewspaceAABB.MaxXYZ.Z;
}
#endif
Vector3 worldspaceCameraPosition = viewspaceCameraPosition.TransformPosition(world.InverseModelviewMatrix);
return new FitResult { CameraPosition = worldspaceCameraPosition, OrthographicViewspaceHeight = targetViewspaceSize.Y };
}
public static FitResult ComputePerspectiveCameraFit(WorldView world, double centerOffsetX, AxisAlignedBoundingBox worldspaceAABB)
{
System.Diagnostics.Debug.Assert(!world.IsOrthographic);
Vector3[] worldspacePoints = worldspaceAABB.GetCorners();
Vector3[] viewspacePoints = worldspacePoints.Select(p => p.TransformPosition(world.ModelviewMatrix)).ToArray();
Vector3 viewspaceCenter = viewspacePoints.Aggregate((a, b) => a + b) / viewspacePoints.Length;
// Construct a temp WorldView with a smaller FOV to give the resulting view a margin.
Vector2 viewportSize = world.ViewportSize + new Vector2(centerOffsetX, 0);
double margin = MarginScale * Math.Min(viewportSize.X, viewportSize.Y);
WorldView reducedWorld = new WorldView(viewportSize.X - margin * 2, viewportSize.Y - margin * 2);
double reducedVFOVDegrees = WorldView.CalcPerspectiveVFOVDegreesFromDistanceAndHeight(world.NearZ, world.NearPlaneHeightInViewspace * (reducedWorld.Height / world.Height));
reducedWorld.CalculatePerspectiveMatrixOffCenter(
reducedWorld.Width, reducedWorld.Height,
0,
1, 2, // Arbitrary
reducedVFOVDegrees
);
Plane[] viewspacePlanes = Frustum.FrustumFromProjectionMatrix(reducedWorld.ProjectionMatrix).Planes.Take(4).ToArray();
Vector3 viewspaceCameraPosition;
switch (PerspectiveFittingAlgorithm)
{
case EPerspectiveFittingAlgorithm.TrialAndError:
return new FitResult { CameraPosition = TryPerspectiveCameraFitByIterativeAdjust(world, centerOffsetX, worldspaceAABB) };
case EPerspectiveFittingAlgorithm.Sphere:
default:
viewspaceCameraPosition = PerspectiveCameraFitToSphere(reducedWorld, viewspaceCenter, viewspacePoints);
break;
case EPerspectiveFittingAlgorithm.CenterOnWorldspaceAABB:
viewspaceCameraPosition = PerspectiveCameraFitAlongAxisThroughCenter(viewspacePlanes, viewspaceCenter, viewspacePoints);
break;
case EPerspectiveFittingAlgorithm.CenterOnViewspaceAABB:
viewspaceCameraPosition = PerspectiveCameraFitToViewspaceAABB(viewspacePlanes, viewspaceCenter, viewspacePoints);
break;
case EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithApproxCentering:
viewspaceCameraPosition = PerspectiveCameraFitByAxisAlignedPlaneIntersections(reducedWorld, viewspacePlanes, viewspaceCenter, viewspacePoints, false);
break;
case EPerspectiveFittingAlgorithm.IntersectionOfBoundingPlanesWithPerfectCentering:
viewspaceCameraPosition = PerspectiveCameraFitByAxisAlignedPlaneIntersections(reducedWorld, viewspacePlanes, viewspaceCenter, viewspacePoints, true);
break;
}
return new FitResult { CameraPosition = viewspaceCameraPosition.TransformPosition(world.InverseModelviewMatrix) };
}
static bool NeedsToBeSmaller(RectangleDouble partScreenBounds, RectangleDouble goalBounds)
{
if (partScreenBounds.Bottom < goalBounds.Bottom
|| partScreenBounds.Top > goalBounds.Top
|| partScreenBounds.Left < goalBounds.Left
|| partScreenBounds.Right > goalBounds.Right)
{
return true;
}
return false;
}
// Original code relocated from https://github.com/MatterHackers/MatterControl/blob/e5967ff858f2844734e4802a6c6c8ac973ad92d1/MatterControlLib/PartPreviewWindow/View3D/View3DWidget.cs
static Vector3 TryPerspectiveCameraFitByIterativeAdjust(WorldView world, double centerOffsetX, AxisAlignedBoundingBox worldspaceAABB)
{
var aabb = worldspaceAABB;
var center = aabb.Center;
// pan to the center
var screenCenter = new Vector2(world.Width / 2 + centerOffsetX / 2, world.Height / 2);
var centerRay = world.GetRayForLocalBounds(screenCenter);
// make the target size a portion of the total size
var goalBounds = new RectangleDouble(0, 0, world.Width, world.Height);
goalBounds.Inflate(-world.Width * .1);
int rescaleAttempts = 0;
var testWorld = new WorldView(world.Width, world.Height);
testWorld.RotationMatrix = world.RotationMatrix;
var distance = 80.0;
void AjustDistance()
{
testWorld.TranslationMatrix = world.TranslationMatrix;
var delta = centerRay.origin + centerRay.directionNormal * distance - center;
testWorld.Translate(delta);
}
AjustDistance();
while (rescaleAttempts++ < 500)
{
var partScreenBounds = testWorld.GetScreenBounds(aabb);
if (NeedsToBeSmaller(partScreenBounds, goalBounds))
{
distance++;
AjustDistance();
partScreenBounds = testWorld.GetScreenBounds(aabb);
// If it crossed over the goal reduct the amount we are adjusting by.
if (!NeedsToBeSmaller(partScreenBounds, goalBounds))
{
break;
}
}
else
{
distance--;
AjustDistance();
partScreenBounds = testWorld.GetScreenBounds(aabb);
// If it crossed over the goal reduct the amount we are adjusting by.
if (NeedsToBeSmaller(partScreenBounds, goalBounds))
{
break;
}
}
}
//TrackballTumbleWidget.AnimateTranslation(center, centerRay.origin + centerRay.directionNormal * distance);
// zoom to fill the view
// viewControls3D.NotifyResetView();
return world.EyePosition - ((centerRay.origin + centerRay.directionNormal * distance) - center);
}
static Vector3 PerspectiveCameraFitToSphere(WorldView world, Vector3 viewspaceCenter, Vector3[] viewspacePoints)
{
double radius = viewspacePoints.Select(p => (p - viewspaceCenter).Length).Max();
double distForBT = radius / Math.Sin(MathHelper.DegreesToRadians(world.VFovDegrees) / 2);
double distForLR = radius / Math.Sin(MathHelper.DegreesToRadians(world.HFovDegrees) / 2);
double distForN = radius + WorldView.PerspectiveProjectionMinimumNearZ;
double dist = Math.Max(Math.Max(distForBT, distForLR), distForN);
return viewspaceCenter + new Vector3(0, 0, dist);
}
static Vector3 PerspectiveCameraFitAlongAxisThroughCenter(Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints)
{
Vector3 viewspaceCameraPosition = viewspaceCenter;
viewspacePoints = viewspacePoints.Select(p => p - viewspaceCameraPosition).ToArray();
double relZ = double.NegativeInfinity;
foreach (Plane viewspacePlane in viewspacePlanes)
{
relZ = Math.Max(relZ, viewspacePoints.Select(
p => p.Z - (viewspacePlane.DistanceFromOrigin - viewspacePlane.Normal.Dot(new Vector3(p.X, p.Y, 0))) / viewspacePlane.Normal.Z
).Max());
}
return viewspaceCameraPosition + new Vector3(0, 0, relZ);
}
static Vector3 PerspectiveCameraFitToViewspaceAABB(Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints)
{
AxisAlignedBoundingBox aabb = new AxisAlignedBoundingBox(viewspacePoints);
return PerspectiveCameraFitAlongAxisThroughCenter(viewspacePlanes, aabb.Center, aabb.GetCorners());
}
struct Line
{
public double gradient;
public double refX;
public double YAt(double x) => gradient * (x - refX);
public Line NegatedY()
{
return new Line { gradient = -gradient, refX = refX };
}
}
static double GetIntersectX(Line a, Line b, double minX, double maxX)
{
double x = (a.gradient * a.refX - b.gradient * b.refX) / (a.gradient - b.gradient);
if (x < minX)
return minX;
else if (x < maxX)
return x;
else // or, infinity, NaN
return double.PositiveInfinity;
}
struct PiecewiseSegment
{
public Vector2 start;
public Line line;
public PiecewiseSegment NegatedY()
{
return new PiecewiseSegment { start = new Vector2(start.X, -start.Y), line = line.NegatedY() };
}
}
static List SweepDescendingMaxY(double minX, double maxX, Line[] lines)
{
// Find the piecewise maximum of all the lines.
// NOTE: Monotonic decreasing Y, monotonic increasing gradient, max Y at minX.
bool startsBelow(PiecewiseSegment seg, Line line) => seg.start.Y < line.YAt(seg.start.X);
// Order segments by gradient, steep to shallow.
// For each line:
// Discard segments of the piecewise function that start below the line.
// Intersect and append a new segment.
Array.Sort(lines, (a, b) => a.gradient.CompareTo(b.gradient));
var output = new List();
foreach (Line line in lines)
{
while (output.Count >= 1 && startsBelow(output.Last(), line))
{
output.RemoveAt(output.Count - 1);
}
if (output.Count == 0)
{
output.Add(new PiecewiseSegment { start = new Vector2(minX, line.YAt(minX)), line = line });
}
else
{
double x = GetIntersectX(output.Last().line, line, output.Last().start.X, maxX);
if (output.Last().start.X < x && x < maxX)
output.Add(new PiecewiseSegment { start = new Vector2(x, line.YAt(x)), line = line });
}
}
return output;
}
static List SweepDescendingMinY(double minX, double maxX, Line[] lines)
{
// Negate X and Y.
// -f(-x) = gradient * (x - refX)
// -f(x) = gradient * (-x - refX)
// f(x) = gradient * (x + refX)
List segs = SweepDescendingMaxY(-maxX, -minX, lines.Select(line => new Line
{
gradient = line.gradient,
refX = -line.refX
}).ToArray()).Select(seg => new PiecewiseSegment
{
start = -seg.start,
line = new Line { gradient = seg.line.gradient, refX = -seg.line.refX }
}).Reverse().ToList();
// The segs above have end points instead of start points. Shift them and set the start point.
for (int i = segs.Count - 1; i > 0; --i)
{
segs[i] = new PiecewiseSegment { start = segs[i - 1].start, line = segs[i].line };
}
segs[0] = new PiecewiseSegment { start = new Vector2(minX, segs[0].line.YAt(minX)), line = segs[0].line };
return segs;
}
static Vector3 PerspectiveCameraFitByAxisAlignedPlaneIntersections(
WorldView world, Plane[] viewspacePlanes, Vector3 viewspaceCenter, Vector3[] viewspacePoints,
bool useSolver)
{
Plane[] viewspaceBoundingPlanes = viewspacePlanes.Select(plane => new Plane(
plane.Normal,
viewspacePoints.Select(point => plane.Normal.Dot(point)).Min()
)).ToArray();
double maxViewspaceZ = viewspacePoints.Select(p => p.Z).Max();
// Axis-aligned plane intersection as 2D line intersection: [a, b].[x or y, z] + c = 0
Vector3 viewspaceLPlane2D = new Vector3(viewspaceBoundingPlanes[0].Normal.X, viewspaceBoundingPlanes[0].Normal.Z, -viewspaceBoundingPlanes[0].DistanceFromOrigin);
Vector3 viewspaceRPlane2D = new Vector3(viewspaceBoundingPlanes[1].Normal.X, viewspaceBoundingPlanes[1].Normal.Z, -viewspaceBoundingPlanes[1].DistanceFromOrigin);
Vector3 viewspaceBPlane2D = new Vector3(viewspaceBoundingPlanes[2].Normal.Y, viewspaceBoundingPlanes[2].Normal.Z, -viewspaceBoundingPlanes[2].DistanceFromOrigin);
Vector3 viewspaceTPlane2D = new Vector3(viewspaceBoundingPlanes[3].Normal.Y, viewspaceBoundingPlanes[3].Normal.Z, -viewspaceBoundingPlanes[3].DistanceFromOrigin);
Vector3 intersectionLRInXZ = viewspaceLPlane2D.Cross(viewspaceRPlane2D);
Vector3 intersectionBTInYZ = viewspaceBPlane2D.Cross(viewspaceTPlane2D);
intersectionLRInXZ.Xy /= intersectionLRInXZ.Z;
intersectionBTInYZ.Xy /= intersectionBTInYZ.Z;
double maxZByPlaneIntersections = Math.Max(intersectionLRInXZ.Y, intersectionBTInYZ.Y);
double maxZByNearPlane = maxViewspaceZ + WorldView.PerspectiveProjectionMinimumNearZ;
// Initial position, before adjustment.
Vector3 viewspaceCameraPosition = new Vector3(intersectionLRInXZ.X, intersectionBTInYZ.X, Math.Max(maxZByPlaneIntersections, maxZByNearPlane));
double optimiseAxis(int axis, double min, double max)
{
if (!useSolver)
{
// Pick a point closest to viewspaceCenter.
return Math.Min(Math.Max(viewspaceCenter[axis], min), max);
}
// [camX, camY, camZ] = viewspaceCameraPosition (the initial guess, with the final Z)
// ndcX = m[1,1] / (z - camZ) * (x - camX)
// ndcY = m[2,2] / (z - camZ) * (y - camY)
Line[] ndcLines = viewspacePoints.Select(viewspacePoint => new Line
{
gradient = world.ProjectionMatrix[axis, axis] / (viewspacePoint.Z - viewspaceCameraPosition.Z),
refX = viewspacePoint[axis]
}).ToArray();
List piecewiseMax = SweepDescendingMaxY(min, max, ndcLines);
List piecewiseMin = SweepDescendingMinY(min, max, ndcLines);
#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
using (var file = new StreamWriter("perspective centering.csv"))
{
foreach (Line line in ndcLines)
{
double ndcAtMin = line.gradient * (min - line.refX);
double ndcAtMax = line.gradient * (max - line.refX);
file.WriteLine("{0}, {1}", min, ndcAtMin);
file.WriteLine("{0}, {1}", max, ndcAtMax);
}
file.WriteLine("");
foreach (PiecewiseSegment seg in piecewiseMax)
file.WriteLine("{0}, {1}", seg.start.X, seg.start.Y);
file.WriteLine("{0}, {1}", max, piecewiseMax.Last().line.gradient * (max - piecewiseMax.Last().line.refX));
file.WriteLine("");
foreach (PiecewiseSegment seg in piecewiseMin)
file.WriteLine("{0}, {1}", seg.start.X, seg.start.Y);
file.WriteLine("{0}, {1}", max, piecewiseMin.Last().line.gradient * (max - piecewiseMin.Last().line.refX));
}
#endif
// Now, with the piecewise min and max functions, determine the X at which max == -min.
// Max is decreasing, -min is increasing. At some point, they should cross over.
// Cross-over cannot be before minX.
if (piecewiseMax[0].start.Y <= -piecewiseMin[0].start.Y)
{
return min;
}
int maxI = 0;
int minI = 0;
double? resultX = null;
#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
using (var file = new StreamWriter("perspective piecewise crossover.csv"))
#endif
{
while (maxI < piecewiseMax.Count && minI < piecewiseMin.Count)
{
PiecewiseSegment maxSeg = piecewiseMax[maxI];
PiecewiseSegment minSeg = piecewiseMin[minI].NegatedY();
double maxSegEndX = maxI + 1 < piecewiseMax.Count ? piecewiseMax[maxI + 1].start.X : max;
double minSegEndX = minI + 1 < piecewiseMin.Count ? piecewiseMin[minI + 1].start.X : max;
double sectionMinX = Math.Max(maxSeg.start.X, minSeg.start.X);
double sectionMaxX = Math.Min(maxSegEndX, minSegEndX);
double crossoverX = GetIntersectX(maxSeg.line, minSeg.line, sectionMinX, sectionMaxX);
#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
file.WriteLine("{0}, {1}, {2}, {3}", sectionMinX, maxSeg.line.YAt(sectionMinX), sectionMinX, minSeg.line.YAt(sectionMinX));
#endif
if (crossoverX < sectionMaxX && !resultX.HasValue)
{
resultX = crossoverX;
#if !ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
return resultX.Value;
#endif
}
if (maxSegEndX < minSegEndX)
{
++maxI;
}
else
{
++minI;
}
}
#if ENABLE_PERSPECTIVE_FITTING_DEBUG_DUMP
file.WriteLine("{0}, {1}, {2}, {3}", max, piecewiseMax.Last().line.YAt(max), max, piecewiseMin.Last().NegatedY().line.YAt(max));
#endif
}
return resultX ?? max;
}
// Two axes are restrained to a single value. The last has a range of valid values.
if (intersectionLRInXZ.Y < intersectionBTInYZ.Y)
{
// The camera will be on the intersection of the top/bottom planes.
// The left/right planes in front intersect with the horizontal line and determine the limits of X.
double minX = (viewspaceRPlane2D.Y * intersectionBTInYZ.Y + viewspaceRPlane2D.Z) / -viewspaceRPlane2D.X;
double maxX = (viewspaceLPlane2D.Y * intersectionBTInYZ.Y + viewspaceLPlane2D.Z) / -viewspaceLPlane2D.X;
viewspaceCameraPosition.X = optimiseAxis(0, minX, maxX);
}
else
{
// The camera will be on the intersection of the left/right planes.
// The top/bottom planes in front intersect with the vertical line and determine the limits of Y.
double minY = (viewspaceTPlane2D.Y * intersectionLRInXZ.Y + viewspaceTPlane2D.Z) / -viewspaceTPlane2D.X;
double maxY = (viewspaceBPlane2D.Y * intersectionLRInXZ.Y + viewspaceBPlane2D.Z) / -viewspaceBPlane2D.X;
viewspaceCameraPosition.Y = optimiseAxis(1, minY, maxY);
}
return viewspaceCameraPosition;
}
/// Tetrahedron-based AABB-frustum intersection code for perspective projection dynamic near/far planes.
/// Temporary, may be replaced with a method that fits to individual triangles if needed.
/// This tetrahedron clipping is only used if
/// ENABLE_PERSPECTIVE_PROJECTION_DYNAMIC_NEAR_FAR is defined in View3DWidget.cs, or
/// USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING is defined.
struct Tetrahedron
{
public Vector3 a, b, c, d;
}
static Tetrahedron[] ClipTetrahedron(Tetrahedron T, Plane plane)
{
// true iff inside
Vector3[] vs = new Vector3[] { T.a, T.b, T.c, T.d };
bool[] sides = vs.Select(v => plane.GetDistanceFromPlane(v) > 0).ToArray();
int numInside = sides.Count(b => b);
Vector3 temp;
switch (numInside)
{
case 0:
default:
return new Tetrahedron[] { };
case 1:
{
int i = Array.IndexOf(sides, true);
(vs[0], vs[i]) = (vs[i], vs[0]);
temp = vs[0]; plane.ClipLine(ref temp, ref vs[1]);
temp = vs[0]; plane.ClipLine(ref temp, ref vs[2]);
temp = vs[0]; plane.ClipLine(ref temp, ref vs[3]);
// One tetra inside.
return new Tetrahedron[] {
new Tetrahedron{ a = vs[0], b = vs[1], c = vs[2], d = vs[3] },
};
}
case 2:
{
int i = Array.IndexOf(sides, true);
(vs[0], vs[i]) = (vs[i], vs[0]);
(sides[0], sides[i]) = (sides[i], sides[0]);
int j = Array.IndexOf(sides, true, 1);
(vs[1], vs[j]) = (vs[j], vs[1]);
Vector3 v02 = vs[2];
Vector3 v03 = vs[3];
Vector3 v12 = vs[2];
Vector3 v13 = vs[3];
temp = vs[0]; plane.ClipLine(ref temp, ref v02);
temp = vs[0]; plane.ClipLine(ref temp, ref v03);
temp = vs[1]; plane.ClipLine(ref temp, ref v12);
temp = vs[1]; plane.ClipLine(ref temp, ref v13);
// Three new tetra sharing the common edge v03-v12.
return new Tetrahedron[] {
new Tetrahedron{ a = v12, b = v03, c = vs[0], d = v02 },
new Tetrahedron{ a = v12, b = v03, c = vs[0], d = vs[1] },
new Tetrahedron{ a = v12, b = v03, c = vs[1], d = v13 },
};
}
case 3:
{
int i = Array.IndexOf(sides, false);
(vs[3], vs[i]) = (vs[i], vs[3]);
Vector3 v03 = vs[3];
Vector3 v13 = vs[3];
Vector3 v23 = vs[3];
temp = vs[0]; plane.ClipLine(ref temp, ref v03);
temp = vs[1]; plane.ClipLine(ref temp, ref v13);
temp = vs[2]; plane.ClipLine(ref temp, ref v23);
// Three new tetra.
return new Tetrahedron[] {
new Tetrahedron{ a = vs[0], b = v03, c = v13, d = v23 },
new Tetrahedron{ a = vs[0], b = vs[1], c = v13, d = v23 },
new Tetrahedron{ a = vs[0], b = vs[1], c = vs[2], d = v23 },
};
}
case 4:
return new Tetrahedron[] { T };
}
}
static readonly Tetrahedron[] BoxOfTetras = new Func(() =>
{
Vector3[] corners = new Vector3[] {
new Vector3(+1, +1, +1), // [0]
new Vector3(-1, +1, +1), // [1]
new Vector3(+1, -1, +1), // [2]
new Vector3(-1, -1, +1), // [3]
new Vector3(+1, +1, -1), // [4]
new Vector3(-1, +1, -1), // [5]
new Vector3(+1, -1, -1), // [6]
new Vector3(-1, -1, -1), // [7]
};
// All the tetras share a common diagonal edge.
var box = new Tetrahedron[] {
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[5], d = corners[4] },
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[4], d = corners[6] },
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[6], d = corners[2] },
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[2], d = corners[3] },
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[3], d = corners[1] },
new Tetrahedron{ a = corners[0], b = corners[7], c = corners[1], d = corners[5] },
};
// Sanity check.
double V = box.Select(T => Math.Abs((T.a - T.d).Dot((T.b - T.d).Cross(T.c - T.d)))).Sum();
System.Diagnostics.Debug.Assert(MathHelper.AlmostEqual(V, 2 * 2 * 2 * 6, 1e-5));
return box;
})();
static Tetrahedron[] MakeAABBTetraArray(AxisAlignedBoundingBox box)
{
Vector3 halfsize = box.Size * 0.5;
Vector3 center = box.Center;
return BoxOfTetras.Select(T => new Tetrahedron
{
a = center + T.a * halfsize,
b = center + T.b * halfsize,
c = center + T.c * halfsize,
d = center + T.d * halfsize,
}).ToArray();
}
static Tetrahedron[] ClipTetras(Tetrahedron[] tetra, Plane plane)
{
return tetra.SelectMany(T => ClipTetrahedron(T, plane)).ToArray();
}
public static Tuple ComputeNearFarOfClippedWorldspaceAABB(bool isOrthographic, Plane[] worldspacePlanes, Matrix4X4 worldToViewspace, AxisAlignedBoundingBox worldspceAABB)
{
if (worldspceAABB == null || worldspceAABB.XSize < 0)
{
return null;
}
#if !USE_TETRAHEDRON_CUTTING_FOR_ORTHOGRAPHIC_NEAR_FAR_FITTING
if (isOrthographic)
{
Mesh mesh = PlatonicSolids.CreateCube(worldspceAABB.Size);
mesh.Translate(worldspceAABB.Center);
double tolerance = 0.001;
foreach (Plane plane in worldspacePlanes)
{
mesh.Split(plane, onPlaneDistance: tolerance, cleanAndMerge: false, discardFacesOnNegativeSide: true);
// Remove any faces outside the plane (without using discardFacesOnNegativeSide).
//for (int i = mesh.Faces.Count - 1; i >= 0; --i)
//{
// Face face = mesh.Faces[i];
// double maxDist = new int[] { face.v0, face.v1, face.v2 }.Select(vi => plane.Normal.Dot(new Vector3(mesh.Vertices[vi]))).Max();
// if (maxDist < (plane.DistanceFromOrigin < 0 ? plane.DistanceFromOrigin * (1 - 1e-4) : plane.DistanceFromOrigin * (1 + 1e-4)))
// {
// mesh.Faces[i] = mesh.Faces[mesh.Faces.Count - 1];
// mesh.Faces.RemoveAt(mesh.Faces.Count - 1);
// }
//}
}
mesh.CleanAndMerge();
if (mesh.Vertices.Any())
{
mesh.Transform(worldToViewspace);
var depths = mesh.Vertices.Select(v => -v.Z);
return Tuple.Create(depths.Min(), depths.Max());
}
}
else
#endif
{
// The above works for orthographic, but won't for perspective as the planes aren't parallel to Z.
// So, cut some tetrahedra instead.
Tetrahedron[] tetras = MakeAABBTetraArray(worldspceAABB);
foreach (Plane plane in worldspacePlanes)
{
tetras = ClipTetras(tetras, plane);
}
if (tetras.Any())
{
var vertices = tetras.SelectMany(T => new Vector3[] { T.a, T.b, T.c, T.d });
var depths = vertices.Select(v => -v.TransformPosition(worldToViewspace).Z).ToArray();
return Tuple.Create(depths.Min(), depths.Max());
}
}
return null;
}
}
}