Skip to content

Commit 0f2676a

Browse files
committed
Add an occlusion culling benchmark sample
Measures what a CPU visibility pass costs over a thousand boxes, two ways and at two ray widths, with the accuracy of each. No shading: this measures visibility. The per-object pass runs at 0.16 ms and removes two thirds of the draw calls, about 1% of a 16.6 ms frame. The visibility buffer is four times dearer, culls less, and is far more accurate. Both numbers come with what they get wrong. Three things this turned up that are worth having written down: Packet width has to be asked of the device. rtcIntersect16/rtcOccluded16 may only be called when RTC_DEVICE_PROPERTY_NATIVE_RAY16_SUPPORTED says so, and calling them anyway is undefined behaviour: the first version of this sample used 16 and corrupted the heap, surfacing as a crash inside AlignedFree with no hint of the real cause. The shipped binaries are AVX2, which tops out at 8. Packets are not automatically faster. They lose the per-object early exit -- once one sample proves a box visible the rest are wasted, but the lanes are already committed -- and 9,000 rays with SIMD come out slower than 7,165 without. For the visibility buffer, where every ray is needed anyway, they draw level. The obvious per-object test is wrong. An occlusion ray stopping just short of a sample point on the box has the box occlude itself, since the point sits on its own surface. Asking what the ray hits first costs more and answers the question actually being asked. Accuracy is reported weighted by screen area, not by object count. In a grid this dense most of what is technically visible is visible through a gap a pixel or two wide, so a raw count of missed objects reads as alarming while the pass is fine: 163 boxes missed, 3% of the screen.
1 parent ca6089b commit 0f2676a

13 files changed

Lines changed: 1291 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44

55
# Screenshot produced by the sample at runtime (docs/ holds the committed ones).
66
HelloEmbree/screenshot.png
7+
8+
# Images the sample writes at runtime (docs/ holds the committed ones).
9+
OcclusionCulling/scene.png
10+
OcclusionCulling/verdict.png
11+
OcclusionCulling/slice.png

Embree.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Evergine.Bindings.Embree",
99
EndProject
1010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloEmbree", "HelloEmbree\HelloEmbree.csproj", "{E4461EA5-FD21-4449-A493-7BF77E32C8D2}"
1111
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OcclusionCulling", "OcclusionCulling\OcclusionCulling.csproj", "{9D90D092-3E99-429C-8657-C4809D72375B}"
13+
EndProject
1214
Global
1315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1416
Debug|Any CPU = Debug|Any CPU
@@ -55,6 +57,18 @@ Global
5557
{E4461EA5-FD21-4449-A493-7BF77E32C8D2}.Release|x64.Build.0 = Release|Any CPU
5658
{E4461EA5-FD21-4449-A493-7BF77E32C8D2}.Release|x86.ActiveCfg = Release|Any CPU
5759
{E4461EA5-FD21-4449-A493-7BF77E32C8D2}.Release|x86.Build.0 = Release|Any CPU
60+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|Any CPU.Build.0 = Debug|Any CPU
62+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|x64.ActiveCfg = Debug|Any CPU
63+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|x64.Build.0 = Debug|Any CPU
64+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|x86.ActiveCfg = Debug|Any CPU
65+
{9D90D092-3E99-429C-8657-C4809D72375B}.Debug|x86.Build.0 = Debug|Any CPU
66+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|Any CPU.ActiveCfg = Release|Any CPU
67+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|Any CPU.Build.0 = Release|Any CPU
68+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|x64.ActiveCfg = Release|Any CPU
69+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|x64.Build.0 = Release|Any CPU
70+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|x86.ActiveCfg = Release|Any CPU
71+
{9D90D092-3E99-429C-8657-C4809D72375B}.Release|x86.Build.0 = Release|Any CPU
5872
EndGlobalSection
5973
GlobalSection(SolutionProperties) = preSolution
6074
HideSolutionNode = FALSE

OcclusionCulling/Camera.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Numerics;
3+
4+
namespace OcclusionCulling
5+
{
6+
/// <summary>
7+
/// A pinhole camera plus the six frustum planes derived from it.
8+
/// </summary>
9+
/// <remarks>
10+
/// The AABB test is the same scalar six-plane centre/half-extent form Evergine's
11+
/// <c>BoundingFrustum.Intersects(ref BoundingBox, out bool)</c> uses, reproduced here so the
12+
/// sample stays free of engine dependencies and runs on every RID the binding ships.
13+
/// </remarks>
14+
internal sealed class Camera
15+
{
16+
private readonly Vector4[] planes = new Vector4[6];
17+
18+
public Camera(Vector3 position, Vector3 target, float fovDegrees, float aspect, float near, float far)
19+
{
20+
this.Position = position;
21+
this.Forward = Vector3.Normalize(target - position);
22+
this.Right = Vector3.Normalize(Vector3.Cross(this.Forward, Vector3.UnitY));
23+
this.Up = Vector3.Cross(this.Right, this.Forward);
24+
25+
this.TanHalfFov = MathF.Tan(fovDegrees * MathF.PI / 180.0f * 0.5f);
26+
this.Aspect = aspect;
27+
28+
var view = Matrix4x4.CreateLookAt(position, target, Vector3.UnitY);
29+
var projection = Matrix4x4.CreatePerspectiveFieldOfView(
30+
fovDegrees * MathF.PI / 180.0f, aspect, near, far);
31+
this.ExtractPlanes(view * projection);
32+
}
33+
34+
public Vector3 Position { get; }
35+
36+
public Vector3 Forward { get; }
37+
38+
public Vector3 Right { get; }
39+
40+
public Vector3 Up { get; }
41+
42+
public float TanHalfFov { get; }
43+
44+
public float Aspect { get; }
45+
46+
/// <summary>
47+
/// Ray direction through a normalised screen position, both in [0, 1].
48+
/// </summary>
49+
public Vector3 RayDirection(float u, float v)
50+
{
51+
float x = ((u * 2.0f) - 1.0f) * this.TanHalfFov * this.Aspect;
52+
float y = (1.0f - (v * 2.0f)) * this.TanHalfFov;
53+
return Vector3.Normalize(this.Forward + (this.Right * x) + (this.Up * y));
54+
}
55+
56+
/// <summary>
57+
/// Tests an AABB against the six planes. Conservative: a box straddling a plane counts
58+
/// as inside.
59+
/// </summary>
60+
public bool Intersects(in Vector3 min, in Vector3 max)
61+
{
62+
Vector3 centre = (min + max) * 0.5f;
63+
Vector3 extent = (max - min) * 0.5f;
64+
65+
for (int i = 0; i < 6; i++)
66+
{
67+
Vector4 p = this.planes[i];
68+
var normal = new Vector3(p.X, p.Y, p.Z);
69+
70+
float distance = Vector3.Dot(normal, centre) + p.W;
71+
float radius = Vector3.Dot(Vector3.Abs(normal), extent);
72+
73+
if (distance + radius < 0.0f)
74+
{
75+
return false;
76+
}
77+
}
78+
79+
return true;
80+
}
81+
82+
private void ExtractPlanes(Matrix4x4 m)
83+
{
84+
this.planes[0] = Normalize(new Vector4(m.M14 + m.M11, m.M24 + m.M21, m.M34 + m.M31, m.M44 + m.M41)); // left
85+
this.planes[1] = Normalize(new Vector4(m.M14 - m.M11, m.M24 - m.M21, m.M34 - m.M31, m.M44 - m.M41)); // right
86+
this.planes[2] = Normalize(new Vector4(m.M14 + m.M12, m.M24 + m.M22, m.M34 + m.M32, m.M44 + m.M42)); // bottom
87+
this.planes[3] = Normalize(new Vector4(m.M14 - m.M12, m.M24 - m.M22, m.M34 - m.M32, m.M44 - m.M42)); // top
88+
this.planes[4] = Normalize(new Vector4(m.M13, m.M23, m.M33, m.M43)); // near
89+
this.planes[5] = Normalize(new Vector4(m.M14 - m.M13, m.M24 - m.M23, m.M34 - m.M33, m.M44 - m.M43)); // far
90+
}
91+
92+
private static Vector4 Normalize(Vector4 plane)
93+
{
94+
float length = new Vector3(plane.X, plane.Y, plane.Z).Length();
95+
return length > 0.0f ? plane / length : plane;
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)