Skip to content

Commit 05aeb11

Browse files
authored
Add a 3D visibility ranges (HLOD) demo (#860)
1 parent 1d8fa9c commit 05aeb11

File tree

15 files changed

+709
-0
lines changed

15 files changed

+709
-0
lines changed

3d/visibility_ranges/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Visibility Ranges (HLOD)
2+
3+
This demo showcases how to set up a hierarchical LOD system using visibility ranges.
4+
5+
This can improve performance significantly in 3D scenes by reducing the number of
6+
draw calls and polygons that have to be drawn every frame.
7+
8+
Use WASD or arrow keys to move, and use the mouse to look around. Press
9+
<kbd>L</kbd> to toggle the use of visibility ranges. Press <kbd>F</kbd> to
10+
toggle the fade mode between *transparency* (the default in this demo) and
11+
*hysteresis* (which is slightly faster, but results in more jarring
12+
transitions).
13+
14+
> **Note**
15+
>
16+
> Performance is expected to decrease significantly after disabling visibility ranges,
17+
> as all trees will be drawn with full detail regardless of distance.
18+
19+
Language: GDScript
20+
21+
Renderer: Forward Plus
22+
23+
## How does it work?
24+
25+
There are 2 goals when using visibility ranges to improve performance:
26+
27+
- Reduce the number of polygons that need to be drawn.
28+
- Reduce the number of draw calls, while also preserving culling opportunities when up close.
29+
30+
To achieve this, the demo contains four levels of LOD for each cluster of 16 trees.
31+
These are the levels displayed from closest to furthest away:
32+
33+
- Individual tree, with high geometric detail.
34+
- Individual tree, with low geometric detail.
35+
- Tree cluster, with high geoemtric detail.
36+
- Tree cluster, with low geometric detail.
37+
38+
When the distance between the camera and the tree's origin is greater than 20
39+
units, the high-detail tree blends into a low-detail tree (transition period
40+
lasts 5 units).
41+
42+
When the distance between the camera and the tree's origin is greater than 150
43+
units, all low-detail trees in the cluster are hidden, and the trees blend into
44+
a high-detail tree cluster. This transition period lasts for a longer distance
45+
(50 units) as the visual difference between these LOD levels is greater.
46+
47+
When the distance between the camera and the cluster's origin is greater than
48+
450 units, the high-detail tree cluster blends into a low-detail tree cluster
49+
(also with a transition period of 50 units).
50+
51+
When the distance between the camera and the cluster's origin is greater than
52+
1,900 units, the low-detail tree cluster fades away with a transition period of
53+
100 units. At this distance, the fog present in the scene makes this transition
54+
harder to notice.
55+
56+
There are several ways to further improve this LOD system:
57+
58+
- Use MultiMeshInstance3D to draw clusters of geometry in a single draw call.
59+
However, individual meshes will not benefit from frustum or occlusion culling
60+
(only the entire cluster is culled at once). Therefore, this must be done
61+
carefully to balance the number of draw calls with culling efficiency.
62+
- Use impostor sprites in the distance. These can be drawn with Sprite3D, or
63+
using MeshInstance3D + QuadMesh with a StandardMaterial3D that has
64+
billboarding enabled.
65+
66+
## Screenshots
67+
68+
![Screenshot](screenshots/visibility_ranges.webp)

3d/visibility_ranges/camera.gd

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
extends Camera3D
2+
3+
const MOUSE_SENSITIVITY = 0.002
4+
const MOVE_SPEED = 10.0
5+
6+
var rot := Vector3()
7+
var velocity := Vector3()
8+
9+
10+
func _ready() -> void:
11+
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
12+
13+
14+
func _input(input_event: InputEvent) -> void:
15+
# Mouse look (only if the mouse is captured, and only after the loading screen has ended).
16+
if input_event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and Engine.get_process_frames() > 2:
17+
# Horizontal mouse look.
18+
rot.y -= input_event.relative.x * MOUSE_SENSITIVITY
19+
# Vertical mouse look.
20+
rot.x = clampf(rot.x - input_event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
21+
transform.basis = Basis.from_euler(rot)
22+
23+
if input_event.is_action_pressed("toggle_mouse_capture"):
24+
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
25+
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
26+
else:
27+
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
28+
29+
30+
func _process(delta: float) -> void:
31+
var motion := Vector3(
32+
Input.get_axis(&"move_left", &"move_right"),
33+
0,
34+
Input.get_axis(&"move_forward", &"move_back")
35+
)
36+
37+
# Normalize motion to prevent diagonal movement from being
38+
# `sqrt(2)` times faster than straight movement.
39+
motion = motion.normalized()
40+
41+
velocity += MOVE_SPEED * delta * (transform.basis * motion)
42+
velocity *= 0.85
43+
position += velocity

3d/visibility_ranges/camera.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://bic02fqt5yf40

3d/visibility_ranges/fps_label.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extends Label
2+
3+
4+
func _process(_delta: float) -> void:
5+
var fps: float = Engine.get_frames_per_second()
6+
text = "%d FPS (%.2f mspf)" % [fps, 1000.0 / fps]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://bd856mfo4l8g8

3d/visibility_ranges/project.godot

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
; Engine configuration file.
2+
; It's best edited using the editor UI and not directly,
3+
; since the parameters that go here are not all obvious.
4+
;
5+
; Format:
6+
; [section] ; section goes between []
7+
; param=value ; assign values to parameters
8+
9+
config_version=5
10+
11+
[application]
12+
13+
config/name="Visibility Ranges (HLOD)"
14+
config/description="This demo showcases how to set up a hierarchical LOD system
15+
using visibility ranges.
16+
17+
This can improve performance significantly in 3D scenes by reducing
18+
the number of draw calls and polygons that have to be drawn every frame."
19+
run/main_scene="res://test.tscn"
20+
config/features=PackedStringArray("4.5")
21+
22+
[display]
23+
24+
window/stretch/mode="canvas_items"
25+
window/stretch/aspect="expand"
26+
window/vsync/vsync_mode=0
27+
28+
[input]
29+
30+
move_forward={
31+
"deadzone": 0.5,
32+
"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,"location":0,"echo":false,"script":null)
33+
, 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,"location":0,"echo":false,"script":null)
34+
]
35+
}
36+
move_back={
37+
"deadzone": 0.5,
38+
"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,"location":0,"echo":false,"script":null)
39+
, 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,"location":0,"echo":false,"script":null)
40+
]
41+
}
42+
move_left={
43+
"deadzone": 0.5,
44+
"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,"location":0,"echo":false,"script":null)
45+
, 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,"location":0,"echo":false,"script":null)
46+
]
47+
}
48+
move_right={
49+
"deadzone": 0.5,
50+
"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,"location":0,"echo":false,"script":null)
51+
, 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,"location":0,"echo":false,"script":null)
52+
]
53+
}
54+
toggle_mouse_capture={
55+
"deadzone": 0.5,
56+
"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,"location":0,"echo":false,"script":null)
57+
, 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,"location":0,"echo":false,"script":null)
58+
]
59+
}
60+
toggle_visibility_ranges={
61+
"deadzone": 0.5,
62+
"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,"location":0,"echo":false,"script":null)
63+
]
64+
}
65+
toggle_fade_mode={
66+
"deadzone": 0.5,
67+
"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,"location":0,"echo":false,"script":null)
68+
]
69+
}
70+
71+
[rendering]
72+
73+
textures/default_filters/anisotropic_filtering_level=4
74+
anti_aliasing/quality/msaa_3d=2

3d/visibility_ranges/screenshots/.gdignore

Whitespace-only changes.
494 KB
Loading

3d/visibility_ranges/test.tscn

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
[gd_scene load_steps=17 format=3 uid="uid://bshkqyd3jv7xc"]
2+
3+
[ext_resource type="Script" uid="uid://bic02fqt5yf40" path="res://camera.gd" id="1_yepcp"]
4+
[ext_resource type="Script" uid="uid://b7erm757yjt5k" path="res://tree_clusters.gd" id="2_ydews"]
5+
[ext_resource type="Script" uid="uid://bd856mfo4l8g8" path="res://fps_label.gd" id="3_vep8a"]
6+
7+
[sub_resource type="Gradient" id="Gradient_hp0a8"]
8+
interpolation_mode = 2
9+
colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
10+
11+
[sub_resource type="GradientTexture2D" id="GradientTexture2D_6fgiw"]
12+
gradient = SubResource("Gradient_hp0a8")
13+
width = 128
14+
fill = 1
15+
fill_from = Vector2(0.5, 0.38)
16+
fill_to = Vector2(0.1, 0.4)
17+
18+
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_ymxen"]
19+
sky_top_color = Color(0.385, 0.4125, 0.55, 1)
20+
sky_horizon_color = Color(0.6432, 0.647667, 0.67, 1)
21+
sky_cover = SubResource("GradientTexture2D_6fgiw")
22+
sky_cover_modulate = Color(1, 0.776471, 0.129412, 1)
23+
ground_horizon_color = Color(0.643137, 0.647059, 0.670588, 1)
24+
sun_angle_max = 40.0
25+
sun_curve = 0.235375
26+
27+
[sub_resource type="Sky" id="Sky_tq5wf"]
28+
sky_material = SubResource("ProceduralSkyMaterial_ymxen")
29+
30+
[sub_resource type="Environment" id="Environment_w7n8k"]
31+
background_mode = 2
32+
sky = SubResource("Sky_tq5wf")
33+
ambient_light_color = Color(1, 1, 1, 1)
34+
ambient_light_sky_contribution = 0.75
35+
tonemap_mode = 3
36+
tonemap_white = 6.0
37+
fog_enabled = true
38+
fog_light_color = Color(0.517647, 0.552941, 0.607843, 1)
39+
fog_density = 0.001
40+
fog_aerial_perspective = 1.0
41+
42+
[sub_resource type="BoxMesh" id="BoxMesh_qxf28"]
43+
lightmap_size_hint = Vector2i(327684, 163856)
44+
add_uv2 = true
45+
size = Vector3(32768, 1, 32768)
46+
subdivide_width = 15
47+
subdivide_depth = 15
48+
49+
[sub_resource type="Gradient" id="Gradient_urgs4"]
50+
offsets = PackedFloat32Array(0, 0.243902, 0.357724, 0.617886, 1)
51+
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)
52+
53+
[sub_resource type="FastNoiseLite" id="FastNoiseLite_g0yjr"]
54+
fractal_octaves = 9
55+
fractal_lacunarity = 2.717
56+
fractal_gain = 0.6
57+
58+
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_0q3y6"]
59+
width = 1024
60+
height = 1024
61+
noise = SubResource("FastNoiseLite_g0yjr")
62+
color_ramp = SubResource("Gradient_urgs4")
63+
seamless = true
64+
65+
[sub_resource type="Gradient" id="Gradient_63ydg"]
66+
colors = PackedColorArray(0, 0.0431373, 0, 1, 0, 0, 0, 0)
67+
68+
[sub_resource type="FastNoiseLite" id="FastNoiseLite_dddeo"]
69+
noise_type = 0
70+
fractal_type = 3
71+
domain_warp_enabled = true
72+
73+
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_j3exn"]
74+
noise = SubResource("FastNoiseLite_dddeo")
75+
color_ramp = SubResource("Gradient_63ydg")
76+
seamless = true
77+
78+
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lf55d"]
79+
albedo_texture = SubResource("NoiseTexture2D_0q3y6")
80+
detail_enabled = true
81+
detail_uv_layer = 1
82+
detail_albedo = SubResource("NoiseTexture2D_j3exn")
83+
uv1_scale = Vector3(2048, 1536, 1)
84+
uv2_scale = Vector3(64, 32, 1)
85+
texture_filter = 5
86+
87+
[node name="Node3D" type="Node3D"]
88+
89+
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
90+
transform = Transform3D(-0.292632, 0.955563, -0.0355886, -0.845857, -0.241319, 0.4757, 0.445973, 0.169308, 0.878887, 0, 11, 0)
91+
shadow_enabled = true
92+
shadow_bias = 0.05
93+
shadow_blur = 1.5
94+
directional_shadow_max_distance = 200.0
95+
96+
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
97+
environment = SubResource("Environment_w7n8k")
98+
99+
[node name="Ground" type="MeshInstance3D" parent="."]
100+
mesh = SubResource("BoxMesh_qxf28")
101+
surface_material_override/0 = SubResource("StandardMaterial3D_lf55d")
102+
103+
[node name="Camera3D" type="Camera3D" parent="."]
104+
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 200)
105+
fov = 74.0
106+
script = ExtResource("1_yepcp")
107+
108+
[node name="TreeClusters" type="Node3D" parent="."]
109+
script = ExtResource("2_ydews")
110+
111+
[node name="Loading" type="Control" parent="TreeClusters"]
112+
layout_mode = 3
113+
anchors_preset = 15
114+
anchor_right = 1.0
115+
anchor_bottom = 1.0
116+
grow_horizontal = 2
117+
grow_vertical = 2
118+
119+
[node name="ColorRect" type="ColorRect" parent="TreeClusters/Loading"]
120+
layout_mode = 1
121+
anchors_preset = 15
122+
anchor_right = 1.0
123+
anchor_bottom = 1.0
124+
grow_horizontal = 2
125+
grow_vertical = 2
126+
color = Color(0, 0, 0, 0.501961)
127+
128+
[node name="Label" type="Label" parent="TreeClusters/Loading"]
129+
layout_mode = 1
130+
anchors_preset = 8
131+
anchor_left = 0.5
132+
anchor_top = 0.5
133+
anchor_right = 0.5
134+
anchor_bottom = 0.5
135+
offset_left = -170.5
136+
offset_top = -24.0
137+
offset_right = 170.5
138+
offset_bottom = 24.0
139+
grow_horizontal = 2
140+
grow_vertical = 2
141+
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
142+
theme_override_constants/outline_size = 8
143+
theme_override_font_sizes/font_size = 32
144+
text = "Loading, please wait…"
145+
146+
[node name="VisibilityRanges" type="Label" parent="TreeClusters"]
147+
offset_left = 16.0
148+
offset_top = 16.0
149+
offset_right = 208.0
150+
offset_bottom = 42.0
151+
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
152+
theme_override_constants/outline_size = 4
153+
text = "Visibility ranges: Enabled"
154+
155+
[node name="FadeMode" type="Label" parent="TreeClusters"]
156+
offset_left = 16.0
157+
offset_top = 48.0
158+
offset_right = 208.0
159+
offset_bottom = 74.0
160+
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
161+
theme_override_constants/outline_size = 4
162+
text = "Fade mode: Enabled (Transparency)"
163+
164+
[node name="FPSLabel" type="Label" parent="."]
165+
anchors_preset = 1
166+
anchor_left = 1.0
167+
anchor_right = 1.0
168+
offset_left = -214.0
169+
offset_top = 16.0
170+
offset_right = -16.0
171+
offset_bottom = 39.0
172+
grow_horizontal = 0
173+
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
174+
theme_override_constants/outline_size = 4
175+
horizontal_alignment = 2
176+
script = ExtResource("3_vep8a")
177+
178+
[node name="Help" type="Label" parent="."]
179+
anchors_preset = 2
180+
anchor_top = 1.0
181+
anchor_bottom = 1.0
182+
offset_left = 16.0
183+
offset_top = -39.0
184+
offset_right = 56.0
185+
offset_bottom = -16.0
186+
grow_vertical = 0
187+
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
188+
theme_override_constants/outline_size = 4
189+
text = "L: Toggle visibility ranges
190+
F: Toggle fade mode"

0 commit comments

Comments
 (0)