11constexpr float kWEps = 1e-4f;
12constexpr float kZBias = 1e-4f;
23 uint32_t triStart = 0;
24 uint32_t triCount = 0;
30 bool frustumEnabled =
true;
31 bool occlusionEnabled =
true;
33 uint32_t height = 144;
34 uint32_t maxOccluders = 192;
36 std::vector<float> depth;
37 std::vector<OccluderEntry> occluders;
38 std::vector<Math::Vector3D> triVerts;
40 ViewFrustum frustum{};
42 Math::Vector3D cameraPos{};
44 bool hasFrame =
false;
45 bool rasterizedThisFrame =
false;
46 bool everRasterized =
false;
48 bool adaptiveEnabled =
true;
49 uint32_t probeInterval = 20;
50 uint32_t skipFrames = 0;
52 CullingSystem::Stats stats{};
63 float x = p.GetX(), y = p.GetY(), z = p.GetZ();
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);
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;
79 sx = (ndcx * 0.5f + 0.5f) * w;
80 sy = (0.5f - ndcy * 0.5f) * h;
84int ClipNear(
const ClipVert* in,
int n, ClipVert* out) {
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;
93 float t = (kWEps - A.w) / (B.w - A.w);
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);
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);
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;
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));
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;
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);
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;
141 for (
int y = minY; y <= maxY; ++y) {
142 float w0 = w0Row, w1 = w1Row, w2 = w2Row;
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);
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;
152 if (z < s.depth[idx]) s.depth[idx] = z;
165void RasterClipTri(
const ClipVert& a,
const ClipVert& b,
const ClipVert& c) {
166 ClipVert in[3] = {a, b, c};
168 int n = ClipNear(in, 3, out);
171 CullState& s = State();
172 float w =
static_cast<float>(s.width);
173 float h =
static_cast<float>(s.height);
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);
188 RasterClipTri(ToClip(p0, vp), ToClip(p1, vp), ToClip(p2, vp));
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();
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);
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);
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);
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;
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);
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;
247 float thresh = boxMinZ - kZBias;
248 for (
int y = minY; y <= maxY; ++y) {
250 for (
int x = minX; x <= maxX; ++x) {
251 if (s.depth[row + x] >= thresh)
return true;
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;
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);
273 return (mxx - mnx) < 2.0f || (mxy - mny) < 2.0f;
279 State().frustumEnabled = enabled;
283 State().occlusionEnabled = enabled;
287 return State().frustumEnabled;
291 return State().occlusionEnabled;
295 CullState& s = State();
301 State().maxOccluders = count;
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;
315 CullState& s = State();
316 if (s.width == 0) s.width = 1;
317 if (s.height == 0) s.height = 1;
320 if (s.adaptiveEnabled && s.rasterizedThisFrame &&
321 s.stats.occlusionCulled == 0)
322 s.skipFrames = s.probeInterval;
324 size_t need =
static_cast<size_t>(s.width) * s.height;
325 if (s.depth.size() != need) s.depth.assign(need, 1.0f);
330 s.viewProj = viewProj;
331 s.cameraPos = cameraPos;
333 s.rasterizedThisFrame =
false;
338 CullState& s = State();
339 if (!s.hasFrame || !s.occlusionEnabled)
return;
341 if (!s.frustum.IsAABBVisible(box.
min, box.
max))
return;
347 s.occluders.push_back(e);
348 ++s.stats.occludersSubmitted;
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;
362 for (uint32_t i = 1; i < vertexCount; ++i)
364 if (!s.frustum.IsAABBVisible(bounds.
min, bounds.
max))
return;
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);
373 s.triVerts.push_back(vertices[idx]);
380 e.triCount = indexCount;
382 s.occluders.push_back(e);
383 ++s.stats.occludersSubmitted;
387 CullState& s = State();
388 if (!s.hasFrame)
return;
390 if (s.skipFrames > 0) {
394 s.rasterizedThisFrame =
false;
395 s.stats.occlusionSkipped =
true;
399 auto t0 = std::chrono::high_resolution_clock::now();
401 std::fill(s.depth.begin(), s.depth.end(), 1.0f);
403 std::sort(s.occluders.begin(), s.occluders.end(),
404 [](
const OccluderEntry& a,
const OccluderEntry& b) {
405 return a.distSq < b.distSq;
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;
413 if (rasterized > 0 && !DepthRectVisible(e.bounds))
continue;
415 RasterBox(e.bounds, s.viewProj, s.cameraPos);
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);
426 s.stats.occludersRasterized = rasterized;
427 s.rasterizedThisFrame = rasterized > 0;
428 if (rasterized > 0) s.everRasterized =
true;
430 auto t1 = std::chrono::high_resolution_clock::now();
431 s.stats.rasterizeMs =
432 std::chrono::duration<float, std::milli>(t1 - t0).count();
436 CullState& s = State();
438 if (!s.hasFrame)
return true;
440 if (s.frustumEnabled) {
441 if (!s.frustum.IsAABBVisible(box.
min, box.
max)) {
442 ++s.stats.frustumCulled;
447 if (!s.occlusionEnabled || !s.rasterizedThisFrame)
return true;
449 if (DepthRectVisible(box))
return true;
451 ++s.stats.occlusionCulled;
456 CullState& s = State();
458 if (!s.hasFrame)
return true;
459 if (s.frustumEnabled) {
460 if (!s.frustum.IsAABBVisible(box.
min, box.
max)) {
461 ++s.stats.frustumCulled;
469 return State().stats;
473 CullState& s = State();
474 if (!s.everRasterized || s.depth.empty()) {
481 return s.depth.data();
485 CullState& s = State();
487 s.depth.shrink_to_fit();
489 s.occluders.shrink_to_fit();
491 s.triVerts.shrink_to_fit();
493 s.rasterizedThisFrame =
false;
494 s.everRasterized =
false;
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
Root namespace for everything the engine exposes.
Per-frame counters for the last completed culling pass.
void Merge(const AABB &o)
Grows this box to also cover o.
bool IsValid() const
True if min is componentwise <= max.
float DistanceSq(const Vector3D &p) const