Skip to content

Commit f8a039e

Browse files
authored
Merge pull request #84745 from lawnjelly/lightcull
Shadow volume culling and tighter shadow caster culling
2 parents cb0d450 + 4577dfd commit f8a039e

File tree

7 files changed

+1515
-13
lines changed

7 files changed

+1515
-13
lines changed

core/error/error_macros.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,4 +812,14 @@ void _err_flush_stdout();
812812
#define DEV_ASSERT(m_cond)
813813
#endif
814814

815+
#ifdef DEV_ENABLED
816+
#define DEV_CHECK_ONCE(m_cond) \
817+
if (unlikely(!(m_cond))) { \
818+
ERR_PRINT_ONCE("DEV_CHECK_ONCE failed \"" _STR(m_cond) "\" is false."); \
819+
} else \
820+
((void)0)
821+
#else
822+
#define DEV_CHECK_ONCE(m_cond)
823+
#endif
824+
815825
#endif // ERROR_MACROS_H

core/templates/paged_array.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ class PagedArray {
229229
count--;
230230
}
231231

232+
void remove_at_unordered(uint64_t p_index) {
233+
ERR_FAIL_UNSIGNED_INDEX(p_index, count);
234+
(*this)[p_index] = (*this)[count - 1];
235+
pop_back();
236+
}
237+
232238
void clear() {
233239
//destruct if needed
234240
if (!std::is_trivially_destructible<T>::value) {

doc/classes/ProjectSettings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,6 +2548,10 @@
25482548
<member name="rendering/lights_and_shadows/positional_shadow/soft_shadow_filter_quality.mobile" type="int" setter="" getter="" default="0">
25492549
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.
25502550
</member>
2551+
<member name="rendering/lights_and_shadows/tighter_shadow_caster_culling" type="bool" setter="" getter="" default="true">
2552+
If [code]true[/code], items that cannot cast shadows into the view frustum will not be rendered into shadow maps.
2553+
This can increase performance.
2554+
</member>
25512555
<member name="rendering/lights_and_shadows/use_physical_light_units" type="bool" setter="" getter="" default="false">
25522556
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.
25532557
</member>

servers/rendering/renderer_scene_cull.cpp

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "core/config/project_settings.h"
3434
#include "core/object/worker_thread_pool.h"
3535
#include "core/os/os.h"
36+
#include "rendering_light_culler.h"
3637
#include "rendering_server_default.h"
3738

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

160161
if (geom->can_cast_shadows) {
161-
light->shadow_dirty = true;
162+
light->make_shadow_dirty();
162163
}
163164

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

267268
if (geom->can_cast_shadows) {
268-
light->shadow_dirty = true;
269+
light->make_shadow_dirty();
269270
}
270271

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

15661567
RSG::light_storage->light_instance_set_transform(light->instance, p_instance->transform);
15671568
RSG::light_storage->light_instance_set_aabb(light->instance, p_instance->transform.xform(p_instance->aabb));
1568-
light->shadow_dirty = true;
1569+
light->make_shadow_dirty();
15691570

15701571
RS::LightBakeMode bake_mode = RSG::light_storage->light_get_bake_mode(p_instance->base);
15711572
if (RSG::light_storage->light_get_type(p_instance->base) != RS::LIGHT_DIRECTIONAL && bake_mode != light->bake_mode) {
@@ -1650,7 +1651,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
16501651
if (geom->can_cast_shadows) {
16511652
for (const Instance *E : geom->lights) {
16521653
InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data);
1653-
light->shadow_dirty = true;
1654+
light->make_shadow_dirty();
16541655
}
16551656
}
16561657

@@ -2075,6 +2076,9 @@ void RendererSceneCull::_update_instance_lightmap_captures(Instance *p_instance)
20752076
}
20762077

20772078
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) {
2079+
// For later tight culling, the light culler needs to know the details of the directional light.
2080+
light_culler->prepare_directional_light(p_instance, p_shadow_index);
2081+
20782082
InstanceLightData *light = static_cast<InstanceLightData *>(p_instance->base_data);
20792083

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

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

2352+
if (!light->is_shadow_update_full()) {
2353+
light_culler->cull_regular_light(instance_shadow_cull_result);
2354+
}
2355+
23482356
for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
23492357
Instance *instance = instance_shadow_cull_result[j];
23502358
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)) {
@@ -2423,6 +2431,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons
24232431

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

2434+
if (!light->is_shadow_update_full()) {
2435+
light_culler->cull_regular_light(instance_shadow_cull_result);
2436+
}
2437+
24262438
for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
24272439
Instance *instance = instance_shadow_cull_result[j];
24282440
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)) {
@@ -2486,6 +2498,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons
24862498

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

2501+
if (!light->is_shadow_update_full()) {
2502+
light_culler->cull_regular_light(instance_shadow_cull_result);
2503+
}
2504+
24892505
for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) {
24902506
Instance *instance = instance_shadow_cull_result[j];
24912507
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)) {
@@ -2940,6 +2956,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul
29402956
}
29412957

29422958
for (uint32_t j = 0; j < cull_data.cull->shadow_count; j++) {
2959+
if (!light_culler->cull_directional_light(cull_data.scenario->instance_aabbs[i], j)) {
2960+
continue;
2961+
}
29432962
for (uint32_t k = 0; k < cull_data.cull->shadows[j].cascade_count; k++) {
29442963
if (IN_FRUSTUM(cull_data.cull->shadows[j].cascades[k].frustum) && VIS_CHECK) {
29452964
uint32_t base_type = idata.flags & InstanceData::FLAG_BASE_TYPE_MASK;
@@ -2992,6 +3011,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul
29923011
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) {
29933012
Instance *render_reflection_probe = instance_owner.get_or_null(p_reflection_probe); //if null, not rendering to it
29943013

3014+
// Prepare the light - camera volume culling system.
3015+
light_culler->prepare_camera(p_camera_data->main_transform, p_camera_data->main_projection);
3016+
29953017
Scenario *scenario = scenario_owner.get_or_null(p_scenario);
29963018

29973019
ERR_FAIL_COND(p_render_buffers.is_null());
@@ -3126,6 +3148,7 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c
31263148
#ifdef DEBUG_CULL_TIME
31273149
uint64_t time_from = OS::get_singleton()->get_ticks_usec();
31283150
#endif
3151+
31293152
if (cull_to > thread_cull_threshold) {
31303153
//multiple threads
31313154
for (InstanceCullResult &thread : scene_cull_result_threads) {
@@ -3263,20 +3286,46 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c
32633286
}
32643287
}
32653288

3266-
if (light->shadow_dirty) {
3267-
light->last_version++;
3268-
light->shadow_dirty = false;
3289+
// We can detect whether multiple cameras are hitting this light, whether or not the shadow is dirty,
3290+
// so that we can turn off tighter caster culling.
3291+
light->detect_light_intersects_multiple_cameras(Engine::get_singleton()->get_frames_drawn());
3292+
3293+
if (light->is_shadow_dirty()) {
3294+
// Dirty shadows have no need to be drawn if
3295+
// the light volume doesn't intersect the camera frustum.
3296+
3297+
// Returns false if the entire light can be culled.
3298+
bool allow_redraw = light_culler->prepare_regular_light(*ins);
3299+
3300+
// Directional lights aren't handled here, _light_instance_update_shadow is called from elsewhere.
3301+
// Checking for this in case this changes, as this is assumed.
3302+
DEV_CHECK_ONCE(RSG::light_storage->light_get_type(ins->base) != RS::LIGHT_DIRECTIONAL);
3303+
3304+
// Tighter caster culling to the camera frustum should work correctly with multiple viewports + cameras.
3305+
// The first camera will cull tightly, but if the light is present on more than 1 camera, the second will
3306+
// do a full render, and mark the light as non-dirty.
3307+
// There is however a cost to tighter shadow culling in this situation (2 shadow updates in 1 frame),
3308+
// so we should detect this and switch off tighter caster culling automatically.
3309+
// This is done in the logic for `decrement_shadow_dirty()`.
3310+
if (allow_redraw) {
3311+
light->last_version++;
3312+
light->decrement_shadow_dirty();
3313+
}
32693314
}
32703315

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

32733318
if (redraw && max_shadows_used < MAX_UPDATE_SHADOWS) {
32743319
//must redraw!
32753320
RENDER_TIMESTAMP("> Render Light3D " + itos(i));
3276-
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);
3321+
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)) {
3322+
light->make_shadow_dirty();
3323+
}
32773324
RENDER_TIMESTAMP("< Render Light3D " + itos(i));
32783325
} else {
3279-
light->shadow_dirty = redraw;
3326+
if (redraw) {
3327+
light->make_shadow_dirty();
3328+
}
32803329
}
32813330
}
32823331
}
@@ -3953,7 +4002,7 @@ void RendererSceneCull::_update_dirty_instance(Instance *p_instance) {
39534002
//ability to cast shadows change, let lights now
39544003
for (const Instance *E : geom->lights) {
39554004
InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data);
3956-
light->shadow_dirty = true;
4005+
light->make_shadow_dirty();
39574006
}
39584007

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

41674216
dummy_occlusion_culling = memnew(RendererSceneOcclusionCull);
4217+
4218+
light_culler = memnew(RenderingLightCuller);
4219+
4220+
bool tighter_caster_culling = GLOBAL_DEF("rendering/lights_and_shadows/tighter_shadow_caster_culling", true);
4221+
light_culler->set_caster_culling_active(tighter_caster_culling);
4222+
light_culler->set_light_culling_active(tighter_caster_culling);
41684223
}
41694224

41704225
RendererSceneCull::~RendererSceneCull() {
@@ -4187,4 +4242,9 @@ RendererSceneCull::~RendererSceneCull() {
41874242
if (dummy_occlusion_culling) {
41884243
memdelete(dummy_occlusion_culling);
41894244
}
4245+
4246+
if (light_culler) {
4247+
memdelete(light_culler);
4248+
light_culler = nullptr;
4249+
}
41904250
}

servers/rendering/renderer_scene_cull.h

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
#include "servers/rendering/storage/utilities.h"
4747
#include "servers/xr/xr_interface.h"
4848

49+
class RenderingLightCuller;
50+
4951
class RendererSceneCull : public RenderingMethod {
5052
public:
5153
RendererSceneRender *scene_render = nullptr;
@@ -679,7 +681,6 @@ class RendererSceneCull : public RenderingMethod {
679681
uint64_t last_version;
680682
List<Instance *>::Element *D; // directional light in scenario
681683

682-
bool shadow_dirty;
683684
bool uses_projector = false;
684685
bool uses_softshadow = false;
685686

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

694+
private:
695+
// Instead of a single dirty flag, we maintain a count
696+
// so that we can detect lights that are being made dirty
697+
// each frame, and switch on tighter caster culling.
698+
int32_t shadow_dirty_count;
699+
700+
uint32_t light_update_frame_id;
701+
bool light_intersects_multiple_cameras;
702+
uint32_t light_intersects_multiple_cameras_timeout_frame_id;
703+
704+
public:
705+
bool is_shadow_dirty() const { return shadow_dirty_count != 0; }
706+
void make_shadow_dirty() { shadow_dirty_count = light_intersects_multiple_cameras ? 1 : 2; }
707+
void detect_light_intersects_multiple_cameras(uint32_t p_frame_id) {
708+
// We need to detect the case where shadow updates are occurring
709+
// more than once per frame. In this case, we need to turn off
710+
// tighter caster culling, so situation reverts to one full shadow update
711+
// per frame (light_intersects_multiple_cameras is set).
712+
if (p_frame_id == light_update_frame_id) {
713+
light_intersects_multiple_cameras = true;
714+
light_intersects_multiple_cameras_timeout_frame_id = p_frame_id + 60;
715+
} else {
716+
// When shadow_volume_intersects_multiple_cameras is set, we
717+
// want to detect the situation this is no longer the case, via a timeout.
718+
// The system can go back to tighter caster culling in this situation.
719+
// Having a long-ish timeout prevents rapid cycling.
720+
if (light_intersects_multiple_cameras && (p_frame_id >= light_intersects_multiple_cameras_timeout_frame_id)) {
721+
light_intersects_multiple_cameras = false;
722+
light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX;
723+
}
724+
}
725+
light_update_frame_id = p_frame_id;
726+
}
727+
728+
void decrement_shadow_dirty() {
729+
shadow_dirty_count--;
730+
DEV_ASSERT(shadow_dirty_count >= 0);
731+
}
732+
733+
// Shadow updates can either full (everything in the shadow volume)
734+
// or closely culled to the camera frustum.
735+
bool is_shadow_update_full() const { return shadow_dirty_count == 0; }
736+
693737
InstanceLightData() {
694738
bake_mode = RS::LIGHT_BAKE_DISABLED;
695-
shadow_dirty = true;
696739
D = nullptr;
697740
last_version = 0;
698741
baked_light = nullptr;
742+
743+
shadow_dirty_count = 1;
744+
light_update_frame_id = UINT32_MAX;
745+
light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX;
746+
light_intersects_multiple_cameras = false;
699747
}
700748
};
701749

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

9571005
LocalVector<Vector2> camera_jitter_array;
1006+
RenderingLightCuller *light_culler = nullptr;
9581007

9591008
virtual RID instance_allocate();
9601009
virtual void instance_initialize(RID p_rid);

0 commit comments

Comments
 (0)