Merge pull request #84745 from lawnjelly/lightcull

Shadow volume culling and tighter shadow caster culling
This commit is contained in:
Rémi Verschelde 2024-01-31 11:13:19 +01:00 committed by GitHub
commit f8a039e9b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1515 additions and 13 deletions

View file

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

View file

@ -229,6 +229,12 @@ public:
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) {

View file

@ -2548,6 +2548,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>

View file

@ -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>
@ -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) {
@ -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) {
@ -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();
}
}
}
@ -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) {
@ -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();
}
}
@ -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;
@ -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)) {
@ -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)) {
@ -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)) {
@ -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;
@ -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());
@ -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) {
@ -3263,9 +3286,31 @@ 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);
@ -3273,10 +3318,14 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c
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();
}
}
}
}
@ -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;
@ -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() {
@ -4187,4 +4242,9 @@ RendererSceneCull::~RendererSceneCull() {
if (dummy_occlusion_culling) {
memdelete(dummy_occlusion_culling);
}
if (light_culler) {
memdelete(light_culler);
light_culler = nullptr;
}
}

View file

@ -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;
@ -679,7 +681,6 @@ public:
uint64_t last_version;
List<Instance *>::Element *D; // directional light in scenario
bool shadow_dirty;
bool uses_projector = false;
bool uses_softshadow = false;
@ -690,12 +691,59 @@ public:
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;
}
};
@ -955,6 +1003,7 @@ public:
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);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,248 @@
/**************************************************************************/
/* rendering_light_culler.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef RENDERING_LIGHT_CULLER_H
#define RENDERING_LIGHT_CULLER_H
#include "core/math/plane.h"
#include "core/math/vector3.h"
#include "renderer_scene_cull.h"
struct Projection;
struct Transform3D;
// For testing performance improvements from the LightCuller:
// Uncomment LIGHT_CULLER_DEBUG_FLASH and it will turn the culler
// on and off every LIGHT_CULLER_DEBUG_FLASH_FREQUENCY camera prepares.
// Uncomment LIGHT_CULLER_DEBUG_LOGGING to get periodic print of the number of casters culled before / after.
// Uncomment LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT to get periodic print of the number of casters culled for the directional light..
// #define LIGHT_CULLER_DEBUG_LOGGING
// #define LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT
// #define LIGHT_CULLER_DEBUG_REGULAR_LIGHT
// #define LIGHT_CULLER_DEBUG_FLASH
#define LIGHT_CULLER_DEBUG_FLASH_FREQUENCY 1024
////////////////////////////////////////////////////////////////////////////////////////////////
// The code to generate the lookup table is included but commented out.
// This may be useful for debugging / regenerating the LUT in the future,
// especially if the order of planes changes.
// When this define is set, the generated lookup table will be printed to debug output.
// The generated lookup table can be copy pasted
// straight to LUT_entry_sizes and LUT_entries.
// See the referenced article for explanation.
// #define RENDERING_LIGHT_CULLER_CALCULATE_LUT
////////////////////////////////////////////////////////////////////////////////////////////////
// This define will be set automatically depending on earlier defines, you can leave this as is.
#if defined(LIGHT_CULLER_DEBUG_LOGGING) || defined(RENDERING_LIGHT_CULLER_CALCULATE_LUT)
#define RENDERING_LIGHT_CULLER_DEBUG_STRINGS
#endif
// Culls shadow casters that can't cast shadows into the camera frustum.
class RenderingLightCuller {
public:
RenderingLightCuller();
private:
class LightSource {
public:
enum SourceType {
ST_UNKNOWN,
ST_DIRECTIONAL,
ST_SPOTLIGHT,
ST_OMNI,
};
LightSource() {
type = ST_UNKNOWN;
angle = 0.0f;
range = FLT_MAX;
}
// All in world space, culling done in world space.
Vector3 pos;
Vector3 dir;
SourceType type;
float angle; // For spotlight.
float range;
};
// Same order as godot.
enum PlaneOrder {
PLANE_NEAR,
PLANE_FAR,
PLANE_LEFT,
PLANE_TOP,
PLANE_RIGHT,
PLANE_BOTTOM,
PLANE_TOTAL,
};
// Same order as godot.
enum PointOrder {
PT_FAR_LEFT_TOP,
PT_FAR_LEFT_BOTTOM,
PT_FAR_RIGHT_TOP,
PT_FAR_RIGHT_BOTTOM,
PT_NEAR_LEFT_TOP,
PT_NEAR_LEFT_BOTTOM,
PT_NEAR_RIGHT_TOP,
PT_NEAR_RIGHT_BOTTOM,
};
// 6 bits, 6 planes.
enum {
NUM_CAM_PLANES = 6,
NUM_CAM_POINTS = 8,
MAX_CULL_PLANES = 17,
LUT_SIZE = 64,
};
public:
// Before each pass with a different camera, you must call this so the culler can pre-create
// the camera frustum planes and corner points in world space which are used for the culling.
bool prepare_camera(const Transform3D &p_cam_transform, const Projection &p_cam_matrix);
// REGULAR LIGHTS (SPOT, OMNI).
// These are prepared then used for culling one by one, single threaded.
// prepare_regular_light() returns false if the entire light is culled (i.e. there is no intersection between the light and the view frustum).
bool prepare_regular_light(const RendererSceneCull::Instance &p_instance) { return _prepare_light(p_instance, -1); }
// Cull according to the regular light planes that were setup in the previous call to prepare_regular_light.
void cull_regular_light(PagedArray<RendererSceneCull::Instance *> &r_instance_shadow_cull_result);
// Directional lights are prepared in advance, and can be culled multithreaded chopping and changing between
// different directional_light_id.
void prepare_directional_light(const RendererSceneCull::Instance *p_instance, int32_t p_directional_light_id);
// Return false if the instance is to be culled.
bool cull_directional_light(const RendererSceneCull::InstanceBounds &p_bound, int32_t p_directional_light_id);
// Can turn on and off from the engine if desired.
void set_caster_culling_active(bool p_active) { data.caster_culling_active = p_active; }
void set_light_culling_active(bool p_active) { data.light_culling_active = p_active; }
private:
struct LightCullPlanes {
void add_cull_plane(const Plane &p);
Plane cull_planes[MAX_CULL_PLANES];
int num_cull_planes = 0;
#ifdef LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT
uint32_t rejected_count = 0;
#endif
};
bool _prepare_light(const RendererSceneCull::Instance &p_instance, int32_t p_directional_light_id = -1);
// Internal version uses LightSource.
bool _add_light_camera_planes(LightCullPlanes &r_cull_planes, const LightSource &p_light_source);
// Directional light gives parallel culling planes (as opposed to point lights).
bool add_light_camera_planes_directional(LightCullPlanes &r_cull_planes, const LightSource &p_light_source);
// Is the light culler active? maybe not in the editor...
bool is_caster_culling_active() const { return data.caster_culling_active; }
bool is_light_culling_active() const { return data.light_culling_active; }
// Do we want to log some debug output?
bool is_logging() const { return data.debug_count == 0; }
struct Data {
// Camera frustum planes (world space) - order ePlane.
Vector<Plane> frustum_planes;
// Camera frustum corners (world space) - order ePoint.
Vector3 frustum_points[NUM_CAM_POINTS];
// Master can have multiple directional lights.
// These need to store their own cull planes individually, as master
// chops and changes between culling different lights
// instead of doing one by one, and we don't want to prepare
// lights multiple times per frame.
LocalVector<LightCullPlanes> directional_cull_planes;
// Single threaded cull planes for regular lights
// (OMNI, SPOT). These lights reuse the same set of cull plane data.
LightCullPlanes regular_cull_planes;
#ifdef LIGHT_CULLER_DEBUG_REGULAR_LIGHT
uint32_t regular_rejected_count = 0;
#endif
// The whole regular light can be out of range of the view frustum, in which case all casters should be culled.
bool out_of_range = false;
#ifdef RENDERING_LIGHT_CULLER_DEBUG_STRINGS
static String plane_bitfield_to_string(unsigned int BF);
// Names of the plane and point enums, useful for debugging.
static const char *string_planes[];
static const char *string_points[];
#endif
// Precalculated look up table.
static uint8_t LUT_entry_sizes[LUT_SIZE];
static uint8_t LUT_entries[LUT_SIZE][8];
bool caster_culling_active = true;
bool light_culling_active = true;
// Light culling is a basic on / off switch.
// Caster culling only works if light culling is also on.
bool is_active() const { return light_culling_active; }
// Ideally a frame counter, but for ease of implementation
// this is just incremented on each prepare_camera.
// used to turn on and off debugging features.
int debug_count = -1;
} data;
// This functionality is not required in general use (and is compiled out),
// as the lookup table can normally be hard coded
// (provided order of planes etc does not change).
// It is provided for debugging / future maintenance.
#ifdef RENDERING_LIGHT_CULLER_CALCULATE_LUT
void get_neighbouring_planes(PlaneOrder p_plane, PlaneOrder r_neigh_planes[4]) const;
void get_corners_of_planes(PlaneOrder p_plane_a, PlaneOrder p_plane_b, PointOrder r_points[2]) const;
void create_LUT();
void compact_LUT_entry(uint32_t p_entry_id);
void debug_print_LUT();
void debug_print_LUT_as_table();
void add_LUT(int p_plane_0, int p_plane_1, PointOrder p_pts[2]);
void add_LUT_entry(uint32_t p_entry_id, PointOrder p_pts[2]);
String debug_string_LUT_entry(const LocalVector<uint8_t> &p_entry, bool p_pair = false);
String string_LUT_entry(const LocalVector<uint8_t> &p_entry);
// Contains a list of points for each combination of plane facing directions.
LocalVector<uint8_t> _calculated_LUT[LUT_SIZE];
#endif
};
#endif // RENDERING_LIGHT_CULLER_H