Skip to content

Commit e1cd119

Browse files
committed
refactor: harden frustum
1 parent 4be5594 commit e1cd119

2 files changed

Lines changed: 230 additions & 21 deletions

File tree

src/FixedMathSharp/Bounds/BoundingFrustum.cs

Lines changed: 133 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace FixedMathSharp;
55

66
/// <summary>
7-
/// Represents a viewing frustum extracted from a combined view-projection matrix.
7+
/// Represents a frustum bounded by six clipping planes.
88
/// </summary>
99
public sealed class BoundingFrustum : IEquatable<BoundingFrustum>
1010
{
@@ -24,7 +24,7 @@ public sealed class BoundingFrustum : IEquatable<BoundingFrustum>
2424

2525
#region Fields
2626

27-
private Fixed4x4 _matrix;
27+
private Fixed4x4? _matrix;
2828
private readonly Vector3d[] _corners;
2929
private readonly FixedPlane[] _planes;
3030

@@ -46,27 +46,65 @@ public BoundingFrustum(Fixed4x4 matrix)
4646
{
4747
_corners = new Vector3d[CornerCount];
4848
_planes = new FixedPlane[PlaneCount];
49-
Matrix = matrix;
49+
SetMatrix(matrix);
50+
}
51+
52+
/// <summary>
53+
/// Initializes a new frustum from six clipping planes.
54+
/// </summary>
55+
public BoundingFrustum(
56+
FixedPlane near,
57+
FixedPlane far,
58+
FixedPlane left,
59+
FixedPlane right,
60+
FixedPlane top,
61+
FixedPlane bottom)
62+
{
63+
_corners = new Vector3d[CornerCount];
64+
_planes = new FixedPlane[PlaneCount];
65+
SetPlanes(near, far, left, right, top, bottom);
66+
}
67+
68+
/// <summary>
69+
/// Initializes a new frustum from six clipping planes in near, far, left, right, top, bottom order.
70+
/// </summary>
71+
public BoundingFrustum(FixedPlane[] planes)
72+
{
73+
if (planes == null)
74+
throw new ArgumentNullException(nameof(planes));
75+
76+
if (planes.Length != PlaneCount)
77+
throw new ArgumentException($"A frustum must be defined by exactly {PlaneCount} planes.", nameof(planes));
78+
79+
_corners = new Vector3d[CornerCount];
80+
_planes = new FixedPlane[PlaneCount];
81+
SetPlanes(planes);
5082
}
5183

5284
#endregion
5385

5486
#region Properties
5587

5688
/// <summary>
57-
/// Gets or sets the matrix used to define this frustum.
89+
/// Gets a value indicating whether this frustum was created from a matrix source.
90+
/// </summary>
91+
public bool HasMatrix
92+
{
93+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
94+
get => _matrix.HasValue;
95+
}
96+
97+
/// <summary>
98+
/// Gets or sets the matrix source used to define this frustum.
5899
/// </summary>
100+
/// <exception cref="InvalidOperationException">
101+
/// Thrown when reading the matrix from a frustum that was created from planes.
102+
/// </exception>
59103
public Fixed4x4 Matrix
60104
{
61105
[MethodImpl(MethodImplOptions.AggressiveInlining)]
62-
get => _matrix;
63-
set
64-
{
65-
_matrix = value;
66-
CreatePlanes();
67-
CreateCorners();
68-
UpdateBounds();
69-
}
106+
get => _matrix ?? throw new InvalidOperationException("This frustum was not created from a matrix.");
107+
set => SetMatrix(value);
70108
}
71109

72110
/// <summary>
@@ -380,14 +418,76 @@ public void GetCorners(Vector3d[] corners)
380418
Array.Copy(_corners, corners, CornerCount);
381419
}
382420

383-
private void CreatePlanes()
421+
/// <summary>
422+
/// Returns a copy of the frustum plane array in near, far, left, right, top, bottom order.
423+
/// </summary>
424+
public FixedPlane[] GetPlanes()
425+
{
426+
var planes = new FixedPlane[PlaneCount];
427+
Array.Copy(_planes, planes, PlaneCount);
428+
return planes;
429+
}
430+
431+
/// <summary>
432+
/// Copies this frustum's planes into the specified array in near, far, left, right, top, bottom order.
433+
/// </summary>
434+
public void GetPlanes(FixedPlane[] planes)
435+
{
436+
if (planes == null)
437+
throw new ArgumentNullException(nameof(planes));
438+
439+
if (planes.Length < PlaneCount)
440+
throw new ArgumentOutOfRangeException(nameof(planes));
441+
442+
Array.Copy(_planes, planes, PlaneCount);
443+
}
444+
445+
private void SetMatrix(Fixed4x4 matrix)
446+
{
447+
_matrix = matrix;
448+
CreatePlanes(matrix);
449+
CreateCorners();
450+
UpdateBounds();
451+
}
452+
453+
private void SetPlanes(FixedPlane[] planes)
454+
{
455+
for (int i = 0; i < PlaneCount; i++)
456+
_planes[i] = FixedPlane.Normalize(planes[i]);
457+
458+
_matrix = null;
459+
CreateCorners();
460+
UpdateBounds();
461+
}
462+
463+
private void SetPlanes(
464+
FixedPlane near,
465+
FixedPlane far,
466+
FixedPlane left,
467+
FixedPlane right,
468+
FixedPlane top,
469+
FixedPlane bottom)
384470
{
385-
_planes[0] = FixedPlane.Normalize(new FixedPlane(-_matrix.m02, -_matrix.m12, -_matrix.m22, -_matrix.m32));
386-
_planes[1] = FixedPlane.Normalize(new FixedPlane(_matrix.m02 - _matrix.m03, _matrix.m12 - _matrix.m13, _matrix.m22 - _matrix.m23, _matrix.m32 - _matrix.m33));
387-
_planes[2] = FixedPlane.Normalize(new FixedPlane(-_matrix.m03 - _matrix.m00, -_matrix.m13 - _matrix.m10, -_matrix.m23 - _matrix.m20, -_matrix.m33 - _matrix.m30));
388-
_planes[3] = FixedPlane.Normalize(new FixedPlane(_matrix.m00 - _matrix.m03, _matrix.m10 - _matrix.m13, _matrix.m20 - _matrix.m23, _matrix.m30 - _matrix.m33));
389-
_planes[4] = FixedPlane.Normalize(new FixedPlane(_matrix.m01 - _matrix.m03, _matrix.m11 - _matrix.m13, _matrix.m21 - _matrix.m23, _matrix.m31 - _matrix.m33));
390-
_planes[5] = FixedPlane.Normalize(new FixedPlane(-_matrix.m03 - _matrix.m01, -_matrix.m13 - _matrix.m11, -_matrix.m23 - _matrix.m21, -_matrix.m33 - _matrix.m31));
471+
_planes[0] = FixedPlane.Normalize(near);
472+
_planes[1] = FixedPlane.Normalize(far);
473+
_planes[2] = FixedPlane.Normalize(left);
474+
_planes[3] = FixedPlane.Normalize(right);
475+
_planes[4] = FixedPlane.Normalize(top);
476+
_planes[5] = FixedPlane.Normalize(bottom);
477+
478+
_matrix = null;
479+
CreateCorners();
480+
UpdateBounds();
481+
}
482+
483+
private void CreatePlanes(Fixed4x4 matrix)
484+
{
485+
_planes[0] = FixedPlane.Normalize(new FixedPlane(-matrix.m02, -matrix.m12, -matrix.m22, -matrix.m32));
486+
_planes[1] = FixedPlane.Normalize(new FixedPlane(matrix.m02 - matrix.m03, matrix.m12 - matrix.m13, matrix.m22 - matrix.m23, matrix.m32 - matrix.m33));
487+
_planes[2] = FixedPlane.Normalize(new FixedPlane(-matrix.m03 - matrix.m00, -matrix.m13 - matrix.m10, -matrix.m23 - matrix.m20, -matrix.m33 - matrix.m30));
488+
_planes[3] = FixedPlane.Normalize(new FixedPlane(matrix.m00 - matrix.m03, matrix.m10 - matrix.m13, matrix.m20 - matrix.m23, matrix.m30 - matrix.m33));
489+
_planes[4] = FixedPlane.Normalize(new FixedPlane(matrix.m01 - matrix.m03, matrix.m11 - matrix.m13, matrix.m21 - matrix.m23, matrix.m31 - matrix.m33));
490+
_planes[5] = FixedPlane.Normalize(new FixedPlane(-matrix.m03 - matrix.m01, -matrix.m13 - matrix.m11, -matrix.m23 - matrix.m21, -matrix.m33 - matrix.m31));
391491
}
392492

393493
private void CreateCorners()
@@ -518,14 +618,26 @@ private static void Project(Vector3d axis, Vector3d[] corners, out Fixed64 min,
518618
/// <inheritdoc/>
519619
public bool Equals(BoundingFrustum? other)
520620
{
521-
return other != null && _matrix.Equals(other._matrix);
621+
if (other == null)
622+
return false;
623+
624+
for (int i = 0; i < PlaneCount; i++)
625+
{
626+
if (_planes[i] != other._planes[i])
627+
return false;
628+
}
629+
630+
return true;
522631
}
523632

524633
/// <inheritdoc/>
525634
public override bool Equals(object? obj) => obj is BoundingFrustum other && Equals(other);
526635

527636
/// <inheritdoc/>
528-
public override int GetHashCode() => _matrix.GetHashCode();
637+
public override int GetHashCode()
638+
{
639+
return HashCode.Combine(_planes[0], _planes[1], _planes[2], _planes[3], _planes[4], _planes[5]);
640+
}
529641

530642
#endregion
531643
}

tests/FixedMathSharp.Tests/Bounds/BoundingFrustum.Tests.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
using System;
12
using Xunit;
23

34
namespace FixedMathSharp.Tests.Bounds;
45

56
public class BoundingFrustumTests
67
{
8+
private static readonly FixedPlane CustomNear = new(Vector3d.Backward, Fixed64.One);
9+
private static readonly FixedPlane CustomFar = new(Vector3d.Forward, new Fixed64(-5));
10+
private static readonly FixedPlane CustomLeft = new(Vector3d.Left, new Fixed64(-2));
11+
private static readonly FixedPlane CustomRight = new(Vector3d.Right, new Fixed64(-2));
12+
private static readonly FixedPlane CustomTop = new(Vector3d.Up, new Fixed64(-3));
13+
private static readonly FixedPlane CustomBottom = new(Vector3d.Down, new Fixed64(-3));
14+
715
[Fact]
816
public void Constructor_IdentityMatrix_CreatesExpectedClipSpaceBounds()
917
{
@@ -166,4 +174,93 @@ public void Matrix_Setter_RebuildsPlanesCornersAndBounds()
166174
Assert.Equal(new Vector3d(-Fixed64.Half, new Fixed64(-1), Fixed64.Zero), frustum.Min);
167175
Assert.Equal(new Vector3d(Fixed64.Half, Fixed64.One, Fixed64.One), frustum.Max);
168176
}
177+
178+
[Fact]
179+
public void Constructor_Planes_CreatesExpectedFrustum()
180+
{
181+
var frustum = new BoundingFrustum(CustomNear, CustomFar, CustomLeft, CustomRight, CustomTop, CustomBottom);
182+
183+
Assert.False(frustum.HasMatrix);
184+
Assert.Throws<InvalidOperationException>(() => frustum.Matrix);
185+
Assert.Equal(new Vector3d(-2, -3, 1), frustum.Min);
186+
Assert.Equal(new Vector3d(2, 3, 5), frustum.Max);
187+
Assert.Equal(ContainmentType.Contains, frustum.Contains(new Vector3d(0, 0, 3)));
188+
Assert.Equal(ContainmentType.Disjoint, frustum.Contains(new Vector3d(0, 0, 6)));
189+
190+
Vector3d[] corners = frustum.GetCorners();
191+
192+
Assert.Equal(new Vector3d(-2, 3, 1), corners[0]);
193+
Assert.Equal(new Vector3d(2, 3, 1), corners[1]);
194+
Assert.Equal(new Vector3d(2, -3, 1), corners[2]);
195+
Assert.Equal(new Vector3d(-2, -3, 1), corners[3]);
196+
Assert.Equal(new Vector3d(-2, 3, 5), corners[4]);
197+
Assert.Equal(new Vector3d(2, 3, 5), corners[5]);
198+
Assert.Equal(new Vector3d(2, -3, 5), corners[6]);
199+
Assert.Equal(new Vector3d(-2, -3, 5), corners[7]);
200+
}
201+
202+
[Fact]
203+
public void Constructor_PlaneArray_ValidatesLengthAndCopiesPlanes()
204+
{
205+
FixedPlane[] planes =
206+
{
207+
CustomNear,
208+
CustomFar,
209+
CustomLeft,
210+
CustomRight,
211+
CustomTop,
212+
CustomBottom
213+
};
214+
215+
var frustum = new BoundingFrustum(planes);
216+
planes[0] = new FixedPlane(Vector3d.Forward, Fixed64.Zero);
217+
218+
Assert.Equal(CustomNear, frustum.Near);
219+
Assert.Throws<ArgumentNullException>(() => new BoundingFrustum(null!));
220+
Assert.Throws<ArgumentException>(() => new BoundingFrustum(new FixedPlane[BoundingFrustum.PlaneCount - 1]));
221+
}
222+
223+
[Fact]
224+
public void GetPlanes_ReturnsCopyInStableOrder()
225+
{
226+
var frustum = new BoundingFrustum(CustomNear, CustomFar, CustomLeft, CustomRight, CustomTop, CustomBottom);
227+
228+
FixedPlane[] planes = frustum.GetPlanes();
229+
230+
Assert.Equal(CustomNear, planes[0]);
231+
Assert.Equal(CustomFar, planes[1]);
232+
Assert.Equal(CustomLeft, planes[2]);
233+
Assert.Equal(CustomRight, planes[3]);
234+
Assert.Equal(CustomTop, planes[4]);
235+
Assert.Equal(CustomBottom, planes[5]);
236+
237+
planes[0] = new FixedPlane(Vector3d.Forward, Fixed64.Zero);
238+
239+
Assert.Equal(CustomNear, frustum.GetPlanes()[0]);
240+
241+
var copied = new FixedPlane[BoundingFrustum.PlaneCount];
242+
frustum.GetPlanes(copied);
243+
244+
Assert.Equal(CustomNear, copied[0]);
245+
Assert.Throws<ArgumentNullException>(() => frustum.GetPlanes(null!));
246+
Assert.Throws<ArgumentOutOfRangeException>(() => frustum.GetPlanes(new FixedPlane[BoundingFrustum.PlaneCount - 1]));
247+
}
248+
249+
[Fact]
250+
public void Equality_ComparesFrustumShapeRatherThanMatrixSource()
251+
{
252+
var matrixFrustum = new BoundingFrustum(Fixed4x4.Identity);
253+
var planeFrustum = new BoundingFrustum(
254+
matrixFrustum.Near,
255+
matrixFrustum.Far,
256+
matrixFrustum.Left,
257+
matrixFrustum.Right,
258+
matrixFrustum.Top,
259+
matrixFrustum.Bottom);
260+
261+
Assert.True(matrixFrustum.HasMatrix);
262+
Assert.Equal(Fixed4x4.Identity, matrixFrustum.Matrix);
263+
Assert.Equal(matrixFrustum, planeFrustum);
264+
Assert.Equal(matrixFrustum.GetHashCode(), planeFrustum.GetHashCode());
265+
}
169266
}

0 commit comments

Comments
 (0)