Repository: godotengine/godot (language: cpp)

Issue:
SpotLight3D causes clustering artifacts with large light angle and small camera far plane
### Godot version

v4.1.1.stable.mono.official [bd6af8e0e]

### System information

Godot v4.1.1.stable.mono - Windows 10.0.22621 - Vulkan (Forward+) - dedicated NVIDIA GeForce GTX 1060 6GB (NVIDIA; 31.0.15.3713) - 12th Gen Intel(R) Core(TM) i5-12600KF (16 Threads)

### Issue description

- This issue persist in 4.2dev4
- Does not require a WorldEnvironment node
- Probably not related to #78500, as this issue occurs without volumetric fog

With a Camera3D far plane of `4000` (default value)
![image](https://github.com/godotengine/godot/assets/18100602/6cc5a71a-645d-48be-a373-8250df201052)

With a Camera3D far plane of `100`
![image](https://github.com/godotengine/godot/assets/18100602/68b74da4-b0c2-4775-9a18-c14fc5ef7ddd)


### Steps to reproduce

- Camera3D must be "behind" the SpotLight3D
- Camera3D far plane must be small (tested with `100m`)
- SpotLight3D angle must be large (tested with 75°)

### Minimal reproduction project

[MRP.zip](https://github.com/godotengine/godot/files/12641633/MRP.zip)



Discussion / hints:
I have a similar problem on my project! there's a situation where the clipping plane needs to be close and it causes the same artifacts. I hope I can find the time to add a demo project later, but I'm using directional lights and getting the same issue.

I experimented with changing the sky and environment parameters, because I thought that would be the issue, but apparently not. 

Graphics code in general is definitely spooky and or haunted. No shade to Godot, I use Blender too and thats just how it is sometimes. Keeping the far plane above 100 is my best fix so far.

### System Information
Godot v4.1.1 stable. Linux Mint Vera, AMD Ryzen 3 (Picasso) dual-core, integrated graphics.
Ok, here's a demo project. It looks like the lighting bug happens on geometry within a certain range between the camera and the far plane.

WASD to move the camera, up/down arrows to move the far plane further/closer, current far plane prints to debug. Hold left click to rotate the cube.

It's probably a lighting optimization that breaks under these conditions, and you can achieve the same bug with any light source.

I'd like to not have to worry about this too.

[godot81772.zip](https://github.com/godotengine/godot/files/12862817/godot81772.zip)

The only time I was truly free from this on my end was at around 300.
I experienced the same problem in the TPSDemo

Fix patch:
diff --git a/servers/rendering/renderer_rd/cluster_builder_rd.h b/servers/rendering/renderer_rd/cluster_builder_rd.h
index 1badce2b8111..0e85fcb7ba7e 100644
--- a/servers/rendering/renderer_rd/cluster_builder_rd.h
+++ b/servers/rendering/renderer_rd/cluster_builder_rd.h
@@ -253,7 +253,11 @@ class ClusterBuilderRD {
 
 		radius *= p_radius;
 
-		if (p_type == LIGHT_TYPE_OMNI) {
+		// Spotlights with wide angle are trated as Omni lights.
+		// If the spot angle is above the threshold, we need a sphere instead of a cone for building the clusters
+		// since the cone gets too flat/large (spot angle close to 90 degrees) or
+		// can't even cover the affected area of the light (spot angle above 90 degrees).
+		if (p_type == LIGHT_TYPE_OMNI || (p_type == LIGHT_TYPE_SPOT && p_spot_aperture > WIDE_SPOT_ANGLE_THRESHOLD_DEG)) {
 			radius *= shared->sphere_overfit; // Overfit icosphere.
 
 			float depth = -xform.origin.z;
@@ -269,15 +273,21 @@ class ClusterBuilderRD {
 			e.scale[0] = radius;
 			e.scale[1] = radius;
 			e.scale[2] = radius;
-			e.type = ELEMENT_TYPE_OMNI_LIGHT;
-			e.original_index = cluster_count_by_type[ELEMENT_TYPE_OMNI_LIGHT];
+			if (p_type == LIGHT_TYPE_OMNI) {
+				e.type = ELEMENT_TYPE_OMNI_LIGHT;
+				e.original_index = cluster_count_by_type[ELEMENT_TYPE_OMNI_LIGHT];
+				cluster_count_by_type[ELEMENT_TYPE_OMNI_LIGHT]++;
+			} else { // LIGHT_TYPE_SPOT with wide angle.
+				e.type = ELEMENT_TYPE_SPOT_LIGHT;
+				e.has_wide_spot_angle = true;
+				e.original_index = cluster_count_by_type[ELEMENT_TYPE_SPOT_LIGHT];
+				cluster_count_by_type[ELEMENT_TYPE_SPOT_LIGHT]++;
+			}
 
 			RendererRD::MaterialStorage::store_transform_transposed_3x4(xform, e.transform_inv);
 
-			cluster_count_by_type[ELEMENT_TYPE_OMNI_LIGHT]++;
-
-		} else /*LIGHT_TYPE_SPOT */ {
-			radius *= shared->cone_overfit; // Overfit icosphere
+		} else /*LIGHT_TYPE_SPOT with no wide angle*/ {
+			radius *= shared->cone_overfit; // Overfit cone.
 
 			real_t len = Math::tan(Math::deg_to_rad(p_spot_aperture)) * radius;
 			// Approximate, probably better to use a cone support function.
@@ -310,24 +320,12 @@ class ClusterBuilderRD {
 			}
 
 			e.touches_far = max_d > z_far;
-
-			// If the spot angle is above the threshold, use a sphere instead of a cone for building the clusters
-			// since the cone gets too flat/large (spot angle close to 90 degrees) or
-			// can't even cover the affected area of the light (spot angle above 90 degrees).
-			if (p_spot_aperture > WIDE_SPOT_ANGLE_THRESHOLD_DEG) {
-				e.scale[0] = radius;
-				e.scale[1] = radius;
-				e.scale[2] = radius;
-				e.has_wide_spot_angle = true;
-			} else {
-				e.scale[0] = len * shared->cone_overfit;
-				e.scale[1] = len * shared->cone_overfit;
-				e.scale[2] = radius;
-				e.has_wide_spot_angle = false;
-			}
-
+			e.scale[0] = len * shared->cone_overfit;
+			e.scale[1] = len * shared->cone_overfit;
+			e.scale[2] = radius;
+			e.has_wide_spot_angle = false;
 			e.type = ELEMENT_TYPE_SPOT_LIGHT;
-			e.original_index = cluster_count_by_type[ELEMENT_TYPE_SPOT_LIGHT]; // Use omni light since they share index.
+			e.original_index = cluster_count_by_type[ELEMENT_TYPE_SPOT_LIGHT];
 
 			RendererRD::MaterialStorage::store_transform_transposed_3x4(xform, e.transform_inv);
 


Test patch: