Skip to content

Commit

Permalink
Shadow volume culling and tighter shadow caster culling
Browse files Browse the repository at this point in the history
Existing shadow caster culling takes no account of the camera.
This PR adds the highly encapsulated class RenderingLightCuller which can cut down the casters in the shadow volume to only those which can cast shadows on the camera frustum.
  • Loading branch information
lawnjelly committed Jan 30, 2024
1 parent 51991e2 commit 4577dfd
Show file tree
Hide file tree
Showing 7 changed files with 1,515 additions and 13 deletions.
10 changes: 10 additions & 0 deletions core/error/error_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -812,4 +812,14 @@ void _err_flush_stdout();
#define DEV_ASSERT(m_cond)
#endif

#ifdef DEV_ENABLED
#define DEV_CHECK_ONCE(m_cond) \
if (unlikely(!(m_cond))) { \
ERR_PRINT_ONCE("DEV_CHECK_ONCE failed \"" _STR(m_cond) "\" is false."); \
} else \
((void)0)
#else
#define DEV_CHECK_ONCE(m_cond)
#endif

#endif // ERROR_MACROS_H
6 changes: 6 additions & 0 deletions core/templates/paged_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ class PagedArray {
count--;
}

void remove_at_unordered(uint64_t p_index) {
ERR_FAIL_UNSIGNED_INDEX(p_index, count);
(*this)[p_index] = (*this)[count - 1];
pop_back();
}

void clear() {
//destruct if needed
if (!std::is_trivially_destructible<T>::value) {
Expand Down
4 changes: 4 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2530,6 +2530,10 @@
<member name="rendering/lights_and_shadows/positional_shadow/soft_shadow_filter_quality.mobile" type="int" setter="" getter="" default="0">
Lower-end override for [member rendering/lights_and_shadows/positional_shadow/soft_shadow_filter_quality] on mobile devices, due to performance concerns or driver support.
</member>
<member name="rendering/lights_and_shadows/tighter_shadow_caster_culling" type="bool" setter="" getter="" default="true">
If [code]true[/code], items that cannot cast shadows into the view frustum will not be rendered into shadow maps.
This can increase performance.
</member>
<member name="rendering/lights_and_shadows/use_physical_light_units" type="bool" setter="" getter="" default="false">
Enables the use of physically based units for light sources. Physically based units tend to be much larger than the arbitrary units used by Godot, but they can be used to match lighting within Godot to real-world lighting. Due to the large dynamic range of lighting conditions present in nature, Godot bakes exposure into the various lighting quantities before rendering. Most light sources bake exposure automatically at run time based on the active [CameraAttributes] resource, but [LightmapGI] and [VoxelGI] require a [CameraAttributes] resource to be set at bake time to reduce the dynamic range. At run time, Godot will automatically reconcile the baked exposure with the active exposure to ensure lighting remains consistent.
</member>
Expand Down
82 changes: 71 additions & 11 deletions servers/rendering/renderer_scene_cull.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "core/config/project_settings.h"
#include "core/object/worker_thread_pool.h"
#include "core/os/os.h"
#include "rendering_light_culler.h"
#include "rendering_server_default.h"

#include <new>
Expand Down Expand Up @@ -158,7 +159,7 @@ void RendererSceneCull::_instance_pair(Instance *p_A, Instance *p_B) {
light->geometries.insert(A);

if (geom->can_cast_shadows) {
light->shadow_dirty = true;
light->make_shadow_dirty();
}

if (A->scenario && A->array_index >= 0) {
Expand Down Expand Up @@ -265,7 +266,7 @@ void RendererSceneCull::_instance_unpair(Instance *p_A, Instance *p_B) {
light->geometries.erase(A);

if (geom->can_cast_shadows) {
light->shadow_dirty = true;
light->make_shadow_dirty();
}

if (A->scenario && A->array_index >= 0) {
Expand Down Expand Up @@ -871,7 +872,7 @@ void RendererSceneCull::instance_set_layer_mask(RID p_instance, uint32_t p_mask)
if (geom->can_cast_shadows) {
for (HashSet<RendererSceneCull::Instance *>::Iterator I = geom->lights.begin(); I != geom->lights.end(); ++I) {
InstanceLightData *light = static_cast<InstanceLightData *>((*I)->base_data);
light->shadow_dirty = true;
light->make_shadow_dirty();
}
}
}
Expand Down Expand Up @@ -1565,7 +1566,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {

RSG::light_storage->light_instance_set_transform(light->instance, p_instance->transform);
RSG::light_storage->light_instance_set_aabb(light->instance, p_instance->transform.xform(p_instance->aabb));
light->shadow_dirty = true;
light->make_shadow_dirty();

RS::LightBakeMode bake_mode = RSG::light_storage->light_get_bake_mode(p_instance->base);
if (RSG::light_storage->light_get_type(p_instance->base) != RS::LIGHT_DIRECTIONAL && bake_mode != light->bake_mode) {
Expand Down Expand Up @@ -1650,7 +1651,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
if (geom->can_cast_shadows) {
for (const Instance *E : geom->lights) {
InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data);
light->shadow_dirty = true;
light->make_shadow_dirty();
}
}

Expand Down Expand Up @@ -2075,6 +2076,9 @@ void RendererSceneCull::_update_instance_lightmap_captures(Instance *p_instance)
}

void RendererSceneCull::_light_instance_setup_directional_shadow(int p_shadow_index, Instance *p_instance, const Transform3D p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, bool p_cam_vaspect) {
// For later tight culling, the light culler needs to know the details of the directional light.
light_culler->prepare_directional_light(p_instance, p_shadow_index);

InstanceLightData *light = static_cast<InstanceLightData *>(p_instance->base_data);

Transform3D light_transform = p_instance->transform;
Expand Down Expand Up @@ -2345,6 +2349,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons

RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++];

if (!light->is_shadow_update_full()) {
light_culler->cull_regular_light(instance_shadow_cull_result);
}

for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
Instance *instance = instance_shadow_cull_result[j];
if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) {
Expand Down Expand Up @@ -2423,6 +2431,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons

RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++];

if (!light->is_shadow_update_full()) {
light_culler->cull_regular_light(instance_shadow_cull_result);
}

for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
Instance *instance = instance_shadow_cull_result[j];
if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) {
Expand Down Expand Up @@ -2486,6 +2498,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons

RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++];

if (!light->is_shadow_update_full()) {
light_culler->cull_regular_light(instance_shadow_cull_result);
}

for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
Instance *instance = instance_shadow_cull_result[j];
if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) {
Expand Down Expand Up @@ -2940,6 +2956,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul
}

for (uint32_t j = 0; j < cull_data.cull->shadow_count; j++) {
if (!light_culler->cull_directional_light(cull_data.scenario->instance_aabbs[i], j)) {
continue;
}
for (uint32_t k = 0; k < cull_data.cull->shadows[j].cascade_count; k++) {
if (IN_FRUSTUM(cull_data.cull->shadows[j].cascades[k].frustum) && VIS_CHECK) {
uint32_t base_type = idata.flags & InstanceData::FLAG_BASE_TYPE_MASK;
Expand Down Expand Up @@ -2992,6 +3011,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul
void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref<RenderSceneBuffers> &p_render_buffers, RID p_environment, RID p_force_camera_attributes, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, bool p_using_shadows, RenderingMethod::RenderInfo *r_render_info) {
Instance *render_reflection_probe = instance_owner.get_or_null(p_reflection_probe); //if null, not rendering to it

// Prepare the light - camera volume culling system.
light_culler->prepare_camera(p_camera_data->main_transform, p_camera_data->main_projection);

Scenario *scenario = scenario_owner.get_or_null(p_scenario);

ERR_FAIL_COND(p_render_buffers.is_null());
Expand Down Expand Up @@ -3126,6 +3148,7 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c
#ifdef DEBUG_CULL_TIME
uint64_t time_from = OS::get_singleton()->get_ticks_usec();
#endif

if (cull_to > thread_cull_threshold) {
//multiple threads
for (InstanceCullResult &thread : scene_cull_result_threads) {
Expand Down Expand Up @@ -3263,20 +3286,46 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c
}
}

if (light->shadow_dirty) {
light->last_version++;
light->shadow_dirty = false;
// We can detect whether multiple cameras are hitting this light, whether or not the shadow is dirty,
// so that we can turn off tighter caster culling.
light->detect_light_intersects_multiple_cameras(Engine::get_singleton()->get_frames_drawn());

if (light->is_shadow_dirty()) {
// Dirty shadows have no need to be drawn if
// the light volume doesn't intersect the camera frustum.

// Returns false if the entire light can be culled.
bool allow_redraw = light_culler->prepare_regular_light(*ins);

// Directional lights aren't handled here, _light_instance_update_shadow is called from elsewhere.
// Checking for this in case this changes, as this is assumed.
DEV_CHECK_ONCE(RSG::light_storage->light_get_type(ins->base) != RS::LIGHT_DIRECTIONAL);

// Tighter caster culling to the camera frustum should work correctly with multiple viewports + cameras.
// The first camera will cull tightly, but if the light is present on more than 1 camera, the second will
// do a full render, and mark the light as non-dirty.
// There is however a cost to tighter shadow culling in this situation (2 shadow updates in 1 frame),
// so we should detect this and switch off tighter caster culling automatically.
// This is done in the logic for `decrement_shadow_dirty()`.
if (allow_redraw) {
light->last_version++;
light->decrement_shadow_dirty();
}
}

bool redraw = RSG::light_storage->shadow_atlas_update_light(p_shadow_atlas, light->instance, coverage, light->last_version);

if (redraw && max_shadows_used < MAX_UPDATE_SHADOWS) {
//must redraw!
RENDER_TIMESTAMP("> Render Light3D " + itos(i));
light->shadow_dirty = _light_instance_update_shadow(ins, p_camera_data->main_transform, p_camera_data->main_projection, p_camera_data->is_orthogonal, p_camera_data->vaspect, p_shadow_atlas, scenario, p_screen_mesh_lod_threshold, p_visible_layers);
if (_light_instance_update_shadow(ins, p_camera_data->main_transform, p_camera_data->main_projection, p_camera_data->is_orthogonal, p_camera_data->vaspect, p_shadow_atlas, scenario, p_screen_mesh_lod_threshold, p_visible_layers)) {
light->make_shadow_dirty();
}
RENDER_TIMESTAMP("< Render Light3D " + itos(i));
} else {
light->shadow_dirty = redraw;
if (redraw) {
light->make_shadow_dirty();
}
}
}
}
Expand Down Expand Up @@ -3953,7 +4002,7 @@ void RendererSceneCull::_update_dirty_instance(Instance *p_instance) {
//ability to cast shadows change, let lights now
for (const Instance *E : geom->lights) {
InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data);
light->shadow_dirty = true;
light->make_shadow_dirty();
}

geom->can_cast_shadows = can_cast_shadows;
Expand Down Expand Up @@ -4165,6 +4214,12 @@ RendererSceneCull::RendererSceneCull() {
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

dummy_occlusion_culling = memnew(RendererSceneOcclusionCull);

light_culler = memnew(RenderingLightCuller);

bool tighter_caster_culling = GLOBAL_DEF("rendering/lights_and_shadows/tighter_shadow_caster_culling", true);
light_culler->set_caster_culling_active(tighter_caster_culling);
light_culler->set_light_culling_active(tighter_caster_culling);
}

RendererSceneCull::~RendererSceneCull() {
Expand All @@ -4187,4 +4242,9 @@ RendererSceneCull::~RendererSceneCull() {
if (dummy_occlusion_culling) {
memdelete(dummy_occlusion_culling);
}

if (light_culler) {
memdelete(light_culler);
light_culler = nullptr;
}
}
53 changes: 51 additions & 2 deletions servers/rendering/renderer_scene_cull.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
#include "servers/rendering/storage/utilities.h"
#include "servers/xr/xr_interface.h"

class RenderingLightCuller;

class RendererSceneCull : public RenderingMethod {
public:
RendererSceneRender *scene_render = nullptr;
Expand Down Expand Up @@ -679,7 +681,6 @@ class RendererSceneCull : public RenderingMethod {
uint64_t last_version;
List<Instance *>::Element *D; // directional light in scenario

bool shadow_dirty;
bool uses_projector = false;
bool uses_softshadow = false;

Expand All @@ -690,12 +691,59 @@ class RendererSceneCull : public RenderingMethod {
RS::LightBakeMode bake_mode;
uint32_t max_sdfgi_cascade = 2;

private:
// Instead of a single dirty flag, we maintain a count
// so that we can detect lights that are being made dirty
// each frame, and switch on tighter caster culling.
int32_t shadow_dirty_count;

uint32_t light_update_frame_id;
bool light_intersects_multiple_cameras;
uint32_t light_intersects_multiple_cameras_timeout_frame_id;

public:
bool is_shadow_dirty() const { return shadow_dirty_count != 0; }
void make_shadow_dirty() { shadow_dirty_count = light_intersects_multiple_cameras ? 1 : 2; }
void detect_light_intersects_multiple_cameras(uint32_t p_frame_id) {
// We need to detect the case where shadow updates are occurring
// more than once per frame. In this case, we need to turn off
// tighter caster culling, so situation reverts to one full shadow update
// per frame (light_intersects_multiple_cameras is set).
if (p_frame_id == light_update_frame_id) {
light_intersects_multiple_cameras = true;
light_intersects_multiple_cameras_timeout_frame_id = p_frame_id + 60;
} else {
// When shadow_volume_intersects_multiple_cameras is set, we
// want to detect the situation this is no longer the case, via a timeout.
// The system can go back to tighter caster culling in this situation.
// Having a long-ish timeout prevents rapid cycling.
if (light_intersects_multiple_cameras && (p_frame_id >= light_intersects_multiple_cameras_timeout_frame_id)) {
light_intersects_multiple_cameras = false;
light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX;
}
}
light_update_frame_id = p_frame_id;
}

void decrement_shadow_dirty() {
shadow_dirty_count--;
DEV_ASSERT(shadow_dirty_count >= 0);
}

// Shadow updates can either full (everything in the shadow volume)
// or closely culled to the camera frustum.
bool is_shadow_update_full() const { return shadow_dirty_count == 0; }

InstanceLightData() {
bake_mode = RS::LIGHT_BAKE_DISABLED;
shadow_dirty = true;
D = nullptr;
last_version = 0;
baked_light = nullptr;

shadow_dirty_count = 1;
light_update_frame_id = UINT32_MAX;
light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX;
light_intersects_multiple_cameras = false;
}
};

Expand Down Expand Up @@ -955,6 +1003,7 @@ class RendererSceneCull : public RenderingMethod {
uint32_t geometry_instance_pair_mask = 0; // used in traditional forward, unnecessary on clustered

LocalVector<Vector2> camera_jitter_array;
RenderingLightCuller *light_culler = nullptr;

virtual RID instance_allocate();
virtual void instance_initialize(RID p_rid);
Expand Down
Loading

0 comments on commit 4577dfd

Please sign in to comment.