├── .gitattributes ├── .gitignore ├── QuadtreeNode.gd ├── README.md ├── grid.png ├── grid.png.import ├── icon.svg ├── icon.svg.import ├── main.tscn ├── new_standard_material_3d.tres └── project.godot /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | -------------------------------------------------------------------------------- /QuadtreeNode.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node3D 3 | 4 | class_name QuadtreeNode 5 | 6 | var focus_position_last: Vector3 = Vector3.ZERO 7 | @export var focus_position: Vector3 = Vector3.ZERO 8 | # Quadtree specific properties 9 | @export var quadtree_size: float = 512 #basically the size of the world 10 | @export var max_chunk_depth: int = 8 #increase to create extra subdivisions (more detail locally) 11 | 12 | var chunks_list = {} 13 | var chunks_list_current = {} 14 | var material = preload("res://new_standard_material_3d.tres") 15 | 16 | # Placeholder for the quadtree structure 17 | var quadtree: QuadtreeChunk 18 | 19 | # Define a Quadtree chunk class 20 | class QuadtreeChunk: 21 | var bounds: AABB 22 | var children = [] 23 | var depth: int 24 | var max_chunk_depth: int 25 | var identifier: String 26 | 27 | func _init(_bounds: AABB, _depth: int, _max_chunk_depth: int): 28 | bounds = _bounds 29 | depth = _depth 30 | max_chunk_depth = _max_chunk_depth 31 | identifier = generate_identifier() 32 | 33 | func generate_identifier() -> String: 34 | # Generate a unique identifier for the chunk based on bounds and depth 35 | return "%s_%s_%d" % [bounds.position, bounds.size, depth] 36 | 37 | func subdivide(lod_center: Vector3): 38 | # Calculate new bounds for children 39 | var half_size = bounds.size.x * 0.5 40 | var quarter_size = bounds.size.x * 0.25 41 | var half_extents = Vector3(half_size, half_size, half_size) 42 | 43 | var child_positions = [ 44 | Vector3(-quarter_size, 0, -quarter_size), 45 | Vector3(quarter_size, 0, -quarter_size), 46 | Vector3(-quarter_size, 0, quarter_size), 47 | Vector3(quarter_size, 0, quarter_size) 48 | ] 49 | 50 | for offset in child_positions: 51 | # Calculate the center for each child 52 | var child_center = bounds.position + offset 53 | # Check the distance to the LOD center for each child 54 | var child_distance = child_center.distance_to(lod_center) 55 | var bounds_size = bounds.size.x 56 | if depth < max_chunk_depth and child_distance < bounds_size * 0.65: 57 | # Center is within the minimum detail distance 58 | # Calculate the bounds for the new child 59 | var child_bounds = AABB(child_center, half_extents) 60 | var new_child = QuadtreeChunk.new(child_bounds, depth + 1, max_chunk_depth) 61 | children.append(new_child) 62 | # Recursively subdivide the closest child chunk 63 | new_child.subdivide(lod_center) 64 | else: 65 | # Center is not within the minimum detail distance, add child at this depth 66 | var child_bounds = AABB(child_center - Vector3(quarter_size, quarter_size, quarter_size), half_extents) 67 | var new_child = QuadtreeChunk.new(child_bounds, depth + 1, max_chunk_depth) 68 | children.append(new_child) 69 | 70 | func visualize_quadtree(chunk: QuadtreeChunk): 71 | # Generate a MeshInstance for each chunk 72 | if not chunk.children: 73 | 74 | chunks_list_current[chunk.identifier] = true 75 | #if chunk.identifier already exists leave it 76 | if chunks_list.has(chunk.identifier): 77 | return 78 | 79 | var mesh_instance = MeshInstance3D.new() 80 | var mesh = PlaneMesh.new() 81 | mesh.size = Vector2(chunk.bounds.size.x, chunk.bounds.size.z) 82 | mesh_instance.mesh = mesh 83 | mesh_instance.position = Vector3(chunk.bounds.position.x,0,chunk.bounds.position.z) + Vector3(chunk.bounds.size.x * 0.5, 0, chunk.bounds.size.z * 0.5) 84 | mesh_instance.material_override = material 85 | add_child(mesh_instance) 86 | 87 | #add this chunk to chunk list 88 | chunks_list[chunk.identifier] = mesh_instance 89 | #if get_tree().get_edited_scene_root(): 90 | #mesh_instance.owner = get_tree().get_edited_scene_root() 91 | #mesh_instance.owner.set_editable_instance(mesh_instance, true) 92 | # Recursively visualize children chunks 93 | for child in chunk.children: 94 | visualize_quadtree(child) 95 | 96 | func _ready(): 97 | # Clear existing children 98 | for child in get_children(): 99 | remove_child(child) 100 | child.queue_free() 101 | update_chunks() 102 | 103 | func _process(delta): 104 | if focus_position != focus_position_last: 105 | 106 | if floor(focus_position) != focus_position_last: 107 | update_chunks() 108 | focus_position_last = floor(focus_position) 109 | 110 | func update_chunks(): 111 | # Initialize the quadtree by creating the root chunk 112 | var bounds = AABB(Vector3(quadtree_size * 0.5, 0, quadtree_size * 0.5), Vector3(quadtree_size, quadtree_size, quadtree_size)) 113 | quadtree = QuadtreeChunk.new(bounds, 0, max_chunk_depth) 114 | # Start the subdivision process 115 | quadtree.subdivide(focus_position) 116 | 117 | chunks_list_current = {} 118 | 119 | # Create a visual representation 120 | visualize_quadtree(quadtree) 121 | 122 | #remove any old unused chunks 123 | var chunks_to_remove = [] 124 | for chunk_id in chunks_list: 125 | if not chunks_list_current.has(chunk_id): 126 | chunks_to_remove.append(chunk_id) 127 | for chunk_id in chunks_to_remove: 128 | chunks_list[chunk_id].queue_free() 129 | chunks_list.erase(chunk_id) 130 | 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot 4 Quadtree Chunk Generation 2 | 3 | This repository contains a Godot 4 script for dynamic quadtree-based chunk generation. It's designed to create and manage a world divided into chunks that are dynamically loaded and unloaded based on a focus position, which can be updated typically to the player's location. This approach helps to optimize the rendering performance by only generating the necessary level of detail where it's needed. 4 | 5 | 6 | https://github.com/DigitallyTailored/godot4-quadtree/assets/13086157/385c1420-8700-493e-b163-f257dfdff4aa 7 | 8 | 9 | ## Features 10 | 11 | - Dynamic LOD (Level of Detail) generation based on focus position 12 | - Quadtree subdivision for efficient chunk management 13 | - Customizable world size and chunk depth 14 | - Visual representation of quadtree chunks in the Godot editor 15 | 16 | ## Installation 17 | 18 | To use the Quadtree Chunk Generation system in your Godot project, follow these steps: 19 | 20 | 1. Copy the `QuadtreeNode.gd` script from this repository into your Godot project's script folder. 21 | 2. Attach the script to a Node3D in your scene. 22 | 3. Adjust the exported variables, such as `quadtree_size` and `max_chunk_depth`, to suit your project's needs. 23 | 24 | ## Usage 25 | 26 | The quadtree node is designed to be used in a 3D Godot environment. Once you have added the script to a Node3D in your scene, you can configure its properties via the Inspector or through code. 27 | 28 | ### Properties 29 | 30 | - `focus_position`: The world coordinates that the quadtree should focus on when generating detailed chunks. 31 | - `quadtree_size`: The overall size of the world that the quadtree covers, effectively setting the bounds for chunk generation. 32 | - `max_chunk_depth`: The maximum depth of chunk subdivision, providing control over the level of detail. 33 | 34 | ### Visualization 35 | 36 | The script includes functionality to visualize the quadtree's chunk structure when running the Godot editor. This visualization helps to understand how the quadtree divides the world and manages the chunks. 37 | 38 | ## License 39 | 40 | This project is licensed under the MIT License. 41 | 42 | ## Acknowledgments 43 | 44 | Big shout out to the excellent [SimonDev](https://www.youtube.com/@simondev758) who I first learnt about quadtrees from. 45 | 46 | --- 47 | 48 | Enjoy creating vast and optimized 3D worlds in Godot with this Quadtree Chunk Generation system! If you encounter any issues or have any questions, feel free to open an issue on this GitHub repository. 49 | -------------------------------------------------------------------------------- /grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitallyTailored/godot4-quadtree/2042ad92a25263296e58e8495d5e47bf89c76fc4/grid.png -------------------------------------------------------------------------------- /grid.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ddtyjr8ck5pys" 6 | path.s3tc="res://.godot/imported/grid.png-9bb66f80dbdd793fb4bb275a0d999fdf.s3tc.ctex" 7 | metadata={ 8 | "imported_formats": ["s3tc_bptc"], 9 | "vram_texture": true 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://grid.png" 15 | dest_files=["res://.godot/imported/grid.png-9bb66f80dbdd793fb4bb275a0d999fdf.s3tc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=2 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=true 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=0 36 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dcd1ynepcfvvm" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://p8nyl186sis8"] 2 | 3 | [ext_resource type="Script" path="res://QuadtreeNode.gd" id="1_kkkv3"] 4 | 5 | [node name="Node3D" type="Node3D"] 6 | 7 | [node name="Node3D" type="Node3D" parent="."] 8 | script = ExtResource("1_kkkv3") 9 | focus_position = Vector3(64, 0, 64) 10 | quadtree_size = 10240.0 11 | 12 | [node name="Camera3D" type="Camera3D" parent="."] 13 | transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 150, 100, 150) 14 | -------------------------------------------------------------------------------- /new_standard_material_3d.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://c5pcc21ceaccr"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://ddtyjr8ck5pys" path="res://grid.png" id="1_4yw7s"] 4 | 5 | [resource] 6 | albedo_color = Color(0.890196, 1, 1, 1) 7 | albedo_texture = ExtResource("1_4yw7s") 8 | uv1_scale = Vector3(0.1, 0.1, 0.1) 9 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 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="chunk-quad" 14 | run/main_scene="res://main.tscn" 15 | config/features=PackedStringArray("4.2", "Forward Plus") 16 | config/icon="res://icon.svg" 17 | --------------------------------------------------------------------------------