SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
CullingSystem.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <vector>
7
8namespace Sleak {
9namespace {
10
11constexpr float kWEps = 1e-4f;
12constexpr float kZBias = 1e-4f;
13
14/// Clip-space vertex (row-vector convention: clip = point * VP).
15struct ClipVert {
16 float x, y, z, w;
17};
18
19/// One submitted occluder queued for FinalizeOccluders, either a box or a range into triVerts.
20struct OccluderEntry {
21 bool isBox = true;
22 Math::AABB bounds{};
23 uint32_t triStart = 0;
24 uint32_t triCount = 0;
25 float distSq = 0.0f;
26};
27
28/// All per-frame culling state: settings, the software depth buffer, and pending occluders.
29struct CullState {
30 bool frustumEnabled = true;
31 bool occlusionEnabled = true;
32 uint32_t width = 256;
33 uint32_t height = 144;
34 uint32_t maxOccluders = 192;
35
36 std::vector<float> depth;
37 std::vector<OccluderEntry> occluders;
38 std::vector<Math::Vector3D> triVerts;
39
40 ViewFrustum frustum{};
41 Math::Matrix4 viewProj{};
42 Math::Vector3D cameraPos{};
43
44 bool hasFrame = false;
45 bool rasterizedThisFrame = false;
46 bool everRasterized = false;
47
48 bool adaptiveEnabled = true;
49 uint32_t probeInterval = 20;
50 uint32_t skipFrames = 0;
51
52 CullingSystem::Stats stats{};
53};
54
55/// Process-wide singleton culling state (the class is all static methods).
56CullState& State() {
57 static CullState s;
58 return s;
59}
60
61/// World point -> clip space.
62inline ClipVert ToClip(const Math::Vector3D& p, const Math::Matrix4& m) {
63 float x = p.GetX(), y = p.GetY(), z = p.GetZ();
64 ClipVert c;
65 c.x = x * m(0, 0) + y * m(1, 0) + z * m(2, 0) + m(3, 0);
66 c.y = x * m(0, 1) + y * m(1, 1) + z * m(2, 1) + m(3, 1);
67 c.z = x * m(0, 2) + y * m(1, 2) + z * m(2, 2) + m(3, 2);
68 c.w = x * m(0, 3) + y * m(1, 3) + z * m(2, 3) + m(3, 3);
69 return c;
70}
71
72/// Clip space -> buffer pixels + NDC z (0 near, 1 far).
73inline void ToScreen(const ClipVert& c, float w, float h, float& sx,
74 float& sy, float& sz) {
75 float inv = 1.0f / c.w;
76 float ndcx = c.x * inv;
77 float ndcy = c.y * inv;
78 sz = c.z * inv;
79 sx = (ndcx * 0.5f + 0.5f) * w;
80 sy = (0.5f - ndcy * 0.5f) * h;
81}
82
83/// Sutherland-Hodgman clip against near plane (w >= kWEps).
84int ClipNear(const ClipVert* in, int n, ClipVert* out) {
85 int m = 0;
86 for (int i = 0; i < n; ++i) {
87 const ClipVert& A = in[i];
88 const ClipVert& B = in[(i + 1) % n];
89 bool inA = A.w >= kWEps;
90 bool inB = B.w >= kWEps;
91 if (inA) out[m++] = A;
92 if (inA != inB) {
93 float t = (kWEps - A.w) / (B.w - A.w);
94 ClipVert I;
95 I.x = A.x + t * (B.x - A.x);
96 I.y = A.y + t * (B.y - A.y);
97 I.z = A.z + t * (B.z - A.z);
98 I.w = A.w + t * (B.w - A.w);
99 out[m++] = I;
100 }
101 }
102 return m;
103}
104
105/// Edge-function rasterizer with incremental row stepping, min-depth write.
106void RasterScreenTri(float x0, float y0, float z0, float x1, float y1,
107 float z1, float x2, float y2, float z2) {
108 CullState& s = State();
109 int W = static_cast<int>(s.width);
110 int H = static_cast<int>(s.height);
111
112 float area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0);
113 if (std::fabs(area) < 1e-4f) return;
114 float invArea = 1.0f / area;
115
116 float fminX = std::min(x0, std::min(x1, x2));
117 float fmaxX = std::max(x0, std::max(x1, x2));
118 float fminY = std::min(y0, std::min(y1, y2));
119 float fmaxY = std::max(y0, std::max(y1, y2));
120
121 int minX = static_cast<int>(std::floor(fminX));
122 int maxX = static_cast<int>(std::ceil(fmaxX));
123 int minY = static_cast<int>(std::floor(fminY));
124 int maxY = static_cast<int>(std::ceil(fmaxY));
125 if (minX < 0) minX = 0;
126 if (minY < 0) minY = 0;
127 if (maxX > W - 1) maxX = W - 1;
128 if (maxY > H - 1) maxY = H - 1;
129 if (minX > maxX || minY > maxY) return;
130
131 float px0 = minX + 0.5f;
132 float py0 = minY + 0.5f;
133 float w0Row = (x2 - x1) * (py0 - y1) - (y2 - y1) * (px0 - x1);
134 float w1Row = (x0 - x2) * (py0 - y2) - (y0 - y2) * (px0 - x2);
135 float w2Row = (x1 - x0) * (py0 - y0) - (y1 - y0) * (px0 - x0);
136
137 float A0 = y1 - y2, A1 = y2 - y0, A2 = y0 - y1;
138 float B0 = x2 - x1, B1 = x0 - x2, B2 = x1 - x0;
139 bool positive = area > 0.0f;
140
141 for (int y = minY; y <= maxY; ++y) {
142 float w0 = w0Row, w1 = w1Row, w2 = w2Row;
143 int row = y * W;
144 for (int x = minX; x <= maxX; ++x) {
145 bool inside = positive ? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f)
146 : (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f);
147 if (inside) {
148 float z = (w0 * z0 + w1 * z1 + w2 * z2) * invArea;
149 if (z < 0.0f) z = 0.0f;
150 if (z > 1.0f) z = 1.0f;
151 int idx = row + x;
152 if (z < s.depth[idx]) s.depth[idx] = z;
153 }
154 w0 += A0;
155 w1 += A1;
156 w2 += A2;
157 }
158 w0Row += B0;
159 w1Row += B1;
160 w2Row += B2;
161 }
162}
163
164/// Near-clip a clip-space triangle, fan-triangulate, rasterize.
165void RasterClipTri(const ClipVert& a, const ClipVert& b, const ClipVert& c) {
166 ClipVert in[3] = {a, b, c};
167 ClipVert out[8];
168 int n = ClipNear(in, 3, out);
169 if (n < 3) return;
170
171 CullState& s = State();
172 float w = static_cast<float>(s.width);
173 float h = static_cast<float>(s.height);
174
175 float sx0, sy0, sz0;
176 ToScreen(out[0], w, h, sx0, sy0, sz0);
177 for (int i = 1; i + 1 < n; ++i) {
178 float sx1, sy1, sz1, sx2, sy2, sz2;
179 ToScreen(out[i], w, h, sx1, sy1, sz1);
180 ToScreen(out[i + 1], w, h, sx2, sy2, sz2);
181 RasterScreenTri(sx0, sy0, sz0, sx1, sy1, sz1, sx2, sy2, sz2);
182 }
183}
184
185/// Projects a world-space triangle and rasterizes it into the depth buffer.
186void RasterWorldTri(const Math::Vector3D& p0, const Math::Vector3D& p1,
187 const Math::Vector3D& p2, const Math::Matrix4& vp) {
188 RasterClipTri(ToClip(p0, vp), ToClip(p1, vp), ToClip(p2, vp));
189}
190
191/// Rasterize up to 3 camera-facing faces of a solid box.
192void RasterBox(const Math::AABB& box, const Math::Matrix4& vp,
193 const Math::Vector3D& cam) {
194 float mnx = box.min.GetX(), mny = box.min.GetY(), mnz = box.min.GetZ();
195 float mxx = box.max.GetX(), mxy = box.max.GetY(), mxz = box.max.GetZ();
196 using V = Math::Vector3D;
197 V c000(mnx, mny, mnz), c100(mxx, mny, mnz);
198 V c010(mnx, mxy, mnz), c110(mxx, mxy, mnz);
199 V c001(mnx, mny, mxz), c101(mxx, mny, mxz);
200 V c011(mnx, mxy, mxz), c111(mxx, mxy, mxz);
201
202 auto quad = [&](const V& a, const V& b, const V& c, const V& d) {
203 RasterWorldTri(a, b, c, vp);
204 RasterWorldTri(a, c, d, vp);
205 };
206
207 if (cam.GetX() < mnx) quad(c000, c001, c011, c010);
208 if (cam.GetX() > mxx) quad(c100, c110, c111, c101);
209 if (cam.GetY() < mny) quad(c000, c100, c101, c001);
210 if (cam.GetY() > mxy) quad(c010, c011, c111, c110);
211 if (cam.GetZ() < mnz) quad(c000, c010, c110, c100);
212 if (cam.GetZ() > mxz) quad(c001, c101, c111, c011);
213}
214
215/// Conservative depth test: true when the box could be in front of the
216/// rasterized occluders anywhere in its projected rect.
217bool DepthRectVisible(const Math::AABB& box) {
218 CullState& s = State();
219 float w = static_cast<float>(s.width);
220 float h = static_cast<float>(s.height);
221 float mnx = 1e30f, mny = 1e30f, mxx = -1e30f, mxy = -1e30f;
222 float boxMinZ = 1e30f;
223 for (int i = 0; i < 8; ++i) {
224 ClipVert c = ToClip(box.Corner(i), s.viewProj);
225 if (c.w <= kWEps) return true;
226 float sx, sy, sz;
227 ToScreen(c, w, h, sx, sy, sz);
228 mnx = std::min(mnx, sx);
229 mxx = std::max(mxx, sx);
230 mny = std::min(mny, sy);
231 mxy = std::max(mxy, sy);
232 boxMinZ = std::min(boxMinZ, sz);
233 }
234
235 int W = static_cast<int>(s.width);
236 int H = static_cast<int>(s.height);
237 int minX = static_cast<int>(std::floor(mnx)) - 1;
238 int maxX = static_cast<int>(std::ceil(mxx)) + 1;
239 int minY = static_cast<int>(std::floor(mny)) - 1;
240 int maxY = static_cast<int>(std::ceil(mxy)) + 1;
241 if (minX < 0) minX = 0;
242 if (minY < 0) minY = 0;
243 if (maxX > W - 1) maxX = W - 1;
244 if (maxY > H - 1) maxY = H - 1;
245 if (minX > maxX || minY > maxY) return true;
246
247 float thresh = boxMinZ - kZBias;
248 for (int y = minY; y <= maxY; ++y) {
249 int row = y * W;
250 for (int x = minX; x <= maxX; ++x) {
251 if (s.depth[row + x] >= thresh) return true;
252 }
253 }
254 return false;
255}
256
257/// True when the box projects entirely in front but spans < 2x2 px.
258bool ProjectedTooSmall(const Math::AABB& b, const Math::Matrix4& vp) {
259 CullState& s = State();
260 float w = static_cast<float>(s.width);
261 float h = static_cast<float>(s.height);
262 float mnx = 1e30f, mny = 1e30f, mxx = -1e30f, mxy = -1e30f;
263 for (int i = 0; i < 8; ++i) {
264 ClipVert c = ToClip(b.Corner(i), vp);
265 if (c.w <= kWEps) return false;
266 float sx, sy, sz;
267 ToScreen(c, w, h, sx, sy, sz);
268 mnx = std::min(mnx, sx);
269 mxx = std::max(mxx, sx);
270 mny = std::min(mny, sy);
271 mxy = std::max(mxy, sy);
272 }
273 return (mxx - mnx) < 2.0f || (mxy - mny) < 2.0f;
274}
275
276} // namespace
277
279 State().frustumEnabled = enabled;
280}
281
283 State().occlusionEnabled = enabled;
284}
285
287 return State().frustumEnabled;
288}
289
291 return State().occlusionEnabled;
292}
293
295 CullState& s = State();
296 s.width = width > 0 ? width : 1;
297 s.height = height > 0 ? height : 1;
298}
299
300void CullingSystem::SetMaxOccluders(uint32_t count) {
301 State().maxOccluders = count;
302}
303
305 uint32_t probeInterval) {
306 CullState& s = State();
307 s.adaptiveEnabled = enabled;
308 s.probeInterval = probeInterval > 0 ? probeInterval : 1;
309 if (!enabled) s.skipFrames = 0;
310}
311
313 const Math::Matrix4& viewProj,
314 const Math::Vector3D& cameraPos) {
315 CullState& s = State();
316 if (s.width == 0) s.width = 1;
317 if (s.height == 0) s.height = 1;
318
319 // Adaptive: a rasterized frame that culled nothing idles the pass.
320 if (s.adaptiveEnabled && s.rasterizedThisFrame &&
321 s.stats.occlusionCulled == 0)
322 s.skipFrames = s.probeInterval;
323
324 size_t need = static_cast<size_t>(s.width) * s.height;
325 if (s.depth.size() != need) s.depth.assign(need, 1.0f);
326
327 s.occluders.clear();
328 s.triVerts.clear();
329 s.frustum = frustum;
330 s.viewProj = viewProj;
331 s.cameraPos = cameraPos;
332 s.hasFrame = true;
333 s.rasterizedThisFrame = false;
334 s.stats = Stats{};
335}
336
338 CullState& s = State();
339 if (!s.hasFrame || !s.occlusionEnabled) return;
340 if (!box.IsValid()) return;
341 if (!s.frustum.IsAABBVisible(box.min, box.max)) return;
342
343 OccluderEntry e;
344 e.isBox = true;
345 e.bounds = box;
346 e.distSq = box.DistanceSq(s.cameraPos);
347 s.occluders.push_back(e);
348 ++s.stats.occludersSubmitted;
349}
350
352 uint32_t vertexCount,
353 const uint32_t* indices,
354 uint32_t indexCount) {
355 CullState& s = State();
356 if (!s.hasFrame || !s.occlusionEnabled) return;
357 if (!vertices || vertexCount == 0) return;
358 if (!indices || indexCount < 3) return;
359 indexCount -= indexCount % 3;
360
361 Math::AABB bounds(vertices[0], vertices[0]);
362 for (uint32_t i = 1; i < vertexCount; ++i)
363 bounds.Merge(Math::AABB(vertices[i], vertices[i]));
364 if (!s.frustum.IsAABBVisible(bounds.min, bounds.max)) return;
365
366 uint32_t start = static_cast<uint32_t>(s.triVerts.size());
367 for (uint32_t i = 0; i < indexCount; ++i) {
368 uint32_t idx = indices[i];
369 if (idx >= vertexCount) {
370 s.triVerts.resize(start);
371 return;
372 }
373 s.triVerts.push_back(vertices[idx]);
374 }
375
376 OccluderEntry e;
377 e.isBox = false;
378 e.bounds = bounds;
379 e.triStart = start;
380 e.triCount = indexCount;
381 e.distSq = bounds.DistanceSq(s.cameraPos);
382 s.occluders.push_back(e);
383 ++s.stats.occludersSubmitted;
384}
385
387 CullState& s = State();
388 if (!s.hasFrame) return;
389
390 if (s.skipFrames > 0) {
391 --s.skipFrames;
392 s.occluders.clear();
393 s.triVerts.clear();
394 s.rasterizedThisFrame = false;
395 s.stats.occlusionSkipped = true;
396 return;
397 }
398
399 auto t0 = std::chrono::high_resolution_clock::now();
400
401 std::fill(s.depth.begin(), s.depth.end(), 1.0f);
402
403 std::sort(s.occluders.begin(), s.occluders.end(),
404 [](const OccluderEntry& a, const OccluderEntry& b) {
405 return a.distSq < b.distSq;
406 });
407
408 uint32_t rasterized = 0;
409 for (const auto& e : s.occluders) {
410 if (rasterized >= s.maxOccluders) break;
411 if (ProjectedTooSmall(e.bounds, s.viewProj)) continue;
412 // Occluder fusion: skip occluders fully behind rasterized ones.
413 if (rasterized > 0 && !DepthRectVisible(e.bounds)) continue;
414 if (e.isBox) {
415 RasterBox(e.bounds, s.viewProj, s.cameraPos);
416 } else {
417 for (uint32_t i = 0; i + 2 < e.triCount; i += 3) {
418 RasterWorldTri(s.triVerts[e.triStart + i],
419 s.triVerts[e.triStart + i + 1],
420 s.triVerts[e.triStart + i + 2], s.viewProj);
421 }
422 }
423 ++rasterized;
424 }
425
426 s.stats.occludersRasterized = rasterized;
427 s.rasterizedThisFrame = rasterized > 0;
428 if (rasterized > 0) s.everRasterized = true;
429
430 auto t1 = std::chrono::high_resolution_clock::now();
431 s.stats.rasterizeMs =
432 std::chrono::duration<float, std::milli>(t1 - t0).count();
433}
434
436 CullState& s = State();
437 ++s.stats.tested;
438 if (!s.hasFrame) return true;
439
440 if (s.frustumEnabled) {
441 if (!s.frustum.IsAABBVisible(box.min, box.max)) {
442 ++s.stats.frustumCulled;
443 return false;
444 }
445 }
446
447 if (!s.occlusionEnabled || !s.rasterizedThisFrame) return true;
448
449 if (DepthRectVisible(box)) return true;
450
451 ++s.stats.occlusionCulled;
452 return false;
453}
454
456 CullState& s = State();
457 ++s.stats.tested;
458 if (!s.hasFrame) return true;
459 if (s.frustumEnabled) {
460 if (!s.frustum.IsAABBVisible(box.min, box.max)) {
461 ++s.stats.frustumCulled;
462 return false;
463 }
464 }
465 return true;
466}
467
469 return State().stats;
470}
471
472const float* CullingSystem::GetDepthBuffer(uint32_t& width, uint32_t& height) {
473 CullState& s = State();
474 if (!s.everRasterized || s.depth.empty()) {
475 width = 0;
476 height = 0;
477 return nullptr;
478 }
479 width = s.width;
480 height = s.height;
481 return s.depth.data();
482}
483
485 CullState& s = State();
486 s.depth.clear();
487 s.depth.shrink_to_fit();
488 s.occluders.clear();
489 s.occluders.shrink_to_fit();
490 s.triVerts.clear();
491 s.triVerts.shrink_to_fit();
492 s.hasFrame = false;
493 s.rasterizedThisFrame = false;
494 s.everRasterized = false;
495 s.stats = Stats{};
496}
497
498} // namespace Sleak
int width
int height
static bool IsVisibleFrustumOnly(const Math::AABB &box)
Frustum-only visibility test, skipping the occlusion buffer entirely.
static const Stats & GetStats()
static void SetAdaptiveOcclusion(bool enabled, uint32_t probeInterval)
static void SetOcclusionBufferSize(uint32_t width, uint32_t height)
Occlusion depth buffer resolution (default 256x144).
static void SetOcclusionCullingEnabled(bool enabled)
static const float * GetDepthBuffer(uint32_t &width, uint32_t &height)
static void SubmitOccluderTriangles(const Math::Vector3D *vertices, uint32_t vertexCount, const uint32_t *indices, uint32_t indexCount)
World-space triangle occluder; same fully-solid-volume requirement as SubmitOccluderBox.
static void SubmitOccluderBox(const Math::AABB &box)
World-space occluders. Boxes must be fully solid volumes.
static void BeginFrame(const ViewFrustum &frustum, const Math::Matrix4 &viewProj, const Math::Vector3D &cameraPos)
static bool IsVisible(const Math::AABB &box)
static void FinalizeOccluders()
Sorts submitted occluders by distance and rasterizes them into the depth buffer up to the max-occlude...
static bool IsFrustumCullingEnabled()
static bool IsOcclusionCullingEnabled()
static void SetMaxOccluders(uint32_t count)
static void SetFrustumCullingEnabled(bool enabled)
Matrix< float, 4, 4 > Matrix4
Definition Matrix.hpp:413
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
Per-frame counters for the last completed culling pass.
void Merge(const AABB &o)
Grows this box to also cover o.
Definition AABB.hpp:36
Vector3D min
Definition AABB.hpp:12
bool IsValid() const
True if min is componentwise <= max.
Definition AABB.hpp:19
Vector3D max
Definition AABB.hpp:13
float DistanceSq(const Vector3D &p) const
Definition AABB.hpp:59