diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index fe7572139c8..25630233a0b 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -1800,6 +1800,11 @@
Lower-end override for [member rendering/quality/shadow_atlas/size] on mobile devices, due to performance concerns or driver support.
+
+ If [code]true[/code], items that cannot cast shadows into the view frustum will not be rendered into shadow maps.
+ This can increase performance.
+ [b]Note:[/b] This setting only takes effect when [member rendering/quality/shadows/light_culling] is also [code]true[/code].
+
Shadow filter mode. Higher-quality settings result in smoother shadows that flicker less when moving. "Disabled" is the fastest option, but also has the lowest quality. "PCF5" is smoother but is also slower. "PCF13" is the smoothest option, but is also the slowest.
[b]Note:[/b] When using the GLES2 backend, the "PCF13" option actually uses 16 samples to emulate linear filtering in the shader. This results in a shadow appearance similar to the one produced by the GLES3 backend.
@@ -1807,6 +1812,10 @@
Lower-end override for [member rendering/quality/shadows/filter_mode] on mobile devices, due to performance concerns or driver support.
+
+ If [code]true[/code], prevents shadows from rendering for lights that do not intersect the view frustum.
+ This can increase performance.
+
Forces [MeshInstance] to always perform skinning on the CPU (applies to both GLES2 and GLES3).
See also [member rendering/quality/skinning/software_skinning_fallback].
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index ca098afd2d5..e7a85c44929 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -721,9 +721,7 @@ bool SceneTree::idle(float p_time) {
#endif
- if (_physics_interpolation_enabled) {
- VisualServer::get_singleton()->pre_draw(true);
- }
+ VisualServer::get_singleton()->pre_draw(true);
return _quit;
}
diff --git a/servers/visual/visual_server_light_culler.cpp b/servers/visual/visual_server_light_culler.cpp
new file mode 100644
index 00000000000..e88d560b7ac
--- /dev/null
+++ b/servers/visual/visual_server_light_culler.cpp
@@ -0,0 +1,1052 @@
+/**************************************************************************/
+/* visual_server_light_culler.cpp */
+/**************************************************************************/
+/* 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. */
+/**************************************************************************/
+
+#include "visual_server_light_culler.h"
+#include "core/math/camera_matrix.h"
+#include "core/math/plane.h"
+#include "scene/3d/camera.h"
+#include "visual_server_globals.h"
+#include "visual_server_scene.h"
+
+#ifdef VISUAL_SERVER_LIGHT_CULLER_DEBUG_STRINGS
+const char *VisualServerLightCuller::Data::string_planes[] = {
+ "NEAR",
+ "FAR",
+ "LEFT",
+ "TOP",
+ "RIGHT",
+ "BOTTOM",
+};
+const char *VisualServerLightCuller::Data::string_points[] = {
+ "FAR_LEFT_TOP",
+ "FAR_LEFT_BOTTOM",
+ "FAR_RIGHT_TOP",
+ "FAR_RIGHT_BOTTOM",
+ "NEAR_LEFT_TOP",
+ "NEAR_LEFT_BOTTOM",
+ "NEAR_RIGHT_TOP",
+ "NEAR_RIGHT_BOTTOM",
+};
+
+String VisualServerLightCuller::Data::plane_bitfield_to_string(unsigned int BF) {
+ String sz;
+
+ for (int n = 0; n < 6; n++) {
+ unsigned int bit = 1 << n;
+ if (BF & bit) {
+ sz += String(string_planes[n]) + ", ";
+ }
+ }
+
+ return sz;
+}
+#endif
+
+bool VisualServerLightCuller::prepare_light(const VisualServerScene::Instance &p_instance) {
+ if (!data.is_active()) {
+ return true;
+ }
+
+ LightSource lsource;
+ switch (VSG::storage->light_get_type(p_instance.base)) {
+ case VS::LIGHT_SPOT:
+ lsource.type = LightSource::ST_SPOTLIGHT;
+ lsource.angle = VSG::storage->light_get_param(p_instance.base, VS::LIGHT_PARAM_SPOT_ANGLE);
+ lsource.range = VSG::storage->light_get_param(p_instance.base, VS::LIGHT_PARAM_RANGE);
+ break;
+ case VS::LIGHT_OMNI:
+ lsource.type = LightSource::ST_OMNI;
+ lsource.range = VSG::storage->light_get_param(p_instance.base, VS::LIGHT_PARAM_RANGE);
+ break;
+ case VS::LIGHT_DIRECTIONAL:
+ lsource.type = LightSource::ST_DIRECTIONAL;
+ // Could deal with a max directional shadow range here? NYI
+ // LIGHT_PARAM_SHADOW_MAX_DISTANCE
+ break;
+ }
+
+ lsource.pos = p_instance.transform.origin;
+ lsource.dir = -p_instance.transform.basis.get_axis(2);
+ lsource.dir.normalize();
+
+ bool visible = _add_light_camera_planes(lsource);
+
+ if (data.light_culling_active) {
+ return visible;
+ }
+ return true;
+}
+
+int VisualServerLightCuller::cull(int p_count, VisualServerScene::Instance **p_result_array) {
+ if (!data.is_active() || !is_caster_culling_active()) {
+ return p_count;
+ }
+
+ // If the light is out of range, no need to check anything, just return 0 casters.
+ // Ideally an out of range light should not even be drawn AT ALL (no shadow map, no PCF etc).
+ if (data.out_of_range) {
+ return 0;
+ }
+
+ int new_count = p_count;
+
+ // Go through all the casters in the list (the list will hopefully shrink as we go).
+ for (int n = 0; n < new_count; n++) {
+ // World space aabb.
+ const AABB &bb = p_result_array[n]->transformed_aabb;
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ print_line("bb : " + String(bb));
+ }
+#endif
+
+ float r_min, r_max;
+ bool show = true;
+
+ for (int p = 0; p < data.num_cull_planes; p++) {
+ // As we only need r_min, could this be optimized?
+ bb.project_range_in_plane(data.cull_planes[p], r_min, r_max);
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ print_line("\tplane " + itos(p) + " : " + String(data.cull_planes[p]) + " r_min " + String(Variant(r_min)) + " r_max " + String(Variant(r_max)));
+ }
+#endif
+
+ if (r_min > 0.0f) {
+ show = false;
+ break;
+ }
+ }
+
+ // Remove.
+ if (!show) {
+ // Quick unsorted remove - swap last element and reduce count.
+ p_result_array[n] = p_result_array[new_count - 1];
+ new_count--;
+
+ // Repeat this element next iteration of the loop as it has been removed and replaced by the last.
+ n--;
+ }
+ }
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ int removed = p_count - new_count;
+ if (removed) {
+ if (((data.debug_count) % 60) == 0) {
+ print_line("[" + itos(data.debug_count) + "] linear cull before " + itos(p_count) + " after " + itos(new_count));
+ }
+ }
+#endif
+
+ return new_count;
+}
+
+void VisualServerLightCuller::add_cull_plane(const Plane &p) {
+ ERR_FAIL_COND(data.num_cull_planes >= MAX_CULL_PLANES);
+ data.cull_planes[data.num_cull_planes++] = p;
+}
+
+// Directional lights are different to points, as the origin is infinitely in the distance, so the plane third
+// points are derived differently.
+bool VisualServerLightCuller::add_light_camera_planes_directional(const LightSource &p_light_source) {
+ uint32_t lookup = 0;
+
+ // Directional light, we will use dot against the light direction to determine back facing planes.
+ for (int n = 0; n < 6; n++) {
+ float dot = data.frustum_planes[n].normal.dot(p_light_source.dir);
+ if (dot > 0.0f) {
+ lookup |= 1 << n;
+
+ // Add backfacing camera frustum planes.
+ add_cull_plane(data.frustum_planes[n]);
+ }
+ }
+
+ ERR_FAIL_COND_V(lookup >= LUT_SIZE, true);
+
+ // Deal with special case... if the light is INSIDE the view frustum (i.e. all planes face away)
+ // then we will add the camera frustum planes to clip the light volume .. there is no need to
+ // render shadow casters outside the frustum as shadows can never re-enter the frustum.
+
+ // Should never happen with directional light?? This may be able to be removed.
+ if (lookup == 63) {
+ data.num_cull_planes = 0;
+ for (int n = 0; n < data.frustum_planes.size(); n++) {
+ add_cull_plane(data.frustum_planes[n]);
+ }
+
+ return true;
+ }
+
+// Each edge forms a plane.
+#ifdef VISUAL_SERVER_LIGHT_CULLER_CALCULATE_LUT
+ const LocalVector &entry = _calculated_LUT[lookup];
+
+ // each edge forms a plane
+ int n_edges = entry.size() - 1;
+#else
+ uint8_t *entry = &data.LUT_entries[lookup][0];
+ int n_edges = data.LUT_entry_sizes[lookup] - 1;
+#endif
+
+ for (int e = 0; e < n_edges; e++) {
+ int i0 = entry[e];
+ int i1 = entry[e + 1];
+ const Vector3 &pt0 = data.frustum_points[i0];
+ const Vector3 &pt1 = data.frustum_points[i1];
+
+ // Create a third point from the light direction.
+ Vector3 pt2 = pt0 - p_light_source.dir;
+
+ // Create plane from 3 points.
+ Plane p(pt0, pt1, pt2);
+ add_cull_plane(p);
+ }
+
+ // Last to 0 edge.
+ if (n_edges) {
+ int i0 = entry[n_edges]; // Last.
+ int i1 = entry[0]; // First.
+
+ const Vector3 &pt0 = data.frustum_points[i0];
+ const Vector3 &pt1 = data.frustum_points[i1];
+
+ // Create a third point from the light direction.
+ Vector3 pt2 = pt0 - p_light_source.dir;
+
+ // Create plane from 3 points.
+ Plane p(pt0, pt1, pt2);
+ add_cull_plane(p);
+ }
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ print_line("lcam.pos is " + String(p_light_source.pos));
+ }
+#endif
+
+ return true;
+}
+
+bool VisualServerLightCuller::_add_light_camera_planes(const LightSource &p_light_source) {
+ if (!data.is_active()) {
+ return true;
+ }
+
+ // We should have called prepare_camera before this.
+ ERR_FAIL_COND_V(data.frustum_planes.size() != 6, true);
+
+ // Start with 0 cull planes.
+ data.num_cull_planes = 0;
+ data.out_of_range = false;
+
+ switch (p_light_source.type) {
+ case LightSource::ST_SPOTLIGHT:
+ case LightSource::ST_OMNI:
+ break;
+ case LightSource::ST_DIRECTIONAL:
+ return add_light_camera_planes_directional(p_light_source);
+ break;
+ default:
+ return false; // not yet supported
+ break;
+ }
+
+ uint32_t lookup = 0;
+
+ // Find which of the camera planes are facing away from the light.
+ // We can also test for the situation where the light max range means it cannot
+ // affect the camera frustum. This is absolutely worth doing because it is relatively
+ // cheap, and if the entire light can be culled this can vastly improve performance
+ // (much more than just culling casters).
+
+ // POINT LIGHT (spotlight, omni)
+ // Instead of using dot product to compare light direction to plane, we can simply
+ // find out which side of the plane the camera is on. By definition this marks the point at which the plane
+ // becomes invisible.
+
+ // OMNIS
+ if (p_light_source.type == LightSource::ST_OMNI) {
+ for (int n = 0; n < 6; n++) {
+ float dist = data.frustum_planes[n].distance_to(p_light_source.pos);
+ if (dist < 0.0f) {
+ lookup |= 1 << n;
+
+ // Add backfacing camera frustum planes.
+ add_cull_plane(data.frustum_planes[n]);
+ } else {
+ // Is the light out of range?
+ // This is one of the tests. If the point source is more than range distance from a frustum plane, it can't
+ // be seen.
+ if (dist >= p_light_source.range) {
+ // If the light is out of range, no need to do anything else, everything will be culled.
+ data.out_of_range = true;
+ return false;
+ }
+ }
+ }
+ } else {
+ // SPOTLIGHTs, more complex to cull.
+ Vector3 pos_end = p_light_source.pos + (p_light_source.dir * p_light_source.range);
+
+ // This is the radius of the cone at distance 1.
+ float radius_at_dist_one = Math::tan(Math::deg2rad(p_light_source.angle));
+
+ // The worst case radius of the cone at the end point can be calculated
+ // (the radius will scale linearly with length along the cone).
+ float end_cone_radius = radius_at_dist_one * p_light_source.range;
+
+ for (int n = 0; n < 6; n++) {
+ float dist = data.frustum_planes[n].distance_to(p_light_source.pos);
+ if (dist < 0.0f) {
+ // Either the plane is backfacing or we are inside the frustum.
+ lookup |= 1 << n;
+
+ // Add backfacing camera frustum planes.
+ add_cull_plane(data.frustum_planes[n]);
+ } else {
+ // The light is in front of the plane.
+
+ // Is the light out of range?
+ if (dist >= p_light_source.range) {
+ data.out_of_range = true;
+ return false;
+ }
+
+ // For a spotlight, we can use an extra test
+ // at this point the cone start is in front of the plane...
+ // If the cone end point is further than the maximum possible distance to the plane
+ // we can guarantee that the cone does not cross the plane, and hence the cone
+ // is outside the frustum.
+ float dist_end = data.frustum_planes[n].distance_to(pos_end);
+
+ if (dist_end >= end_cone_radius) {
+ data.out_of_range = true;
+ return false;
+ }
+ }
+ }
+ }
+
+ // The lookup should be within the LUT, logic should prevent this.
+ ERR_FAIL_COND_V(lookup >= LUT_SIZE, true);
+
+ // Deal with special case... if the light is INSIDE the view frustum (i.e. all planes face away)
+ // then we will add the camera frustum planes to clip the light volume .. there is no need to
+ // render shadow casters outside the frustum as shadows can never re-enter the frustum.
+ if (lookup == 63) {
+ data.num_cull_planes = 0;
+ for (int n = 0; n < data.frustum_planes.size(); n++) {
+ add_cull_plane(data.frustum_planes[n]);
+ }
+
+ return true;
+ }
+
+ // Each edge forms a plane.
+ uint8_t *entry = &data.LUT_entries[lookup][0];
+ int n_edges = data.LUT_entry_sizes[lookup] - 1;
+
+ for (int e = 0; e < n_edges; e++) {
+ int i0 = entry[e];
+ int i1 = entry[e + 1];
+ const Vector3 &pt0 = data.frustum_points[i0];
+ const Vector3 &pt1 = data.frustum_points[i1];
+
+ // Create plane from 3 points.
+ Plane p(pt0, pt1, p_light_source.pos);
+ add_cull_plane(p);
+ }
+
+ // Last to 0 edge.
+ if (n_edges) {
+ int i0 = entry[n_edges]; // Last.
+ int i1 = entry[0]; // First.
+
+ const Vector3 &pt0 = data.frustum_points[i0];
+ const Vector3 &pt1 = data.frustum_points[i1];
+
+ // Create plane from 3 points.
+ Plane p(pt0, pt1, p_light_source.pos);
+ add_cull_plane(p);
+ }
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ print_line("lsource.pos is " + String(p_light_source.pos));
+ }
+#endif
+
+ return true;
+}
+
+bool VisualServerLightCuller::prepare_camera(const Transform &p_cam_transform, const CameraMatrix &p_cam_matrix) {
+ data.debug_count++;
+
+ // For debug flash off and on.
+#ifdef LIGHT_CULLER_DEBUG_FLASH
+ if (!Engine::get_singleton()->is_editor_hint()) {
+ int dc = data.debug_count / LIGHT_CULLER_DEBUG_FLASH_FREQUENCY;
+ bool bnew_active;
+ bnew_active = (dc % 2) == 0;
+
+ if (bnew_active != data.active) {
+ data.active = bnew_active;
+ print_line("switching light culler " + String(Variant(data.active)));
+ }
+ }
+#endif
+
+ if (!data.is_active()) {
+ return false;
+ }
+
+ // Get the camera frustum planes in world space.
+ data.frustum_planes = p_cam_matrix.get_projection_planes(p_cam_transform);
+
+ data.num_cull_planes = 0;
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ for (int p = 0; p < 6; p++) {
+ print_line("plane " + itos(p) + " : " + String(data.frustum_planes[p]));
+ }
+ }
+#endif
+
+ // We want to calculate the frustum corners in a specific order.
+ const CameraMatrix::Planes intersections[8][3] = {
+ { CameraMatrix::PLANE_FAR, CameraMatrix::PLANE_LEFT, CameraMatrix::PLANE_TOP },
+ { CameraMatrix::PLANE_FAR, CameraMatrix::PLANE_LEFT, CameraMatrix::PLANE_BOTTOM },
+ { CameraMatrix::PLANE_FAR, CameraMatrix::PLANE_RIGHT, CameraMatrix::PLANE_TOP },
+ { CameraMatrix::PLANE_FAR, CameraMatrix::PLANE_RIGHT, CameraMatrix::PLANE_BOTTOM },
+ { CameraMatrix::PLANE_NEAR, CameraMatrix::PLANE_LEFT, CameraMatrix::PLANE_TOP },
+ { CameraMatrix::PLANE_NEAR, CameraMatrix::PLANE_LEFT, CameraMatrix::PLANE_BOTTOM },
+ { CameraMatrix::PLANE_NEAR, CameraMatrix::PLANE_RIGHT, CameraMatrix::PLANE_TOP },
+ { CameraMatrix::PLANE_NEAR, CameraMatrix::PLANE_RIGHT, CameraMatrix::PLANE_BOTTOM },
+ };
+
+ for (int i = 0; i < 8; i++) {
+ // 3 plane intersection, gives us a point.
+ bool res = data.frustum_planes[intersections[i][0]].intersect_3(data.frustum_planes[intersections[i][1]], data.frustum_planes[intersections[i][2]], &data.frustum_points[i]);
+
+ // What happens with a zero frustum? NYI - deal with this.
+ ERR_FAIL_COND_V(!res, false);
+
+#ifdef LIGHT_CULLER_DEBUG_LOGGING
+ if (is_logging()) {
+ print_line("point " + itos(i) + " -> " + String(data.frustum_points[i]));
+ }
+#endif
+ }
+
+ return true;
+}
+
+VisualServerLightCuller::VisualServerLightCuller() {
+ // Used to determine which frame to give debug output.
+ data.debug_count = -1;
+
+ // bactive is switching on and off the light culler
+ data.caster_culling_active = Engine::get_singleton()->is_editor_hint() == false;
+
+#ifdef VISUAL_SERVER_LIGHT_CULLER_CALCULATE_LUT
+ create_LUT();
+#endif
+}
+
+/* clang-format off */
+uint8_t VisualServerLightCuller::Data::LUT_entry_sizes[LUT_SIZE] = {0, 4, 4, 0, 4, 6, 6, 8, 4, 6, 6, 8, 6, 6, 6, 6, 4, 6, 6, 8, 0, 8, 8, 0, 6, 6, 6, 6, 8, 6, 6, 4, 4, 6, 6, 8, 6, 6, 6, 6, 0, 8, 8, 0, 8, 6, 6, 4, 6, 6, 6, 6, 8, 6, 6, 4, 8, 6, 6, 4, 0, 4, 4, 0, };
+
+// The lookup table used to determine which edges form the silhouette of the camera frustum,
+// depending on the viewing angle (defined by which camera planes are backward facing).
+uint8_t VisualServerLightCuller::Data::LUT_entries[LUT_SIZE][8] = {
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{7, 6, 4, 5, 0, 0, 0, 0, },
+{1, 0, 2, 3, 0, 0, 0, 0, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{1, 5, 4, 0, 0, 0, 0, 0, },
+{1, 5, 7, 6, 4, 0, 0, 0, },
+{4, 0, 2, 3, 1, 5, 0, 0, },
+{5, 7, 6, 4, 0, 2, 3, 1, },
+{0, 4, 6, 2, 0, 0, 0, 0, },
+{0, 4, 5, 7, 6, 2, 0, 0, },
+{6, 2, 3, 1, 0, 4, 0, 0, },
+{2, 3, 1, 0, 4, 5, 7, 6, },
+{0, 1, 5, 4, 6, 2, 0, 0, },
+{0, 1, 5, 7, 6, 2, 0, 0, },
+{6, 2, 3, 1, 5, 4, 0, 0, },
+{2, 3, 1, 5, 7, 6, 0, 0, },
+{2, 6, 7, 3, 0, 0, 0, 0, },
+{2, 6, 4, 5, 7, 3, 0, 0, },
+{7, 3, 1, 0, 2, 6, 0, 0, },
+{3, 1, 0, 2, 6, 4, 5, 7, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{2, 6, 4, 0, 1, 5, 7, 3, },
+{7, 3, 1, 5, 4, 0, 2, 6, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{2, 0, 4, 6, 7, 3, 0, 0, },
+{2, 0, 4, 5, 7, 3, 0, 0, },
+{7, 3, 1, 0, 4, 6, 0, 0, },
+{3, 1, 0, 4, 5, 7, 0, 0, },
+{2, 0, 1, 5, 4, 6, 7, 3, },
+{2, 0, 1, 5, 7, 3, 0, 0, },
+{7, 3, 1, 5, 4, 6, 0, 0, },
+{3, 1, 5, 7, 0, 0, 0, 0, },
+{3, 7, 5, 1, 0, 0, 0, 0, },
+{3, 7, 6, 4, 5, 1, 0, 0, },
+{5, 1, 0, 2, 3, 7, 0, 0, },
+{7, 6, 4, 5, 1, 0, 2, 3, },
+{3, 7, 5, 4, 0, 1, 0, 0, },
+{3, 7, 6, 4, 0, 1, 0, 0, },
+{5, 4, 0, 2, 3, 7, 0, 0, },
+{7, 6, 4, 0, 2, 3, 0, 0, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{3, 7, 6, 2, 0, 4, 5, 1, },
+{5, 1, 0, 4, 6, 2, 3, 7, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{3, 7, 5, 4, 6, 2, 0, 1, },
+{3, 7, 6, 2, 0, 1, 0, 0, },
+{5, 4, 6, 2, 3, 7, 0, 0, },
+{7, 6, 2, 3, 0, 0, 0, 0, },
+{3, 2, 6, 7, 5, 1, 0, 0, },
+{3, 2, 6, 4, 5, 1, 0, 0, },
+{5, 1, 0, 2, 6, 7, 0, 0, },
+{1, 0, 2, 6, 4, 5, 0, 0, },
+{3, 2, 6, 7, 5, 4, 0, 1, },
+{3, 2, 6, 4, 0, 1, 0, 0, },
+{5, 4, 0, 2, 6, 7, 0, 0, },
+{6, 4, 0, 2, 0, 0, 0, 0, },
+{3, 2, 0, 4, 6, 7, 5, 1, },
+{3, 2, 0, 4, 5, 1, 0, 0, },
+{5, 1, 0, 4, 6, 7, 0, 0, },
+{1, 0, 4, 5, 0, 0, 0, 0, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+{3, 2, 0, 1, 0, 0, 0, 0, },
+{5, 4, 6, 7, 0, 0, 0, 0, },
+{0, 0, 0, 0, 0, 0, 0, 0, },
+};
+
+/* clang-format on */
+
+#ifdef VISUAL_SERVER_LIGHT_CULLER_CALCULATE_LUT
+
+// See e.g. http://lspiroengine.com/?p=153 for reference.
+// Principles are the same, but differences to the article:
+// * Order of planes / points is different in Godot.
+// * We use a lookup table at runtime.
+void VisualServerLightCuller::create_LUT() {
+ // Each pair of planes that are opposite can have an edge.
+ for (int plane_0 = 0; plane_0 < PLANE_TOTAL; plane_0++) {
+ // For each neighbour of the plane.
+ PlaneOrder neighs[4];
+ get_neighbouring_planes((PlaneOrder)plane_0, neighs);
+
+ for (int n = 0; n < 4; n++) {
+ int plane_1 = neighs[n];
+
+ // If these are opposite we need to add the 2 points they share.
+ PointOrder pts[2];
+ get_corners_of_planes((PlaneOrder)plane_0, (PlaneOrder)plane_1, pts);
+
+ add_LUT(plane_0, plane_1, pts);
+ }
+ }
+
+ for (uint32_t n = 0; n < LUT_SIZE; n++) {
+ compact_LUT_entry(n);
+ }
+
+ debug_print_LUT();
+ debug_print_LUT_as_table();
+}
+
+// we can pre-create the entire LUT and store it hard coded as a static inside the executable!
+// it is only small in size, 64 entries with max 8 bytes per entry
+void VisualServerLightCuller::debug_print_LUT_as_table() {
+ print_line("\nLIGHT VOLUME TABLE BEGIN\n");
+
+ print_line("Copy this to LUT_entry_sizes:\n");
+ String sz = "{";
+ for (int n = 0; n < LUT_SIZE; n++) {
+ const LocalVector &entry = _calculated_LUT[n];
+
+ sz += itos(entry.size()) + ", ";
+ }
+ sz += "}";
+ print_line(sz);
+ print_line("\nCopy this to LUT_entries:\n");
+
+ for (int n = 0; n < LUT_SIZE; n++) {
+ const LocalVector &entry = _calculated_LUT[n];
+
+ String sz = "{";
+
+ // First is the number of points in the entry.
+ int s = entry.size();
+
+ for (int p = 0; p < 8; p++) {
+ if (p < s)
+ sz += itos(entry[p]);
+ else
+ sz += "0"; // just a spacer
+
+ sz += ", ";
+ }
+
+ sz += "},";
+ print_line(sz);
+ }
+
+ print_line("\nLIGHT VOLUME TABLE END\n");
+}
+
+void VisualServerLightCuller::debug_print_LUT() {
+ for (uint32_t n = 0; n < LUT_SIZE; n++) {
+ String sz;
+ sz = "LUT" + itos(n) + ":\t";
+
+ sz += Data::plane_bitfield_to_string(n);
+ print_line(sz);
+
+ const LocalVector &entry = _calculated_LUT[n];
+
+ sz = "\t" + string_LUT_entry(entry);
+
+ print_line(sz);
+ }
+}
+
+String VisualServerLightCuller::string_LUT_entry(const LocalVector &p_entry) {
+ String string;
+
+ for (uint32_t n = 0; n < p_entry.size(); n++) {
+ uint8_t val = p_entry[n];
+ DEV_ASSERT(val < 8);
+ const char *sz_point = Data::string_points[val];
+ string += sz_point;
+ string += ", ";
+ }
+
+ return string;
+}
+
+String VisualServerLightCuller::debug_string_LUT_entry(const LocalVector &p_entry, bool p_pair) {
+ String string;
+
+ for (int i = 0; i < p_entry.size(); i++) {
+ int pt_order = p_entry[i];
+
+ if (p_pair && ((i % 2) == 0)) {
+ string += itos(pt_order) + "-";
+ } else {
+ string += itos(pt_order) + ", ";
+ }
+ }
+
+ return string;
+}
+
+void VisualServerLightCuller::add_LUT(int p_plane_0, int p_plane_1, PointOrder p_pts[2]) {
+ uint32_t bit0 = 1 << p_plane_0;
+ uint32_t bit1 = 1 << p_plane_1;
+
+ // All entries of the LUT that have plane 0 set and plane 1 not set.
+ for (uint32_t n = 0; n < 64; n++) {
+ // If bit0 not set...
+ if (!(n & bit0))
+ continue;
+
+ // If bit1 set...
+ if (n & bit1)
+ continue;
+
+ // Meets criteria.
+ add_LUT_entry(n, p_pts);
+ }
+}
+
+void VisualServerLightCuller::add_LUT_entry(uint32_t p_entry_id, PointOrder p_pts[2]) {
+ DEV_ASSERT(p_entry_id < LUT_SIZE);
+ LocalVector &entry = _calculated_LUT[p_entry_id];
+
+ entry.push_back(p_pts[0]);
+ entry.push_back(p_pts[1]);
+}
+
+void VisualServerLightCuller::compact_LUT_entry(uint32_t p_entry_id) {
+ DEV_ASSERT(p_entry_id < LUT_SIZE);
+ LocalVector &entry = _calculated_LUT[p_entry_id];
+
+ int num_pairs = entry.size() / 2;
+
+ if (num_pairs == 0)
+ return;
+
+ LocalVector temp;
+
+ String string;
+ string = "Compact LUT" + itos(p_entry_id) + ":\t";
+ string += debug_string_LUT_entry(entry, true);
+ print_line(string);
+
+ // Add first pair.
+ temp.push_back(entry[0]);
+ temp.push_back(entry[1]);
+ unsigned int BFpairs = 1;
+
+ string = debug_string_LUT_entry(temp) + " -> ";
+ print_line(string);
+
+ // Attempt to add a pair each time.
+ for (int done = 1; done < num_pairs; done++) {
+ string = "done " + itos(done) + ": ";
+ // Find a free pair.
+ for (int p = 1; p < num_pairs; p++) {
+ unsigned int bit = 1 << p;
+ // Is it done already?
+ if (BFpairs & bit)
+ continue;
+
+ // There must be at least 1 free pair.
+ // Attempt to add.
+ int a = entry[p * 2];
+ int b = entry[(p * 2) + 1];
+
+ string += "[" + itos(a) + "-" + itos(b) + "], ";
+
+ int found_a = temp.find(a);
+ int found_b = temp.find(b);
+
+ // Special case, if they are both already in the list, no need to add
+ // as this is a link from the tail to the head of the list.
+ if ((found_a != -1) && (found_b != -1)) {
+ string += "foundAB link " + itos(found_a) + ", " + itos(found_b) + " ";
+ BFpairs |= bit;
+ goto found;
+ }
+
+ // Find a.
+ if (found_a != -1) {
+ string += "foundA " + itos(found_a) + " ";
+ temp.insert(found_a + 1, b);
+ BFpairs |= bit;
+ goto found;
+ }
+
+ // Find b.
+ if (found_b != -1) {
+ string += "foundB " + itos(found_b) + " ";
+ temp.insert(found_b, a);
+ BFpairs |= bit;
+ goto found;
+ }
+
+ } // Check each pair for adding.
+
+ // If we got here before finding a link, the whole set of planes is INVALID
+ // e.g. far and near plane only, does not create continuous sillouhette of edges.
+ print_line("\tINVALID");
+ entry.clear();
+ return;
+
+ found:;
+ print_line(string);
+ string = "\ttemp now : " + debug_string_LUT_entry(temp);
+ print_line(string);
+ }
+
+ // temp should now be the sorted entry .. delete the old one and replace by temp.
+ entry.clear();
+ entry = temp;
+}
+
+void VisualServerLightCuller::get_neighbouring_planes(PlaneOrder p_plane, PlaneOrder r_neigh_planes[4]) const {
+ // Table of neighbouring planes to each.
+ static const PlaneOrder neigh_table[PLANE_TOTAL][4] = {
+ { // LSM_FP_NEAR
+ PLANE_LEFT,
+ PLANE_RIGHT,
+ PLANE_TOP,
+ PLANE_BOTTOM },
+ { // LSM_FP_FAR
+ PLANE_LEFT,
+ PLANE_RIGHT,
+ PLANE_TOP,
+ PLANE_BOTTOM },
+ { // LSM_FP_LEFT
+ PLANE_TOP,
+ PLANE_BOTTOM,
+ PLANE_NEAR,
+ PLANE_FAR },
+ { // LSM_FP_TOP
+ PLANE_LEFT,
+ PLANE_RIGHT,
+ PLANE_NEAR,
+ PLANE_FAR },
+ { // LSM_FP_RIGHT
+ PLANE_TOP,
+ PLANE_BOTTOM,
+ PLANE_NEAR,
+ PLANE_FAR },
+ { // LSM_FP_BOTTOM
+ PLANE_LEFT,
+ PLANE_RIGHT,
+ PLANE_NEAR,
+ PLANE_FAR },
+ };
+
+ for (int n = 0; n < 4; n++) {
+ r_neigh_planes[n] = neigh_table[p_plane][n];
+ }
+}
+
+// Given two planes, returns the two points shared by those planes. The points are always
+// returned in counter-clockwise order, assuming the first input plane is facing towards
+// the viewer.
+
+// param p_plane_a The plane facing towards the viewer.
+// param p_plane_b A plane neighboring p_plane_a.
+// param r_points An array of exactly two elements to be filled with the indices of the points
+// on return.
+
+void VisualServerLightCuller::get_corners_of_planes(PlaneOrder p_plane_a, PlaneOrder p_plane_b, PointOrder r_points[2]) const {
+ static const PointOrder fp_table[PLANE_TOTAL][PLANE_TOTAL][2] = {
+ {
+ // LSM_FP_NEAR
+ {
+ // LSM_FP_NEAR
+ PT_NEAR_LEFT_TOP, PT_NEAR_RIGHT_TOP, // Invalid combination.
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_RIGHT_TOP, PT_FAR_LEFT_TOP, // Invalid combination.
+ },
+ {
+ // LSM_FP_LEFT
+ PT_NEAR_LEFT_TOP,
+ PT_NEAR_LEFT_BOTTOM,
+ },
+ {
+ // LSM_FP_TOP
+ PT_NEAR_RIGHT_TOP,
+ PT_NEAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_NEAR_RIGHT_BOTTOM,
+ PT_NEAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_NEAR_LEFT_BOTTOM,
+ PT_NEAR_RIGHT_BOTTOM,
+ },
+ },
+
+ {
+ // LSM_FP_FAR
+ {
+ // LSM_FP_NEAR
+ PT_FAR_LEFT_TOP, PT_FAR_RIGHT_TOP, // Invalid combination.
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_RIGHT_TOP, PT_FAR_LEFT_TOP, // Invalid combination.
+ },
+ {
+ // LSM_FP_LEFT
+ PT_FAR_LEFT_BOTTOM,
+ PT_FAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_TOP
+ PT_FAR_LEFT_TOP,
+ PT_FAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_FAR_RIGHT_TOP,
+ PT_FAR_RIGHT_BOTTOM,
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_FAR_RIGHT_BOTTOM,
+ PT_FAR_LEFT_BOTTOM,
+ },
+ },
+
+ {
+ // LSM_FP_LEFT
+ {
+ // LSM_FP_NEAR
+ PT_NEAR_LEFT_BOTTOM,
+ PT_NEAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_LEFT_TOP,
+ PT_FAR_LEFT_BOTTOM,
+ },
+ {
+ // LSM_FP_LEFT
+ PT_FAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination.
+ },
+ {
+ // LSM_FP_TOP
+ PT_NEAR_LEFT_TOP,
+ PT_FAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_FAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination.
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_FAR_LEFT_BOTTOM,
+ PT_NEAR_LEFT_BOTTOM,
+ },
+ },
+
+ {
+ // LSM_FP_TOP
+ {
+ // LSM_FP_NEAR
+ PT_NEAR_LEFT_TOP,
+ PT_NEAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_RIGHT_TOP,
+ PT_FAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_LEFT
+ PT_FAR_LEFT_TOP,
+ PT_NEAR_LEFT_TOP,
+ },
+ {
+ // LSM_FP_TOP
+ PT_NEAR_LEFT_TOP, PT_FAR_LEFT_TOP, // Invalid combination.
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_NEAR_RIGHT_TOP,
+ PT_FAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_FAR_LEFT_BOTTOM, PT_NEAR_LEFT_BOTTOM, // Invalid combination.
+ },
+ },
+
+ {
+ // LSM_FP_RIGHT
+ {
+ // LSM_FP_NEAR
+ PT_NEAR_RIGHT_TOP,
+ PT_NEAR_RIGHT_BOTTOM,
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_RIGHT_BOTTOM,
+ PT_FAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_LEFT
+ PT_FAR_RIGHT_BOTTOM, PT_FAR_RIGHT_BOTTOM, // Invalid combination.
+ },
+ {
+ // LSM_FP_TOP
+ PT_FAR_RIGHT_TOP,
+ PT_NEAR_RIGHT_TOP,
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_FAR_RIGHT_BOTTOM, PT_FAR_RIGHT_BOTTOM, // Invalid combination.
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_NEAR_RIGHT_BOTTOM,
+ PT_FAR_RIGHT_BOTTOM,
+ },
+ },
+
+ // ==
+
+ // P_NEAR,
+ // P_FAR,
+ // P_LEFT,
+ // P_TOP,
+ // P_RIGHT,
+ // P_BOTTOM,
+
+ {
+ // LSM_FP_BOTTOM
+ {
+ // LSM_FP_NEAR
+ PT_NEAR_RIGHT_BOTTOM,
+ PT_NEAR_LEFT_BOTTOM,
+ },
+ {
+ // LSM_FP_FAR
+ PT_FAR_LEFT_BOTTOM,
+ PT_FAR_RIGHT_BOTTOM,
+ },
+ {
+ // LSM_FP_LEFT
+ PT_NEAR_LEFT_BOTTOM,
+ PT_FAR_LEFT_BOTTOM,
+ },
+ {
+ // LSM_FP_TOP
+ PT_NEAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination.
+ },
+ {
+ // LSM_FP_RIGHT
+ PT_FAR_RIGHT_BOTTOM,
+ PT_NEAR_RIGHT_BOTTOM,
+ },
+ {
+ // LSM_FP_BOTTOM
+ PT_FAR_LEFT_BOTTOM, PT_NEAR_LEFT_BOTTOM, // Invalid combination.
+ },
+ },
+
+ // ==
+
+ };
+ r_points[0] = fp_table[p_plane_a][p_plane_b][0];
+ r_points[1] = fp_table[p_plane_a][p_plane_b][1];
+}
+
+#endif
diff --git a/servers/visual/visual_server_light_culler.h b/servers/visual/visual_server_light_culler.h
new file mode 100644
index 00000000000..2c420660553
--- /dev/null
+++ b/servers/visual/visual_server_light_culler.h
@@ -0,0 +1,218 @@
+/**************************************************************************/
+/* visual_server_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 VISUAL_SERVER_LIGHT_CULLER_H
+#define VISUAL_SERVER_LIGHT_CULLER_H
+
+#include "core/math/plane.h"
+#include "core/math/vector3.h"
+#include "visual_server_scene.h"
+
+struct CameraMatrix;
+class Transform;
+
+// 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.
+
+// #define LIGHT_CULLER_DEBUG_LOGGING
+// #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 VISUAL_SERVER_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(VISUAL_SERVER_LIGHT_CULLER_CALCULATE_LUT)
+#define VISUAL_SERVER_LIGHT_CULLER_DEBUG_STRINGS
+#endif
+
+// Culls shadow casters that can't cast shadows into the camera frustum.
+class VisualServerLightCuller {
+public:
+ VisualServerLightCuller();
+
+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 Transform &p_cam_transform, const CameraMatrix &p_cam_matrix);
+
+ // Returns false if the entire light is culled (i.e. there is no intersection between the light and the view frustum).
+ bool prepare_light(const VisualServerScene::Instance &p_instance);
+
+ // Cull according to the planes that were setup in the previous call to prepare_light.
+ int cull(int p_count, VisualServerScene::Instance **p_result_array);
+
+ // 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:
+ // Internal version uses LightSource.
+ bool _add_light_camera_planes(const LightSource &p_light_source);
+
+ // Directional light gives parallel culling planes (as opposed to point lights).
+ bool add_light_camera_planes_directional(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; }
+
+ // Culling planes.
+ void add_cull_plane(const Plane &p);
+
+ struct Data {
+ // Camera frustum planes (world space) - order ePlane.
+ Vector frustum_planes;
+
+ // Camera frustum corners (world space) - order ePoint.
+ Vector3 frustum_points[NUM_CAM_POINTS];
+
+ // We are storing cull planes in a ye olde style array to prevent needless allocations.
+ Plane cull_planes[MAX_CULL_PLANES];
+ int num_cull_planes = 0;
+
+ // The whole light can be out of range of the view frustum, in which case all casters should be culled.
+ bool out_of_range = false;
+
+#ifdef VISUAL_SERVER_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 VISUAL_SERVER_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 &p_entry, bool p_pair = false);
+ String string_LUT_entry(const LocalVector &p_entry);
+
+ // Contains a list of points for each combination of plane facing directions.
+ LocalVector _calculated_LUT[LUT_SIZE];
+#endif
+};
+
+#endif // VISUAL_SERVER_LIGHT_CULLER_H
diff --git a/servers/visual/visual_server_scene.cpp b/servers/visual/visual_server_scene.cpp
index cd7bfe28744..89826bfd92f 100644
--- a/servers/visual/visual_server_scene.cpp
+++ b/servers/visual/visual_server_scene.cpp
@@ -33,6 +33,7 @@
#include "core/math/transform_interpolator.h"
#include "core/os/os.h"
#include "visual_server_globals.h"
+#include "visual_server_light_culler.h"
#include "visual_server_raster.h"
#include
@@ -337,7 +338,7 @@ void *VisualServerScene::_instance_pair(void *p_self, SpatialPartitionID, Instan
List::Element *E = light->geometries.push_back(pinfo);
if (geom->can_cast_shadows) {
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
geom->lighting_dirty = true;
@@ -409,7 +410,7 @@ void VisualServerScene::_instance_unpair(void *p_self, SpatialPartitionID, Insta
light->geometries.erase(E);
if (geom->can_cast_shadows) {
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
geom->lighting_dirty = true;
@@ -490,6 +491,12 @@ void VisualServerScene::pre_draw(bool p_will_draw) {
if (_interpolation_data.interpolation_enabled) {
update_interpolation_frame(p_will_draw);
}
+
+ // Opportunity to cheaply get any project settings that have changed.
+ if (ProjectSettings::get_singleton()->has_changes()) {
+ light_culler->set_caster_culling_active(GLOBAL_GET("rendering/quality/shadows/caster_culling"));
+ light_culler->set_light_culling_active(GLOBAL_GET("rendering/quality/shadows/light_culling"));
+ }
}
void VisualServerScene::scenario_set_debug(RID p_scenario, VS::ScenarioDebugMode p_debug_mode) {
@@ -802,7 +809,7 @@ void VisualServerScene::instance_set_layer_mask(RID p_instance, uint32_t p_mask)
if (geom->can_cast_shadows) {
for (List::Element *E = geom->lighting.front(); E; E = E->next()) {
InstanceLightData *light = static_cast(E->get()->base_data);
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
}
}
@@ -1217,7 +1224,7 @@ void VisualServerScene::instance_set_visible(RID p_instance, bool p_visible) {
if (geom->can_cast_shadows) {
for (List::Element *E = geom->lighting.front(); E; E = E->next()) {
InstanceLightData *light = static_cast(E->get()->base_data);
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
}
}
@@ -2043,7 +2050,7 @@ void VisualServerScene::_update_instance(Instance *p_instance) {
InstanceLightData *light = static_cast(p_instance->base_data);
VSG::scene_render->light_instance_set_transform(light->instance, *instance_xform);
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
if (p_instance->base_type == VS::INSTANCE_REFLECTION_PROBE) {
@@ -2075,7 +2082,7 @@ void VisualServerScene::_update_instance(Instance *p_instance) {
if (geom->can_cast_shadows) {
for (List::Element *E = geom->lighting.front(); E; E = E->next()) {
InstanceLightData *light = static_cast(E->get()->base_data);
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
}
@@ -2461,6 +2468,15 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons
switch (VSG::storage->light_get_type(p_instance->base)) {
case VS::LIGHT_DIRECTIONAL: {
+ // Directional light always needs preparing as it takes a different path to other lights.
+ light_culler->prepare_light(*p_instance);
+
+ // Directional lights can always do a tighter cull.
+ // This should occur because shadow_dirty_count is never decremented for directional lights.
+#ifdef DEV_ENABLED
+ DEV_CHECK_ONCE(!light->is_shadow_update_full());
+#endif
+
float max_distance = p_cam_projection.get_z_far();
float shadow_max = VSG::storage->light_get_param(p_instance->base, VS::LIGHT_PARAM_SHADOW_MAX_DISTANCE);
if (shadow_max > 0 && !p_cam_orthogonal) { //its impractical (and leads to unwanted behaviors) to set max distance in orthogonal camera
@@ -2719,6 +2735,10 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons
VSG::scene_render->light_instance_set_shadow_transform(light->instance, ortho_camera, ortho_transform, 0, distances[i + 1], i, bias_scale);
}
+ // Do a secondary cull to remove casters that don't intersect with the camera frustum.
+ // Note this could possibly be done in a more efficient place if we can share the cull results for each split.
+ cull_count = light_culler->cull(cull_count, instance_shadow_cull_result);
+
VSG::scene_render->render_shadow(light->instance, p_shadow_atlas, i, (RasterizerScene::InstanceBase **)instance_shadow_cull_result, cull_count);
}
@@ -2743,6 +2763,12 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons
planes.write[5] = light_transform.xform(Plane(Vector3(0, 0, -z), 0));
int cull_count = p_scenario->sps->cull_convex(planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, VS::INSTANCE_GEOMETRY_MASK);
+
+ // Do a secondary cull to remove casters that don't intersect with the camera frustum.
+ if (!light->is_shadow_update_full()) {
+ cull_count = light_culler->cull(cull_count, instance_shadow_cull_result);
+ }
+
Plane near_plane(light_transform.origin, light_transform.basis.get_axis(2) * z);
for (int j = 0; j < cull_count; j++) {
@@ -2796,6 +2822,11 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons
int cull_count = _cull_convex_from_point(p_scenario, light_transform, cm, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK);
+ // Do a secondary cull to remove casters that don't intersect with the camera frustum.
+ if (!light->is_shadow_update_full()) {
+ cull_count = light_culler->cull(cull_count, instance_shadow_cull_result);
+ }
+
Plane near_plane(xform.origin, -xform.basis.get_axis(2));
for (int j = 0; j < cull_count; j++) {
Instance *instance = instance_shadow_cull_result[j];
@@ -2831,6 +2862,11 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons
Vector planes = cm.get_projection_planes(light_transform);
int cull_count = _cull_convex_from_point(p_scenario, light_transform, cm, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK);
+ // Do a secondary cull to remove casters that don't intersect with the camera frustum.
+ if (!light->is_shadow_update_full()) {
+ cull_count = light_culler->cull(cull_count, instance_shadow_cull_result);
+ }
+
Plane near_plane(light_transform.origin, -light_transform.basis.get_axis(2));
for (int j = 0; j < cull_count; j++) {
Instance *instance = instance_shadow_cull_result[j];
@@ -2991,6 +3027,9 @@ void VisualServerScene::render_camera(Ref &p_interface, ARVRInter
};
void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const CameraMatrix &p_cam_projection, bool p_cam_orthogonal, RID p_force_environment, uint32_t p_visible_layers, RID p_scenario, RID p_shadow_atlas, RID p_reflection_probe, int32_t &r_previous_room_id_hint) {
+ // Prepare the light - camera volume culling system.
+ light_culler->prepare_camera(p_cam_transform, p_cam_projection);
+
// Note, in stereo rendering:
// - p_cam_transform will be a transform in the middle of our two eyes
// - p_cam_projection is a wider frustrum that encompasses both eyes
@@ -3283,16 +3322,41 @@ void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const Ca
}
}
- 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_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(VSG::storage->light_get_type(ins->base) != VS::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 = VSG::scene_render->shadow_atlas_update_light(p_shadow_atlas, light->instance, coverage, light->last_version);
if (redraw) {
//must redraw!
- light->shadow_dirty = _light_instance_update_shadow(ins, p_cam_transform, p_cam_projection, p_cam_orthogonal, p_shadow_atlas, scenario, p_visible_layers);
+ if (_light_instance_update_shadow(ins, p_cam_transform, p_cam_projection, p_cam_orthogonal, p_shadow_atlas, scenario, p_visible_layers)) {
+ // If the light requests another update (animated material?)...
+ light->make_shadow_dirty();
+ }
}
}
}
@@ -4517,7 +4581,7 @@ void VisualServerScene::_update_dirty_instance(Instance *p_instance) {
//ability to cast shadows change, let lights now
for (List::Element *E = geom->lighting.front(); E; E = E->next()) {
InstanceLightData *light = static_cast(E->get()->base_data);
- light->shadow_dirty = true;
+ light->make_shadow_dirty();
}
geom->can_cast_shadows = can_cast_shadows;
@@ -4629,12 +4693,17 @@ VisualServerScene::VisualServerScene() {
probe_bake_thread.start(_gi_probe_bake_threads, this);
probe_bake_thread_exit = false;
+ light_culler = memnew(VisualServerLightCuller);
+
render_pass = 1;
singleton = this;
_use_bvh = GLOBAL_DEF("rendering/quality/spatial_partitioning/use_bvh", true);
GLOBAL_DEF("rendering/quality/spatial_partitioning/bvh_collision_margin", 0.1);
ProjectSettings::get_singleton()->set_custom_property_info("rendering/quality/spatial_partitioning/bvh_collision_margin", PropertyInfo(Variant::REAL, "rendering/quality/spatial_partitioning/bvh_collision_margin", PROPERTY_HINT_RANGE, "0.0,2.0,0.01"));
+ light_culler->set_caster_culling_active(GLOBAL_DEF("rendering/quality/shadows/caster_culling", true));
+ light_culler->set_light_culling_active(GLOBAL_DEF("rendering/quality/shadows/light_culling", true));
+
_visual_server_callbacks = nullptr;
}
@@ -4642,4 +4711,9 @@ VisualServerScene::~VisualServerScene() {
probe_bake_thread_exit = true;
probe_bake_sem.post();
probe_bake_thread.wait_to_finish();
+
+ if (light_culler) {
+ memdelete(light_culler);
+ light_culler = nullptr;
+ }
}
diff --git a/servers/visual/visual_server_scene.h b/servers/visual/visual_server_scene.h
index ca203cc5a7b..337955eef6a 100644
--- a/servers/visual/visual_server_scene.h
+++ b/servers/visual/visual_server_scene.h
@@ -43,6 +43,8 @@
#include "portals/portal_renderer.h"
#include "servers/arvr/arvr_interface.h"
+class VisualServerLightCuller;
+
class VisualServerScene {
public:
enum {
@@ -484,16 +486,60 @@ public:
RID instance;
uint64_t last_version;
List::Element *D; // directional light in scenario
-
- bool shadow_dirty;
-
List geometries;
Instance *baked_light;
int32_t previous_room_id_hint;
+ 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() {
- shadow_dirty = true;
+ shadow_dirty_count = 1;
+ light_update_frame_id = UINT32_MAX;
+ light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX;
+ light_intersects_multiple_cameras = false;
+
D = nullptr;
last_version = 0;
baked_light = nullptr;
@@ -623,6 +669,7 @@ public:
RID light_instance_cull_result[MAX_LIGHTS_CULLED];
int light_cull_count;
int directional_light_count;
+ VisualServerLightCuller *light_culler;
RID reflection_probe_instance_cull_result[MAX_REFLECTION_PROBES_CULLED];
int reflection_probe_cull_count;