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