diff --git a/3d/visibility_ranges/README.md b/3d/visibility_ranges/README.md
new file mode 100644
index 0000000000..efd9b06552
--- /dev/null
+++ b/3d/visibility_ranges/README.md
@@ -0,0 +1,68 @@
+# Visibility Ranges (HLOD)
+
+This demo showcases how to set up a hierarchical LOD system using visibility ranges.
+
+This can improve performance significantly in 3D scenes by reducing the number of
+draw calls and polygons that have to be drawn every frame.
+
+Use WASD or arrow keys to move, and use the mouse to look around. Press
+L to toggle the use of visibility ranges. Press F to
+toggle the fade mode between *transparency* (the default in this demo) and
+*hysteresis* (which is slightly faster, but results in more jarring
+transitions).
+
+> **Note**
+>
+> Performance is expected to decrease significantly after disabling visibility ranges,
+> as all trees will be drawn with full detail regardless of distance.
+
+Language: GDScript
+
+Renderer: Forward Plus
+
+## How does it work?
+
+There are 2 goals when using visibility ranges to improve performance:
+
+- Reduce the number of polygons that need to be drawn.
+- Reduce the number of draw calls, while also preserving culling opportunities when up close.
+
+To achieve this, the demo contains four levels of LOD for each cluster of 16 trees.
+These are the levels displayed from closest to furthest away:
+
+- Individual tree, with high geometric detail.
+- Individual tree, with low geometric detail.
+- Tree cluster, with high geoemtric detail.
+- Tree cluster, with low geometric detail.
+
+When the distance between the camera and the tree's origin is greater than 20
+units, the high-detail tree blends into a low-detail tree (transition period
+lasts 5 units).
+
+When the distance between the camera and the tree's origin is greater than 150
+units, all low-detail trees in the cluster are hidden, and the trees blend into
+a high-detail tree cluster. This transition period lasts for a longer distance
+(50 units) as the visual difference between these LOD levels is greater.
+
+When the distance between the camera and the cluster's origin is greater than
+450 units, the high-detail tree cluster blends into a low-detail tree cluster
+(also with a transition period of 50 units).
+
+When the distance between the camera and the cluster's origin is greater than
+1,900 units, the low-detail tree cluster fades away with a transition period of
+100 units. At this distance, the fog present in the scene makes this transition
+harder to notice.
+
+There are several ways to further improve this LOD system:
+
+- Use MultiMeshInstance3D to draw clusters of geometry in a single draw call.
+ However, individual meshes will not benefit from frustum or occlusion culling
+ (only the entire cluster is culled at once). Therefore, this must be done
+ carefully to balance the number of draw calls with culling efficiency.
+- Use impostor sprites in the distance. These can be drawn with Sprite3D, or
+ using MeshInstance3D + QuadMesh with a StandardMaterial3D that has
+ billboarding enabled.
+
+## Screenshots
+
+![Screenshot](screenshots/visibility_ranges.webp)
diff --git a/3d/visibility_ranges/camera.gd b/3d/visibility_ranges/camera.gd
new file mode 100644
index 0000000000..8dc3de6933
--- /dev/null
+++ b/3d/visibility_ranges/camera.gd
@@ -0,0 +1,43 @@
+extends Camera3D
+
+const MOUSE_SENSITIVITY = 0.002
+const MOVE_SPEED = 10.0
+
+var rot = Vector3()
+var velocity = Vector3()
+
+
+func _ready():
+ Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+
+
+func _input(event):
+ # Mouse look (only if the mouse is captured, and only after the loading screen has ended).
+ if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and Engine.get_process_frames() > 2:
+ # Horizontal mouse look.
+ rot.y -= event.relative.x * MOUSE_SENSITIVITY
+ # Vertical mouse look.
+ rot.x = clamp(rot.x - event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
+ transform.basis = Basis.from_euler(rot)
+
+ if event.is_action_pressed("toggle_mouse_capture"):
+ if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
+ Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
+ else:
+ Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+
+
+func _process(delta):
+ var motion = Vector3(
+ Input.get_axis(&"move_left", &"move_right"),
+ 0,
+ Input.get_axis(&"move_forward", &"move_back")
+ )
+
+ # Normalize motion to prevent diagonal movement from being
+ # `sqrt(2)` times faster than straight movement.
+ motion = motion.normalized()
+
+ velocity += MOVE_SPEED * delta * (transform.basis * motion)
+ velocity *= 0.85
+ position += velocity
diff --git a/3d/visibility_ranges/fps_label.gd b/3d/visibility_ranges/fps_label.gd
new file mode 100644
index 0000000000..6feeacb507
--- /dev/null
+++ b/3d/visibility_ranges/fps_label.gd
@@ -0,0 +1,5 @@
+extends Label
+
+
+func _process(_delta):
+ text = "%d FPS (%.2f mspf)" % [Engine.get_frames_per_second(), 1000.0 / Engine.get_frames_per_second()]
diff --git a/3d/visibility_ranges/project.godot b/3d/visibility_ranges/project.godot
new file mode 100644
index 0000000000..1ebd867263
--- /dev/null
+++ b/3d/visibility_ranges/project.godot
@@ -0,0 +1,74 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Visibility Ranges (HLOD)"
+config/description="This demo showcases how to set up a hierarchical LOD system
+using visibility ranges.
+
+This can improve performance significantly in 3D scenes by reducing
+the number of draw calls and polygons that have to be drawn every frame."
+run/main_scene="res://test.tscn"
+config/features=PackedStringArray("4.1")
+
+[display]
+
+window/vsync/vsync_mode=0
+window/stretch/mode="canvas_items"
+window/stretch/aspect="expand"
+
+[input]
+
+move_forward={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":122,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+move_back={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+move_left={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":113,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+move_right={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+toggle_mouse_capture={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194341,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
+toggle_visibility_ranges={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":76,"physical_keycode":0,"key_label":0,"unicode":108,"echo":false,"script":null)
+]
+}
+toggle_fade_mode={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":70,"physical_keycode":0,"key_label":0,"unicode":102,"echo":false,"script":null)
+]
+}
+
+[rendering]
+
+textures/default_filters/anisotropic_filtering_level=4
+anti_aliasing/quality/msaa_3d=2
diff --git a/3d/visibility_ranges/screenshots/.gdignore b/3d/visibility_ranges/screenshots/.gdignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/3d/visibility_ranges/screenshots/visibility_ranges.webp b/3d/visibility_ranges/screenshots/visibility_ranges.webp
new file mode 100644
index 0000000000..16c540d996
Binary files /dev/null and b/3d/visibility_ranges/screenshots/visibility_ranges.webp differ
diff --git a/3d/visibility_ranges/test.tscn b/3d/visibility_ranges/test.tscn
new file mode 100644
index 0000000000..b341cb6af7
--- /dev/null
+++ b/3d/visibility_ranges/test.tscn
@@ -0,0 +1,190 @@
+[gd_scene load_steps=17 format=3 uid="uid://bshkqyd3jv7xc"]
+
+[ext_resource type="Script" path="res://camera.gd" id="1_yepcp"]
+[ext_resource type="Script" path="res://tree_clusters.gd" id="2_ydews"]
+[ext_resource type="Script" path="res://fps_label.gd" id="3_vep8a"]
+
+[sub_resource type="Gradient" id="Gradient_hp0a8"]
+interpolation_mode = 2
+colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_6fgiw"]
+gradient = SubResource("Gradient_hp0a8")
+width = 128
+fill = 1
+fill_from = Vector2(0.5, 0.38)
+fill_to = Vector2(0.1, 0.4)
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_ymxen"]
+sky_top_color = Color(0.385, 0.4125, 0.55, 1)
+sky_horizon_color = Color(0.6432, 0.647667, 0.67, 1)
+sky_cover = SubResource("GradientTexture2D_6fgiw")
+sky_cover_modulate = Color(1, 0.776471, 0.129412, 1)
+ground_horizon_color = Color(0.643137, 0.647059, 0.670588, 1)
+sun_angle_max = 40.0
+sun_curve = 0.235375
+
+[sub_resource type="Sky" id="Sky_tq5wf"]
+sky_material = SubResource("ProceduralSkyMaterial_ymxen")
+
+[sub_resource type="Environment" id="Environment_w7n8k"]
+background_mode = 2
+sky = SubResource("Sky_tq5wf")
+ambient_light_color = Color(1, 1, 1, 1)
+ambient_light_sky_contribution = 0.75
+tonemap_mode = 3
+tonemap_white = 6.0
+fog_enabled = true
+fog_light_color = Color(0.517647, 0.552941, 0.607843, 1)
+fog_density = 0.001
+fog_aerial_perspective = 1.0
+
+[sub_resource type="BoxMesh" id="BoxMesh_qxf28"]
+lightmap_size_hint = Vector2i(327684, 163856)
+add_uv2 = true
+size = Vector3(32768, 1, 32768)
+subdivide_width = 15
+subdivide_depth = 15
+
+[sub_resource type="Gradient" id="Gradient_urgs4"]
+offsets = PackedFloat32Array(0, 0.243902, 0.357724, 0.617886, 1)
+colors = PackedColorArray(0.164706, 0.101961, 0, 1, 0.123774, 0.283202, 0.173896, 1, 0.354642, 0.374758, 0.206693, 1, 0.490333, 0.5, 0.48, 1, 0.1961, 0.37, 0.271457, 1)
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_g0yjr"]
+fractal_octaves = 9
+fractal_lacunarity = 2.717
+fractal_gain = 0.6
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_0q3y6"]
+width = 1024
+height = 1024
+seamless = true
+color_ramp = SubResource("Gradient_urgs4")
+noise = SubResource("FastNoiseLite_g0yjr")
+
+[sub_resource type="Gradient" id="Gradient_63ydg"]
+colors = PackedColorArray(0, 0.0431373, 0, 1, 0, 0, 0, 0)
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_dddeo"]
+noise_type = 0
+fractal_type = 3
+domain_warp_enabled = true
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_j3exn"]
+seamless = true
+color_ramp = SubResource("Gradient_63ydg")
+noise = SubResource("FastNoiseLite_dddeo")
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lf55d"]
+albedo_texture = SubResource("NoiseTexture2D_0q3y6")
+detail_enabled = true
+detail_uv_layer = 1
+detail_albedo = SubResource("NoiseTexture2D_j3exn")
+uv1_scale = Vector3(2048, 1536, 1)
+uv2_scale = Vector3(64, 32, 1)
+texture_filter = 5
+
+[node name="Node3D" type="Node3D"]
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.292632, 0.955563, -0.0355886, -0.845857, -0.241319, 0.4757, 0.445973, 0.169308, 0.878887, 0, 11, 0)
+shadow_enabled = true
+shadow_bias = 0.05
+shadow_blur = 1.5
+directional_shadow_max_distance = 200.0
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_w7n8k")
+
+[node name="Ground" type="MeshInstance3D" parent="."]
+mesh = SubResource("BoxMesh_qxf28")
+surface_material_override/0 = SubResource("StandardMaterial3D_lf55d")
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 200)
+fov = 74.0
+script = ExtResource("1_yepcp")
+
+[node name="TreeClusters" type="Node3D" parent="."]
+script = ExtResource("2_ydews")
+
+[node name="Loading" type="Control" parent="TreeClusters"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="ColorRect" type="ColorRect" parent="TreeClusters/Loading"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+color = Color(0, 0, 0, 0.501961)
+
+[node name="Label" type="Label" parent="TreeClusters/Loading"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -170.5
+offset_top = -24.0
+offset_right = 170.5
+offset_bottom = 24.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 8
+theme_override_font_sizes/font_size = 32
+text = "Loading, please wait…"
+
+[node name="VisibilityRanges" type="Label" parent="TreeClusters"]
+offset_left = 16.0
+offset_top = 16.0
+offset_right = 208.0
+offset_bottom = 42.0
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 4
+text = "Visibility ranges: Enabled"
+
+[node name="FadeMode" type="Label" parent="TreeClusters"]
+offset_left = 16.0
+offset_top = 48.0
+offset_right = 208.0
+offset_bottom = 74.0
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 4
+text = "Fade mode: Enabled (Transparency)"
+
+[node name="FPSLabel" type="Label" parent="."]
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -214.0
+offset_top = 16.0
+offset_right = -16.0
+offset_bottom = 39.0
+grow_horizontal = 0
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 4
+horizontal_alignment = 2
+script = ExtResource("3_vep8a")
+
+[node name="Help" type="Label" parent="."]
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_left = 16.0
+offset_top = -39.0
+offset_right = 56.0
+offset_bottom = -16.0
+grow_vertical = 0
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 4
+text = "L: Toggle visibility ranges
+F: Toggle fade mode"
diff --git a/3d/visibility_ranges/tree.tscn b/3d/visibility_ranges/tree.tscn
new file mode 100644
index 0000000000..c81590f7f4
--- /dev/null
+++ b/3d/visibility_ranges/tree.tscn
@@ -0,0 +1,73 @@
+[gd_scene load_steps=7 format=3 uid="uid://bv4peo31bj672"]
+
+[ext_resource type="Material" uid="uid://bo08s5dqyqpon" path="res://tree_material.tres" id="1_784bs"]
+[ext_resource type="Material" uid="uid://cqmgkacgqkl5c" path="res://trunk_material.tres" id="2_ap7c2"]
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_6uoay"]
+top_radius = 0.0
+bottom_radius = 1.75
+height = 4.0
+radial_segments = 32
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_qqg4e"]
+bottom_radius = 0.6
+radial_segments = 20
+rings = 1
+cap_bottom = false
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_74uyc"]
+top_radius = 0.0
+bottom_radius = 1.75
+height = 4.0
+radial_segments = 8
+rings = 1
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_43jmr"]
+bottom_radius = 0.6
+radial_segments = 4
+rings = 1
+cap_bottom = false
+
+[node name="Tree" type="Node3D"]
+
+[node name="HighDetail" type="Node3D" parent="."]
+
+[node name="Top" type="MeshInstance3D" parent="HighDetail" groups=["tree_high_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0)
+visibility_range_end = 20.0
+visibility_range_end_margin = 5.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_6uoay")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("1_784bs")
+
+[node name="Trunk" type="MeshInstance3D" parent="HighDetail" groups=["tree_high_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+visibility_range_end = 20.0
+visibility_range_end_margin = 5.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_qqg4e")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("2_ap7c2")
+
+[node name="LowDetail" type="Node3D" parent="."]
+
+[node name="Top" type="MeshInstance3D" parent="LowDetail" groups=["tree_low_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0)
+visibility_range_begin = 15.0
+visibility_range_end = 200.0
+visibility_range_end_margin = 50.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_74uyc")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("1_784bs")
+
+[node name="Trunk" type="MeshInstance3D" parent="LowDetail" groups=["tree_low_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+visibility_range_begin = 15.0
+visibility_range_end = 200.0
+visibility_range_end_margin = 50.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_43jmr")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("2_ap7c2")
diff --git a/3d/visibility_ranges/tree_cluster.tscn b/3d/visibility_ranges/tree_cluster.tscn
new file mode 100644
index 0000000000..93988bbcff
--- /dev/null
+++ b/3d/visibility_ranges/tree_cluster.tscn
@@ -0,0 +1,133 @@
+[gd_scene load_steps=8 format=3 uid="uid://bqjwf4fg6gvu5"]
+
+[ext_resource type="PackedScene" uid="uid://bv4peo31bj672" path="res://tree.tscn" id="1_xp6ld"]
+[ext_resource type="Material" uid="uid://bo08s5dqyqpon" path="res://tree_material.tres" id="2_dw421"]
+[ext_resource type="Material" uid="uid://cqmgkacgqkl5c" path="res://trunk_material.tres" id="3_dl0c6"]
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_6uoay"]
+top_radius = 0.0
+bottom_radius = 1.75
+height = 4.0
+radial_segments = 32
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_qqg4e"]
+bottom_radius = 0.6
+radial_segments = 20
+rings = 1
+cap_bottom = false
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_74uyc"]
+top_radius = 0.0
+bottom_radius = 1.75
+height = 4.0
+radial_segments = 8
+rings = 1
+
+[sub_resource type="CylinderMesh" id="CylinderMesh_43jmr"]
+bottom_radius = 0.6
+radial_segments = 4
+rings = 1
+cap_bottom = false
+
+[node name="TreeCluster" type="Node3D"]
+
+[node name="Tree" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.23853, -0.604848, 2.36929)
+
+[node name="Tree2" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.17988, 9.53674e-07, 11.1474)
+
+[node name="Tree3" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 0.994625, 0.103539, 0, -0.103539, 0.994625, -9.7838, 0, 15.4517)
+
+[node name="Tree4" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 0.999611, -0.0278867, 0, 0.0278867, 0.999611, -6.71813, 0, 6.46976)
+
+[node name="Tree5" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.6214, -0.095274, 3.48558)
+
+[node name="Tree6" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 0.990373, 0.13843, 0, -0.13843, 0.990373, 7.85683, -1.13758, -8.35561)
+
+[node name="Tree7" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.4899, -0.270895, -12.7505)
+
+[node name="Tree8" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.88321, -0.0901899, 15.154)
+
+[node name="Tree9" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.8513, 0, 1.831)
+
+[node name="Tree10" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(0.999923, 0.0124439, 0, -0.0124439, 0.999923, 0, 0, 0, 1, -12.9166, -0.497135, -3.58321)
+
+[node name="Tree11" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 0.999997, 0.00230383, 0, -0.00230383, 0.999997, -12.6297, 0, 8.99774)
+
+[node name="Tree12" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.9091, -0.833557, 9.89316)
+
+[node name="Tree13" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.5096, -0.271834, -8.60772)
+
+[node name="Tree14" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.9704, 0, 16.1838)
+
+[node name="Tree15" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(0.999292, 0.0376204, 0, -0.0376204, 0.999292, 0, 0, 0, 1, -12.6976, 0, -14.5755)
+
+[node name="Tree16" parent="." instance=ExtResource("1_xp6ld")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.76961, -0.509378, 20.9842)
+
+[node name="Cluster" type="Node3D" parent="."]
+transform = Transform3D(8, 0, 0, 0, 1, 0, 0, 0, 8, 0, 0, 0)
+
+[node name="HighDetail" type="Node3D" parent="Cluster"]
+
+[node name="Top" type="MeshInstance3D" parent="Cluster/HighDetail" groups=["tree_cluster_high_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0)
+cast_shadow = 0
+visibility_range_begin = 200.0
+visibility_range_begin_margin = 50.0
+visibility_range_end = 500.0
+visibility_range_end_margin = 50.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_6uoay")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("2_dw421")
+
+[node name="Trunk" type="MeshInstance3D" parent="Cluster/HighDetail" groups=["tree_cluster_high_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+cast_shadow = 0
+visibility_range_begin = 200.0
+visibility_range_begin_margin = 50.0
+visibility_range_end = 500.0
+visibility_range_end_margin = 50.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_qqg4e")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("3_dl0c6")
+
+[node name="LowDetail" type="Node3D" parent="Cluster"]
+
+[node name="Top" type="MeshInstance3D" parent="Cluster/LowDetail" groups=["tree_cluster_low_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, 0)
+cast_shadow = 0
+visibility_range_begin = 450.0
+visibility_range_end = 2000.0
+visibility_range_end_margin = 100.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_74uyc")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("2_dw421")
+
+[node name="Trunk" type="MeshInstance3D" parent="Cluster/LowDetail" groups=["tree_cluster_low_detail"]]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+cast_shadow = 0
+visibility_range_begin = 450.0
+visibility_range_end = 2000.0
+visibility_range_end_margin = 100.0
+visibility_range_fade_mode = 1
+mesh = SubResource("CylinderMesh_43jmr")
+skeleton = NodePath("../..")
+surface_material_override/0 = ExtResource("3_dl0c6")
diff --git a/3d/visibility_ranges/tree_clusters.gd b/3d/visibility_ranges/tree_clusters.gd
new file mode 100644
index 0000000000..42def20bd0
--- /dev/null
+++ b/3d/visibility_ranges/tree_clusters.gd
@@ -0,0 +1,85 @@
+extends Node3D
+
+const NUM_TREE_CLUSTERS = 2000
+const SPREAD = 1250
+const TREE_CLUSTER_SCENE = preload("res://tree_cluster.tscn")
+
+## If `false`, highest detail is always used (slower).
+var visibility_ranges_enabled = true
+
+## `true` = use transparencdy fade, `false` = use hysteresis.
+var fade_mode_enabled = true
+
+func _ready():
+ for i in 2:
+ # Draw two frames to let the loading screen be visible.
+ await get_tree().process_frame
+
+ # Use a predefined random seed for better reproducibility of results.
+ seed(0x60d07)
+
+ for i in NUM_TREE_CLUSTERS:
+ var tree_cluster = TREE_CLUSTER_SCENE.instantiate()
+ tree_cluster.position = Vector3(randf_range(-SPREAD, SPREAD), 0, randf_range(-SPREAD, SPREAD))
+ add_child(tree_cluster)
+
+ $Loading.visible = false
+
+
+func _input(event):
+ if event.is_action_pressed(&"toggle_visibility_ranges"):
+ visibility_ranges_enabled = not visibility_ranges_enabled
+ $VisibilityRanges.text = "Visibility ranges: %s" % ("Enabled" if visibility_ranges_enabled else "Disabled")
+ $VisibilityRanges.modulate = Color.WHITE if visibility_ranges_enabled else Color.YELLOW
+ $FadeMode.visible = visibility_ranges_enabled
+
+ # When disabling visibility ranges, display the high-detail trees at any range.
+ for node in get_tree().get_nodes_in_group(&"tree_high_detail"):
+ if visibility_ranges_enabled:
+ node.visibility_range_begin = 0
+ node.visibility_range_end = 20
+ else:
+ node.visibility_range_begin = 0
+ node.visibility_range_end = 0
+ for node in get_tree().get_nodes_in_group(&"tree_low_detail"):
+ node.visible = visibility_ranges_enabled
+ for node in get_tree().get_nodes_in_group(&"tree_cluster_high_detail"):
+ node.visible = visibility_ranges_enabled
+ for node in get_tree().get_nodes_in_group(&"tree_cluster_low_detail"):
+ node.visible = visibility_ranges_enabled
+
+ if event.is_action_pressed(&"toggle_fade_mode"):
+ fade_mode_enabled = not fade_mode_enabled
+ $FadeMode.text = "Fade mode: %s" % ("Enabled (Transparency)" if fade_mode_enabled else "Disabled (Hysteresis)")
+
+ for node in get_tree().get_nodes_in_group(&"tree_high_detail"):
+ if fade_mode_enabled:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
+ else:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
+
+ for node in get_tree().get_nodes_in_group(&"tree_low_detail"):
+ if fade_mode_enabled:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
+ node.visibility_range_end_margin = 50
+ else:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
+ node.visibility_range_end_margin = 0
+
+ for node in get_tree().get_nodes_in_group(&"tree_cluster_high_detail"):
+ if fade_mode_enabled:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
+ node.visibility_range_begin_margin = 50
+ node.visibility_range_end_margin = 50
+ else:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
+ node.visibility_range_begin_margin = 0
+ node.visibility_range_end_margin = 0
+
+ for node in get_tree().get_nodes_in_group(&"tree_cluster_low_detail"):
+ if fade_mode_enabled:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
+ node.visibility_range_end_margin = 100
+ else:
+ node.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
+ node.visibility_range_end_margin = 0
diff --git a/3d/visibility_ranges/tree_material.tres b/3d/visibility_ranges/tree_material.tres
new file mode 100644
index 0000000000..db6e12f9aa
--- /dev/null
+++ b/3d/visibility_ranges/tree_material.tres
@@ -0,0 +1,17 @@
+[gd_resource type="StandardMaterial3D" load_steps=3 format=3 uid="uid://bo08s5dqyqpon"]
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_uhsu1"]
+fractal_octaves = 9
+fractal_lacunarity = 7.0
+fractal_gain = 1.0
+fractal_weighted_strength = 1.0
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_xslbn"]
+seamless = true
+noise = SubResource("FastNoiseLite_uhsu1")
+
+[resource]
+albedo_color = Color(0.1705, 0.55, 0.297, 1)
+albedo_texture = SubResource("NoiseTexture2D_xslbn")
+uv1_scale = Vector3(1, 8, 1)
+uv1_triplanar_sharpness = 3.73213
diff --git a/3d/visibility_ranges/trunk_material.tres b/3d/visibility_ranges/trunk_material.tres
new file mode 100644
index 0000000000..56f4f746a0
--- /dev/null
+++ b/3d/visibility_ranges/trunk_material.tres
@@ -0,0 +1,17 @@
+[gd_resource type="StandardMaterial3D" load_steps=3 format=3 uid="uid://cqmgkacgqkl5c"]
+
+[sub_resource type="FastNoiseLite" id="FastNoiseLite_uhsu1"]
+fractal_octaves = 9
+fractal_lacunarity = 7.0
+fractal_gain = 1.0
+fractal_weighted_strength = 1.0
+
+[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_xslbn"]
+seamless = true
+noise = SubResource("FastNoiseLite_uhsu1")
+
+[resource]
+albedo_color = Color(0.5, 0.371667, 0.225, 1)
+albedo_texture = SubResource("NoiseTexture2D_xslbn")
+uv1_scale = Vector3(8, 1, 1)
+uv1_triplanar_sharpness = 3.73213