Jitter raster occlusion camera to reduce false positives.

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.
This commit is contained in:
lawnjelly 2023-12-13 13:45:57 +00:00
parent 29b3d9e9e5
commit 691854d589
8 changed files with 129 additions and 9 deletions

View file

@ -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);

View file

@ -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].

View file

@ -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;
@ -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);
@ -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);
}

View file

@ -163,8 +163,10 @@ private:
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;

View file

@ -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;
@ -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)) {
@ -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);

View file

@ -286,6 +286,13 @@ public:
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 {

View file

@ -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();
}
@ -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;
@ -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;
}

View file

@ -53,14 +53,10 @@ public:
PackedByteArray debug_data;
float debug_tex_range = 0.0f;
public:
bool is_empty() const;
virtual void clear();
virtual void resize(const Size2i &p_size);
uint64_t occlusion_frame = 0;
Size2i occlusion_buffer_size;
void update_mips();
_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;
}
@ -154,7 +150,47 @@ public:
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(){};
};