1 
2 /**
3     inmath.frustum
4 
5     Note: this module is not completly tested!
6     Use with special care, results might be wrong.
7 
8     Authors: David Herberth, Inochi2D Project
9     License: MIT
10 */
11 module inmath.frustum;
12 
13 private {
14     import inmath.linalg : vec3, mat4, dot;
15     import inmath.math : abs, cradians;
16     import inmath.aabb : AABB;
17     import inmath.plane : Plane;
18 }
19 
20 enum {
21     OUTSIDE = 0, /// Used as flag to indicate if the object intersects with the frustum.
22     INSIDE, /// ditto
23     INTERSECT /// ditto
24 }
25 
26 ///
27 struct Frustum {
28     enum {
29         LEFT, /// Used to access the planes array.
30         RIGHT, /// ditto
31         BOTTOM, /// ditto
32         TOP, /// ditto
33         NEAR, /// ditto
34         FAR /// ditto
35     }
36 
37     Plane[6] planes; /// Holds all 6 planes of the frustum.
38 
39     @safe pure nothrow:
40 
41     @property ref inout(Plane) left() inout return { return planes[LEFT]; }
42     @property ref inout(Plane) right() inout return { return planes[RIGHT]; }
43     @property ref inout(Plane) bottom() inout return { return planes[BOTTOM]; }
44     @property ref inout(Plane) top() inout return { return planes[TOP]; }
45     @property ref inout(Plane) near() inout return { return planes[NEAR]; }
46     @property ref inout(Plane) far() inout return { return planes[FAR]; }
47 
48     /// Constructs the frustum from a model-view-projection matrix.
49     /// Params:
50     /// mvp = a model-view-projection matrix
51     this(mat4 mvp) {
52         mvp.transpose(); // we store the matrix row-major
53         
54         planes = [
55             // left
56             Plane(mvp[0][3] + mvp[0][0],
57                   mvp[1][3] + mvp[1][0],
58                   mvp[2][3] + mvp[2][0],
59                   mvp[3][3] + mvp[3][0]),
60 
61             // right
62             Plane(mvp[0][3] - mvp[0][0],
63                   mvp[1][3] - mvp[1][0],
64                   mvp[2][3] - mvp[2][0],
65                   mvp[3][3] - mvp[3][0]),
66 
67             // bottom
68             Plane(mvp[0][3] + mvp[0][1],
69                   mvp[1][3] + mvp[1][1],
70                   mvp[2][3] + mvp[2][1],
71                   mvp[3][3] + mvp[3][1]),
72             // top
73             Plane(mvp[0][3] - mvp[0][1],
74                   mvp[1][3] - mvp[1][1],
75                   mvp[2][3] - mvp[2][1],
76                   mvp[3][3] - mvp[3][1]),
77             // near
78             Plane(mvp[0][3] + mvp[0][2],
79                   mvp[1][3] + mvp[1][2],
80                   mvp[2][3] + mvp[2][2],
81                   mvp[3][3] + mvp[3][2]),
82             // far
83             Plane(mvp[0][3] - mvp[0][2],
84                   mvp[1][3] - mvp[1][2],
85                   mvp[2][3] - mvp[2][2],
86                   mvp[3][3] - mvp[3][2])
87         ];
88 
89         normalize();
90     }
91 
92     /// Constructs the frustum from 6 planes.
93     /// Params:
94     /// planes = the 6 frustum planes in the order: left, right, bottom, top, near, far.
95     this(Plane[6] planes) {
96         this.planes = planes;
97         normalize();
98     }
99 
100     private void normalize() {
101         foreach(ref e; planes) {
102             e.normalize();
103         }
104     }
105 
106     /// Checks if the $(I aabb) intersects with the frustum.
107     /// Returns OUTSIDE (= 0), INSIDE (= 1) or INTERSECT (= 2).
108     int intersects(AABB aabb) const {
109         vec3 hextent = aabb.halfExtent;
110         vec3 center = aabb.center;
111 
112         int result = INSIDE;
113         foreach(plane; planes) {
114             float d = dot(center, plane.normal);
115             float r = dot(hextent, abs(plane.normal));
116 
117             if(d + r < -plane.d) {
118                 // outside
119                 return OUTSIDE;
120             }
121             if(d - r < -plane.d) {
122                result = INTERSECT;
123             }
124         }
125 
126         return result;
127     }
128 
129     @("frustrum")
130     unittest {
131         mat4 view = mat4.lookAt(vec3(0), vec3(0, 0, 1), vec3(0, 1, 0));
132         enum aspect = 4.0/3.0;
133         enum fov = 60;
134         enum near = 1;
135         enum far = 100;
136         mat4 proj = mat4.perspective(aspect, 1.0, fov, near, far);
137         auto f = Frustum(proj * view);
138         assert(f.intersects(AABB(vec3(0, 0, 1), vec3(0, 0, 1))) == INSIDE);
139         assert(f.intersects(AABB(vec3(-1), vec3(1))) == INTERSECT);
140         assert(f.intersects(AABB(vec3(-1), vec3(0.99))) == OUTSIDE);
141         assert(f.intersects(AABB(vec3(-1000), vec3(1000))) == INTERSECT);
142         assert(f.intersects(AABB(vec3(0, 0, -1000), vec3(1, 1, 1000))) == INTERSECT);
143         assert(f.intersects(AABB(vec3(-1000, 0, 0), vec3(1000, 0.1, 0.1))) == OUTSIDE);
144         for(int i = near; i < far; i += 10) {
145             assert(f.intersects(AABB(vec3(0, 0,  i), vec3(0.1, 0.1,   i + 1))) == INSIDE);
146             assert(f.intersects(AABB(vec3(0, 0, -i), vec3(0.1, 0.1, -(i + 1)))) == OUTSIDE);
147         }
148         import std.math : tan;
149         float c = aspect * far / tan(cradians!fov);
150         assert(f.intersects(AABB(vec3(c, 0, 99), vec3(c + 1, 1, 101))) == INTERSECT);
151         assert(f.intersects(AABB(vec3(c - 4, 0, 98), vec3(c - 2, 1, 99.99))) == INSIDE);
152         assert(f.intersects(AABB(vec3(c, 0, 100), vec3(c + 1, 0, 101))) == OUTSIDE);
153 
154         proj = mat4.orthographic(-aspect, aspect, -1.0, 1.0, 0, far);
155         f = Frustum(proj * view);
156         assert(f.intersects(AABB(vec3(0, 0, 1), vec3(0, 0, 1))) == INSIDE);
157         assert(f.intersects(AABB(vec3(-1), vec3(1))) == INTERSECT);
158         assert(f.intersects(AABB(vec3(-1), vec3(0.01))) == INTERSECT);
159         assert(f.intersects(AABB(vec3(0, 0, far - 5), vec3(1, 1, far))) == INSIDE);
160         assert(f.intersects(AABB(vec3(0, 0, far - 5), vec3(1, 1, far + 5))) == INTERSECT);
161         assert(f.intersects(AABB(vec3(-1000, 0, -0.01), vec3(1000, 1, 0))) == INTERSECT);
162         assert(f.intersects(AABB(vec3(-1000, 0, -0.02), vec3(1000, 1, -0.01))) == OUTSIDE);
163     }
164 
165     /// Returns true if the $(I aabb) intersects with the frustum or is inside it.
166     bool opBinaryRight(string s : "in")(AABB aabb) const {
167         return intersects(aabb) > 0;
168     }
169 }