Skip to content

Commit

Permalink
Jitter raster occlusion camera to reduce false positives.
Browse files Browse the repository at this point in the history
Due to the low resolution of the occlusion buffer, small gaps between occluders can be closed and incorrectly occlude instances which should show through the gaps. To ameliorate this problem, this PR jitters the occlusion buffer over time, making it more likely an instance will be seen through a gap. This is used in conjunction with an occlusion timer per instance, to prevent instances flickering on and off rapidly.
  • Loading branch information
lawnjelly committed Apr 3, 2024
1 parent 29b3d9e commit 691854d
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 9 deletions.
2 changes: 2 additions & 0 deletions core/config/project_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,8 @@ ProjectSettings::ProjectSettings() {
GLOBAL_DEF("debug/settings/crash_handler/message.editor",
String("Please include this when reporting the bug on: https://github.com/godotengine/godot/issues"));
GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/occlusion_culling/bvh_build_quality", PROPERTY_HINT_ENUM, "Low,Medium,High"), 2);
GLOBAL_DEF_RST("rendering/occlusion_culling/jitter_projection", true);

GLOBAL_DEF_RST("internationalization/rendering/force_right_to_left_layout_direction", false);
GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "internationalization/rendering/root_node_layout_direction", PROPERTY_HINT_ENUM, "Based on Application Locale,Left-to-Right,Right-to-Left,Based on System Locale"), 0);

Expand Down
3 changes: 3 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2643,6 +2643,9 @@
The [url=https://en.wikipedia.org/wiki/Bounding_volume_hierarchy]Bounding Volume Hierarchy[/url] quality to use when rendering the occlusion culling buffer. Higher values will result in more accurate occlusion culling, at the cost of higher CPU usage. See also [member rendering/occlusion_culling/occlusion_rays_per_thread].
[b]Note:[/b] This property is only read when the project starts. To adjust the BVH build quality at runtime, use [method RenderingServer.viewport_set_occlusion_culling_build_quality].
</member>
<member name="rendering/occlusion_culling/jitter_projection" type="bool" setter="" getter="" default="true">
If [code]true[/code], the projection used for rendering the occlusion buffer will be jittered. This can help prevent objects being incorrectly culled when visible through small gaps.
</member>
<member name="rendering/occlusion_culling/occlusion_rays_per_thread" type="int" setter="" getter="" default="512">
The number of occlusion rays traced per CPU thread. Higher values will result in more accurate occlusion culling, at the cost of higher CPU usage. The occlusion culling buffer's pixel count is roughly equal to [code]occlusion_rays_per_thread * number_of_logical_cpu_cores[/code], so it will depend on the system's CPU. Therefore, CPUs with fewer cores will use a lower resolution to attempt keeping performance costs even across devices. See also [member rendering/occlusion_culling/bvh_build_quality].
[b]Note:[/b] This property is only read when the project starts. To adjust the number of occlusion rays traced per thread at runtime, use [method RenderingServer.viewport_set_occlusion_rays_per_thread].
Expand Down
63 changes: 62 additions & 1 deletion modules/raycast/raycast_occlusion_cull.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,64 @@ void RaycastOcclusionCull::buffer_set_size(RID p_buffer, const Vector2i &p_size)
buffers[p_buffer].resize(p_size);
}

Projection RaycastOcclusionCull::_jitter_projection(const Projection &p_cam_projection, const Size2i &p_viewport_size) {
if (!_jitter_enabled) {
return p_cam_projection;
}

// Prevent divide by zero when using NULL viewport.
if ((p_viewport_size.x <= 0) || (p_viewport_size.y <= 0)) {
return p_cam_projection;
}

Projection p = p_cam_projection;

int32_t frame = Engine::get_singleton()->get_frames_drawn();
frame %= 9;

Vector2 jitter;

switch (frame) {
default:
break;
case 1: {
jitter = Vector2(-1, -1);
} break;
case 2: {
jitter = Vector2(1, -1);
} break;
case 3: {
jitter = Vector2(-1, 1);
} break;
case 4: {
jitter = Vector2(1, 1);
} break;
case 5: {
jitter = Vector2(-0.5f, -0.5f);
} break;
case 6: {
jitter = Vector2(0.5f, -0.5f);
} break;
case 7: {
jitter = Vector2(-0.5f, 0.5f);
} break;
case 8: {
jitter = Vector2(0.5f, 0.5f);
} break;
}

// The multiplier here determines the divergence from center,
// and is to some extent a balancing act.
// Higher divergence gives fewer false hidden, but more false shown.
// False hidden is obvious to viewer, false shown is not.
// False shown can lower percentage that are occluded, and therefore performance.
jitter *= Vector2(1 / (float)p_viewport_size.x, 1 / (float)p_viewport_size.y) * 0.05f;

p.add_jitter_offset(jitter);

return p;
}

void RaycastOcclusionCull::buffer_update(RID p_buffer, const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal) {
if (!buffers.has(p_buffer)) {
return;
Expand All @@ -550,7 +608,9 @@ void RaycastOcclusionCull::buffer_update(RID p_buffer, const Transform3D &p_cam_
Scenario &scenario = scenarios[buffer.scenario_rid];
scenario.update();

buffer.update_camera_rays(p_cam_transform, p_cam_projection, p_cam_orthogonal);
Projection jittered_proj = _jitter_projection(p_cam_projection, buffer.get_occlusion_buffer_size());

buffer.update_camera_rays(p_cam_transform, jittered_proj, p_cam_orthogonal);

scenario.raycast(buffer.camera_rays, buffer.camera_ray_masks.ptr(), buffer.camera_rays_tile_count);
buffer.sort_rays(-p_cam_transform.basis.get_column(2), p_cam_orthogonal);
Expand Down Expand Up @@ -596,6 +656,7 @@ void RaycastOcclusionCull::_init_embree() {
RaycastOcclusionCull::RaycastOcclusionCull() {
raycast_singleton = this;
int default_quality = GLOBAL_GET("rendering/occlusion_culling/bvh_build_quality");
_jitter_enabled = GLOBAL_GET("rendering/occlusion_culling/jitter_projection");
build_quality = RS::ViewportOcclusionCullingBuildQuality(default_quality);
}

Expand Down
2 changes: 2 additions & 0 deletions modules/raycast/raycast_occlusion_cull.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ class RaycastOcclusionCull : public RendererSceneOcclusionCull {
HashMap<RID, Scenario> scenarios;
HashMap<RID, RaycastHZBuffer> buffers;
RS::ViewportOcclusionCullingBuildQuality build_quality;
bool _jitter_enabled = false;

void _init_embree();
Projection _jitter_projection(const Projection &p_cam_projection, const Size2i &p_viewport_size);

public:
virtual bool is_occluder(RID p_rid) override;
Expand Down
4 changes: 3 additions & 1 deletion servers/rendering/renderer_scene_cull.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
idata.base_rid = p_instance->base;
idata.parent_array_index = p_instance->visibility_parent ? p_instance->visibility_parent->array_index : -1;
idata.visibility_index = p_instance->visibility_index;
idata.occlusion_timeout = 0;

for (Instance *E : p_instance->visibility_dependencies) {
Instance *dep_instance = E;
Expand Down Expand Up @@ -2775,7 +2776,7 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul
#define VIS_RANGE_CHECK ((idata.visibility_index == -1) || _visibility_range_check<false>(cull_data.scenario->instance_visibility[idata.visibility_index], cull_data.cam_transform.origin, cull_data.visibility_viewport_mask) == 0)
#define VIS_PARENT_CHECK (_visibility_parent_check(cull_data, idata))
#define VIS_CHECK (visibility_check < 0 ? (visibility_check = (visibility_flags != InstanceData::FLAG_VISIBILITY_DEPENDENCY_NEEDS_CHECK || (VIS_RANGE_CHECK && VIS_PARENT_CHECK))) : visibility_check)
#define OCCLUSION_CULLED (cull_data.occlusion_buffer != nullptr && (cull_data.scenario->instance_data[i].flags & InstanceData::FLAG_IGNORE_OCCLUSION_CULLING) == 0 && cull_data.occlusion_buffer->is_occluded(cull_data.scenario->instance_aabbs[i].bounds, cull_data.cam_transform.origin, inv_cam_transform, *cull_data.camera_matrix, z_near))
#define OCCLUSION_CULLED (cull_data.occlusion_buffer != nullptr && (cull_data.scenario->instance_data[i].flags & InstanceData::FLAG_IGNORE_OCCLUSION_CULLING) == 0 && cull_data.occlusion_buffer->is_occluded(cull_data.scenario->instance_aabbs[i].bounds, cull_data.cam_transform.origin, inv_cam_transform, *cull_data.camera_matrix, z_near, cull_data.scenario->instance_data[i].occlusion_timeout))

if (!HIDDEN_BY_VISIBILITY_CHECKS) {
if ((LAYER_CHECK && IN_FRUSTUM(cull_data.cull->frustum) && VIS_CHECK && !OCCLUSION_CULLED) || (cull_data.scenario->instance_data[i].flags & InstanceData::FLAG_IGNORE_ALL_CULLING)) {
Expand Down Expand Up @@ -4252,6 +4253,7 @@ RendererSceneCull::RendererSceneCull() {
indexer_update_iterations = GLOBAL_GET("rendering/limits/spatial_indexer/update_iterations_per_frame");
thread_cull_threshold = GLOBAL_GET("rendering/limits/spatial_indexer/threaded_cull_minimum_instances");
thread_cull_threshold = MAX(thread_cull_threshold, (uint32_t)WorkerThreadPool::get_singleton()->get_thread_count()); //make sure there is at least one thread per CPU
RendererSceneOcclusionCull::HZBuffer::occlusion_jitter_enabled = GLOBAL_GET("rendering/occlusion_culling/jitter_projection");

dummy_occlusion_culling = memnew(RendererSceneOcclusionCull);

Expand Down
7 changes: 7 additions & 0 deletions servers/rendering/renderer_scene_cull.h
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ class RendererSceneCull : public RenderingMethod {
Instance *instance = nullptr;
int32_t parent_array_index = -1;
int32_t visibility_index = -1;

// Each time occlusion culling determines an instance is visible,
// set this to occlusion_frame plus some delay.
// Once the timeout is reached, allow the instance to be occlusion culled.
// This creates a delay for occlusion culling, which prevents flickering
// when jittering the raster occlusion projection.
uint64_t occlusion_timeout = 0;
};

struct InstanceVisibilityData {
Expand Down
7 changes: 7 additions & 0 deletions servers/rendering/renderer_scene_occlusion_cull.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const Vector3 RendererSceneOcclusionCull::HZBuffer::corners[8] = {
Vector3(1, 1, 1)
};

bool RendererSceneOcclusionCull::HZBuffer::occlusion_jitter_enabled = false;

bool RendererSceneOcclusionCull::HZBuffer::is_empty() const {
return sizes.is_empty();
}
Expand All @@ -66,6 +68,8 @@ void RendererSceneOcclusionCull::HZBuffer::clear() {
}

void RendererSceneOcclusionCull::HZBuffer::resize(const Size2i &p_size) {
occlusion_buffer_size = p_size;

if (p_size == Size2i()) {
clear();
return;
Expand Down Expand Up @@ -124,6 +128,9 @@ void RendererSceneOcclusionCull::HZBuffer::resize(const Size2i &p_size) {
}

void RendererSceneOcclusionCull::HZBuffer::update_mips() {
// Keep this up to date as a local to be used for occlusion timers.
occlusion_frame = Engine::get_singleton()->get_frames_drawn();

if (sizes.is_empty()) {
return;
}
Expand Down
50 changes: 43 additions & 7 deletions servers/rendering/renderer_scene_occlusion_cull.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,10 @@ class RendererSceneOcclusionCull {
PackedByteArray debug_data;
float debug_tex_range = 0.0f;

public:
bool is_empty() const;
virtual void clear();
virtual void resize(const Size2i &p_size);

void update_mips();
uint64_t occlusion_frame = 0;
Size2i occlusion_buffer_size;

_FORCE_INLINE_ bool is_occluded(const real_t p_bounds[6], const Vector3 &p_cam_position, const Transform3D &p_cam_inv_transform, const Projection &p_cam_projection, real_t p_near) const {
_FORCE_INLINE_ bool _is_occluded(const real_t p_bounds[6], const Vector3 &p_cam_position, const Transform3D &p_cam_inv_transform, const Projection &p_cam_projection, real_t p_near) const {
if (is_empty()) {
return false;
}
Expand Down Expand Up @@ -154,7 +150,47 @@ class RendererSceneOcclusionCull {
return !visible;
}

public:
static bool occlusion_jitter_enabled;

bool is_empty() const;
virtual void clear();
virtual void resize(const Size2i &p_size);

void update_mips();

// Thin wrapper around _is_occluded(),
// allowing occlusion timers to delay the disappearance
// of objects to prevent flickering when using jittering.
_FORCE_INLINE_ bool is_occluded(const real_t p_bounds[6], const Vector3 &p_cam_position, const Transform3D &p_cam_inv_transform, const Projection &p_cam_projection, real_t p_near, uint64_t &r_occlusion_timeout) const {
bool occluded = _is_occluded(p_bounds, p_cam_position, p_cam_inv_transform, p_cam_projection, p_near);

// Special case, temporal jitter disabled,
// so we don't use occlusion timers.
if (!occlusion_jitter_enabled) {
return occluded;
}

if (!occluded) {
//#define DEBUG_RASTER_OCCLUSION_JITTER
#ifdef DEBUG_RASTER_OCCLUSION_JITTER
r_occlusion_timeout = occlusion_frame + 1;
#else
r_occlusion_timeout = occlusion_frame + 9;
#endif
} else if (r_occlusion_timeout) {
// Regular timeout, allow occlusion culling
// to proceed as normal after the delay.
if (occlusion_frame >= r_occlusion_timeout) {
r_occlusion_timeout = 0;
}
}

return occluded && !r_occlusion_timeout;
}

RID get_debug_texture();
const Size2i &get_occlusion_buffer_size() const { return occlusion_buffer_size; }

virtual ~HZBuffer(){};
};
Expand Down

0 comments on commit 691854d

Please sign in to comment.