From 64a9292f9fa44bdc03cdc0a0c1403ad5114a3f03 Mon Sep 17 00:00:00 2001 From: Rob Kelly Date: Sat, 4 Oct 2025 16:49:49 -0600 Subject: [PATCH] Reorganized worldgen structure hierarchy around generation LODs --- src/util/auto_range.gd | 16 +-- src/world/generation/chunk/chunk.gd | 43 ------- src/world/generation/chunk/chunk.gd.uid | 1 - src/world/generation/chunk/chunk.tscn | 23 ---- .../generation/feature/arcology/arcology.tscn | 50 +++++--- .../generation/feature/arcology/empty.tscn | 2 +- .../generation/feature/generation_feature.gd | 60 +++++---- .../metro_construct_simple.gd | 13 +- .../metro_construct_simple.tscn | 5 +- .../generation/feature/metro/metro_empty.tscn | 2 +- src/world/generation/generated_element.gd | 40 ++++++ src/world/generation/generated_element.gd.uid | 1 + src/world/generation/generated_world.gd | 24 +++- .../arcology_grid_layer.gd | 35 +----- .../arcology_grid_layer.tscn | 2 + .../generation/layer/generation_layer.gd | 7 +- .../layer/grid_layer/bounded_grid_layer.gd | 104 ++++++++++++++++ .../grid_layer/bounded_grid_layer.gd.uid | 1 + .../generation/layer/grid_layer/grid_layer.gd | 117 ++++-------------- .../layer/grid_layer/grid_layer.gd.uid | 2 +- .../metro_grid_layer/metro_grid_layer.gd | 12 +- .../metro_grid_layer/metro_grid_layer.tscn | 1 + .../worldgen_manager/worldgen_manager.gd | 24 +++- 23 files changed, 305 insertions(+), 280 deletions(-) delete mode 100644 src/world/generation/chunk/chunk.gd delete mode 100644 src/world/generation/chunk/chunk.gd.uid delete mode 100644 src/world/generation/chunk/chunk.tscn create mode 100644 src/world/generation/generated_element.gd create mode 100644 src/world/generation/generated_element.gd.uid create mode 100644 src/world/generation/layer/grid_layer/bounded_grid_layer.gd create mode 100644 src/world/generation/layer/grid_layer/bounded_grid_layer.gd.uid diff --git a/src/util/auto_range.gd b/src/util/auto_range.gd index 16d82bb..93df9da 100644 --- a/src/util/auto_range.gd +++ b/src/util/auto_range.gd @@ -1,25 +1,13 @@ class_name AutoRange extends Node ## Component that automatically adjusts the visibility range of its parent to match LOD bounds -enum LODLevel { - LOW, - MEDIUM, - HIGH, -} - -@export var lod_level: LODLevel +@export var lod: WorldGen.LOD @export_range(0.0, 1.0) var end_margin_pct := 0.05 func _ready() -> void: var target: GeometryInstance3D = get_parent() - match lod_level: - LODLevel.LOW: - target.visibility_range_end = WorldGenManager.low_detail_radius - LODLevel.MEDIUM: - target.visibility_range_end = WorldGenManager.med_detail_radius - LODLevel.HIGH: - target.visibility_range_end = WorldGenManager.high_detail_radius + target.visibility_range_end = WorldGenManager.get_lod_radius(lod) target.visibility_range_end_margin = end_margin_pct * target.visibility_range_end queue_free() diff --git a/src/world/generation/chunk/chunk.gd b/src/world/generation/chunk/chunk.gd deleted file mode 100644 index bee6881..0000000 --- a/src/world/generation/chunk/chunk.gd +++ /dev/null @@ -1,43 +0,0 @@ -class_name Chunk extends Node3D -## A discrete generated chunk of the world - -const SIZE := Vector2(64, 64) -const SCENE := preload("res://src/world/generation/chunk/chunk.tscn") - -@export var construct_offset := Vector2(0.1, 0.0) -@export var construct_height_factor := 300.0 -@export var construct_height_threshold := 30.0 - - -func chunk_position() -> Vector2: - return Chunk.world_to_chunk(position) - - -func generate() -> void: - # TODO: this - - # Let's generate a building... - var construct_noise := WorldGenManager.noise.get_noise_2dv(chunk_position() + construct_offset) - var construct_height := construct_noise * construct_height_factor - if construct_height > construct_height_threshold: - var construct := CSGBox3D.new() - add_child(construct) - construct.size = Vector3(60, construct_height, 60) - construct.position = Vector3(2, construct_height / 2, 2) - construct.collision_layer = 1 - construct.use_collision = true - - -static func chunk_to_world(chunk_pos: Vector2) -> Vector3: - return Vector3(chunk_pos.x * SIZE.x, 0, chunk_pos.y * SIZE.y) - - -static func world_to_chunk(world_pos: Vector3) -> Vector2: - return Vector2(world_pos.x / SIZE.x, world_pos.z / SIZE.y) - - -static func generate_chunk(chunk_pos: Vector2) -> Chunk: - var instance: Chunk = SCENE.instantiate() - instance.position = Chunk.chunk_to_world(chunk_pos) - instance.generate() - return instance diff --git a/src/world/generation/chunk/chunk.gd.uid b/src/world/generation/chunk/chunk.gd.uid deleted file mode 100644 index 59f2606..0000000 --- a/src/world/generation/chunk/chunk.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chqpqe4anvamd diff --git a/src/world/generation/chunk/chunk.tscn b/src/world/generation/chunk/chunk.tscn deleted file mode 100644 index 319936a..0000000 --- a/src/world/generation/chunk/chunk.tscn +++ /dev/null @@ -1,23 +0,0 @@ -[gd_scene load_steps=4 format=3 uid="uid://crs68yhijqkca"] - -[ext_resource type="Script" uid="uid://chqpqe4anvamd" path="res://src/world/generation/chunk/chunk.gd" id="1_87ter"] - -[sub_resource type="PlaneMesh" id="PlaneMesh_0cma0"] -size = Vector2(64, 64) -subdivide_width = 64 -subdivide_depth = 64 -center_offset = Vector3(32, 0, 32) - -[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_87ter"] -points = PackedVector3Array(1.9073486e-06, 0, 1.9073486e-06, 1.9073486e-06, 0, 64, 64, 0, 1.9073486e-06, 64, 0, 64) - -[node name="Chunk" type="Node3D"] -script = ExtResource("1_87ter") - -[node name="MeshInstance3D" type="MeshInstance3D" parent="."] -mesh = SubResource("PlaneMesh_0cma0") - -[node name="StaticBody3D" type="StaticBody3D" parent="MeshInstance3D"] - -[node name="CollisionShape3D" type="CollisionShape3D" parent="MeshInstance3D/StaticBody3D"] -shape = SubResource("ConvexPolygonShape3D_87ter") diff --git a/src/world/generation/feature/arcology/arcology.tscn b/src/world/generation/feature/arcology/arcology.tscn index fdfda0d..a53b73a 100644 --- a/src/world/generation/feature/arcology/arcology.tscn +++ b/src/world/generation/feature/arcology/arcology.tscn @@ -72,6 +72,7 @@ material = ExtResource("3_yw1ox") [node name="AutoRange" type="Node" parent="InnerPyramid"] script = ExtResource("4_kox75") +lod = 1 end_margin_pct = 0.01 metadata/_custom_type_script = "uid://cv0o1lirqeq44" @@ -87,6 +88,7 @@ shape = SubResource("ConcavePolygonShape3D_ek7o7") [node name="AutoRange" type="Node" parent="WorldFloor"] script = ExtResource("4_kox75") +lod = 1 end_margin_pct = 0.01 metadata/_custom_type_script = "uid://cv0o1lirqeq44" @@ -96,11 +98,13 @@ metadata/_custom_type_script = "uid://cv0o1lirqeq44" [node name="MetroGridLayer" parent="MetroQuadrant/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 55) -bounding_box = AABB(0, 0, 0, 49088, 100, 448) +bounding_box = AABB(0, 0, 0, 49088, 400, 448) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 503) -bounding_box = AABB(0, 0, 0, 448, 100, 48640) +bounding_box = AABB(0, 0, 0, 448, 400, 48640) +noise_offset = Vector3(1, 1, 1) [node name="InnerMetro" type="Node3D" parent="MetroQuadrant"] @@ -108,13 +112,15 @@ bounding_box = AABB(0, 0, 0, 448, 100, 48640) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45047, 0, 503) construct_height_factor_x = SubResource("Curve_0w44q") construct_height_factor_z = SubResource("Curve_amoo5") -bounding_box = AABB(0, 0, 0, 4096, 100, 44480) +bounding_box = AABB(0, 0, 0, 4096, 400, 44480) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant/InnerMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 503, 0, 45047) construct_height_factor_x = SubResource("Curve_amoo5") construct_height_factor_z = SubResource("Curve_0w44q") -bounding_box = AABB(0, 0, 0, 44480, 100, 4096) +bounding_box = AABB(0, 0, 0, 44480, 400, 4096) +noise_offset = Vector3(1, 1, 1) [node name="MetroQuadrant2" type="Node3D" parent="."] transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 100000) @@ -123,11 +129,13 @@ transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0 [node name="MetroGridLayer" parent="MetroQuadrant2/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 55) -bounding_box = AABB(0, 0, 0, 49088, 100, 448) +bounding_box = AABB(0, 0, 0, 49088, 400, 448) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant2/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 503) -bounding_box = AABB(0, 0, 0, 448, 100, 48640) +bounding_box = AABB(0, 0, 0, 448, 400, 48640) +noise_offset = Vector3(1, 1, 1) [node name="InnerMetro" type="Node3D" parent="MetroQuadrant2"] @@ -135,13 +143,15 @@ bounding_box = AABB(0, 0, 0, 448, 100, 48640) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45047, 0, 503) construct_height_factor_x = SubResource("Curve_0w44q") construct_height_factor_z = SubResource("Curve_amoo5") -bounding_box = AABB(0, 0, 0, 4096, 100, 44480) +bounding_box = AABB(0, 0, 0, 4096, 400, 44480) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant2/InnerMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 503, 0, 45047) construct_height_factor_x = SubResource("Curve_amoo5") construct_height_factor_z = SubResource("Curve_0w44q") -bounding_box = AABB(0, 0, 0, 44480, 100, 4096) +bounding_box = AABB(0, 0, 0, 44480, 400, 4096) +noise_offset = Vector3(1, 1, 1) [node name="MetroQuadrant3" type="Node3D" parent="."] transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 100000, 0, 100000) @@ -150,11 +160,13 @@ transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 1000 [node name="MetroGridLayer" parent="MetroQuadrant3/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 55) -bounding_box = AABB(0, 0, 0, 49088, 100, 448) +bounding_box = AABB(0, 0, 0, 49088, 400, 448) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant3/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 503) -bounding_box = AABB(0, 0, 0, 448, 100, 48640) +bounding_box = AABB(0, 0, 0, 448, 400, 48640) +noise_offset = Vector3(1, 1, 1) [node name="InnerMetro" type="Node3D" parent="MetroQuadrant3"] @@ -162,13 +174,15 @@ bounding_box = AABB(0, 0, 0, 448, 100, 48640) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45047, 0, 503) construct_height_factor_x = SubResource("Curve_0w44q") construct_height_factor_z = SubResource("Curve_amoo5") -bounding_box = AABB(0, 0, 0, 4096, 100, 44480) +bounding_box = AABB(0, 0, 0, 4096, 400, 44480) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant3/InnerMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 503, 0, 45047) construct_height_factor_x = SubResource("Curve_amoo5") construct_height_factor_z = SubResource("Curve_0w44q") -bounding_box = AABB(0, 0, 0, 44480, 100, 4096) +bounding_box = AABB(0, 0, 0, 44480, 400, 4096) +noise_offset = Vector3(1, 1, 1) [node name="MetroQuadrant4" type="Node3D" parent="."] transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 100000, 0, 0) @@ -177,11 +191,13 @@ transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 1000 [node name="MetroGridLayer" parent="MetroQuadrant4/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 55) -bounding_box = AABB(0, 0, 0, 49088, 100, 448) +bounding_box = AABB(0, 0, 0, 49088, 400, 448) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant4/OuterMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 55, 0, 503) -bounding_box = AABB(0, 0, 0, 448, 100, 48640) +bounding_box = AABB(0, 0, 0, 448, 400, 48640) +noise_offset = Vector3(1, 1, 1) [node name="InnerMetro" type="Node3D" parent="MetroQuadrant4"] @@ -189,10 +205,12 @@ bounding_box = AABB(0, 0, 0, 448, 100, 48640) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45047, 0, 503) construct_height_factor_x = SubResource("Curve_0w44q") construct_height_factor_z = SubResource("Curve_amoo5") -bounding_box = AABB(0, 0, 0, 4096, 100, 44480) +bounding_box = AABB(0, 0, 0, 4096, 400, 44480) +noise_offset = Vector3(1, 1, 1) [node name="MetroGridLayer2" parent="MetroQuadrant4/InnerMetro" instance=ExtResource("4_fy7wq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 503, 0, 45047) construct_height_factor_x = SubResource("Curve_amoo5") construct_height_factor_z = SubResource("Curve_0w44q") -bounding_box = AABB(0, 0, 0, 44480, 100, 4096) +bounding_box = AABB(0, 0, 0, 44480, 400, 4096) +noise_offset = Vector3(1, 1, 1) diff --git a/src/world/generation/feature/arcology/empty.tscn b/src/world/generation/feature/arcology/empty.tscn index b971d6e..69e6cf6 100644 --- a/src/world/generation/feature/arcology/empty.tscn +++ b/src/world/generation/feature/arcology/empty.tscn @@ -25,5 +25,5 @@ shape = SubResource("ConcavePolygonShape3D_q70le") [node name="AutoRange" type="Node" parent="WorldFloor"] script = ExtResource("2_q70le") -lod_level = 1 +lod = 1 metadata/_custom_type_script = "uid://cv0o1lirqeq44" diff --git a/src/world/generation/feature/generation_feature.gd b/src/world/generation/feature/generation_feature.gd index 28580cb..8c34826 100644 --- a/src/world/generation/feature/generation_feature.gd +++ b/src/world/generation/feature/generation_feature.gd @@ -1,47 +1,43 @@ -class_name GenerationFeature extends Node3D +class_name GenerationFeature extends GeneratedElement ## Base class for world features generated during worldgen. -## -## Layers contain features. Some features may contain layers. -@export var noise_scale := Vector3.ONE -@export var noise_offset := Vector3.ZERO +## Set of sub-layers that require generation at each LOD +var sub_layers: Dictionary[WorldGen.LOD, Dictionary] = { + WorldGen.LOD.LOW: {}, + WorldGen.LOD.MEDIUM: {}, + WorldGen.LOD.HIGH: {}, +} -var sub_layers: Array[GenerationLayer]: - get(): - if not _defined_sub_layers: - sub_layers = _find_sub_layers(self) - _defined_sub_layers = true - return sub_layers -var _defined_sub_layers := false -var _generated := false +## Set of LODs at which this feature has been generated +var generated: Dictionary[WorldGen.LOD, bool] = {} -func _find_sub_layers(node: Node) -> Array[GenerationLayer]: - var layers: Array[GenerationLayer] = [] +func _ready() -> void: + _discover_sub_layers(self) + + +func _discover_sub_layers(node: Node) -> void: if node is GenerationLayer: - layers.append(node) + for lod in WorldGen.LOD_LIST: + sub_layers[lod][node] = false for c: Node in node.get_children(): # Do not walk outside of this scene if c.owner in [owner, self]: - layers.append_array(_find_sub_layers(c)) - return layers + _discover_sub_layers(c) -func probe() -> void: - # TODO may want to make low-detail & high-detail probes distinct - if not _generated: - generate() - _generated = true +func _generate(lod: WorldGen.LOD) -> bool: + if lod not in generated: + generate_feature(lod) + generated[lod] = true - for layer in sub_layers: - layer.probe() + for layer: GenerationLayer in sub_layers[lod]: + if layer.generate(lod): + sub_layers[lod].erase(layer) + + return not sub_layers[lod] -## Generate elements of this feature. Called by default on the first call to `probe` -func generate() -> void: +## Generate elements of this feature. Called by default on the first call to `probe` for this lod. +func generate_feature(_lod: WorldGen.LOD) -> void: pass # Implemented in derived type - - -func sample_noise() -> float: - var sample_point := global_position * noise_scale + noise_offset - return WorldGenManager.noise.get_noise_3dv(sample_point) diff --git a/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.gd b/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.gd index f18a08e..6a3f5f4 100644 --- a/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.gd +++ b/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.gd @@ -6,9 +6,10 @@ class_name MetroConstructSimple extends GenerationFeature @onready var construct_box: CSGBox3D = %ConstructBox -func generate() -> void: - var value := absf(sample_noise()) - var height := value * height_factor + min_height - construct_box.position.y = height / 2 - construct_box.size.y = height - _generated = true +func generate_feature(lod: WorldGen.LOD) -> void: + match lod: + WorldGen.LOD.MEDIUM: + var value := absf(sample_noise()) + var height := value * height_factor + min_height + construct_box.position.y = height / 2 + construct_box.size.y = height diff --git a/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.tscn b/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.tscn index 62a0778..2e88814 100644 --- a/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.tscn +++ b/src/world/generation/feature/metro/metro_construct_simple/metro_construct_simple.tscn @@ -13,6 +13,7 @@ points = PackedVector3Array(-31.999998, 0, -31.999998, -31.999998, 0, 31.999998, [node name="MetroConstructSimple" type="Node3D"] script = ExtResource("1_jv74y") +generated_lods = 6 metadata/_custom_type_script = "uid://drk82eeqk2mjs" [node name="WorldFloor" type="MeshInstance3D" parent="."] @@ -27,7 +28,7 @@ shape = SubResource("ConvexPolygonShape3D_y2o1w") [node name="AutoRange" type="Node" parent="WorldFloor"] script = ExtResource("2_74b57") -lod_level = 1 +lod = 2 metadata/_custom_type_script = "uid://cv0o1lirqeq44" [node name="ConstructBox" type="CSGBox3D" parent="."] @@ -39,5 +40,5 @@ size = Vector3(48, 48, 48) [node name="AutoRange" type="Node" parent="ConstructBox"] script = ExtResource("2_74b57") -lod_level = 1 +lod = 2 metadata/_custom_type_script = "uid://cv0o1lirqeq44" diff --git a/src/world/generation/feature/metro/metro_empty.tscn b/src/world/generation/feature/metro/metro_empty.tscn index d8ff478..9c90460 100644 --- a/src/world/generation/feature/metro/metro_empty.tscn +++ b/src/world/generation/feature/metro/metro_empty.tscn @@ -26,5 +26,5 @@ shape = SubResource("ConvexPolygonShape3D_e5j5s") [node name="AutoRange" type="Node" parent="WorldFloor"] script = ExtResource("2_xbnbu") -lod_level = 1 +lod = 2 metadata/_custom_type_script = "uid://cv0o1lirqeq44" diff --git a/src/world/generation/generated_element.gd b/src/world/generation/generated_element.gd new file mode 100644 index 0000000..8b96db7 --- /dev/null +++ b/src/world/generation/generated_element.gd @@ -0,0 +1,40 @@ +class_name GeneratedElement extends Node3D +## Base class for all generated world elements + +@export_flags("Low", "Medium", "High") var generated_lods := 0b111 + +## Scale to apply to position before sampling world generation noise. +@export var noise_scale := Vector3.ONE + +## Offset to apply to position before sampling world generation noise. +@export var noise_offset := Vector3.ZERO + + +## Generate this element at the given level of detail. +## +## Returns `true` if the element has completed generation at this LOD. +## Some composite elements may require multiple `generate` calls to complete generation. +func generate(lod: WorldGen.LOD) -> bool: + if lod & generated_lods: + return _generate(lod) + return true + + +## Generate this element at the given level of detail. +## +## This method is called by `generate` and should be overridden by derived types with +## custom generation logic. +## Returns `true` if the element has completed generation at this LOD. +## Some composite elements may require multiple `generate` calls to complete generation. +func _generate(_lod: WorldGen.LOD) -> bool: + return true # Implemented by derived types. + + +## Sample world generation noise at this element's position with scale & offset. +func sample_noise() -> float: + return sample_noise_at(global_position) + + +## Sample world generation noise at the given point, applying this element's scale & offset. +func sample_noise_at(pos: Vector3) -> float: + return WorldGenManager.noise.get_noise_3dv(pos * noise_scale + noise_offset) diff --git a/src/world/generation/generated_element.gd.uid b/src/world/generation/generated_element.gd.uid new file mode 100644 index 0000000..ccc5cee --- /dev/null +++ b/src/world/generation/generated_element.gd.uid @@ -0,0 +1 @@ +uid://c7x4yin558csb diff --git a/src/world/generation/generated_world.gd b/src/world/generation/generated_world.gd index 0a8dc28..a666cf6 100644 --- a/src/world/generation/generated_world.gd +++ b/src/world/generation/generated_world.gd @@ -1,10 +1,24 @@ class_name GeneratedWorld extends Node3D +var elements: Dictionary[WorldGen.LOD, Dictionary] = { + WorldGen.LOD.LOW: {}, + WorldGen.LOD.MEDIUM: {}, + WorldGen.LOD.HIGH: {}, +} + + +func _ready() -> void: + for c: Node in get_children(): + if c is GeneratedElement: + for lod in WorldGen.LOD_LIST: + if not elements[lod]: + elements[lod] = {} + elements[lod][c] = false + func _process(_delta: float) -> void: # Probe all child generation layers & features each frame - for c: Node in get_children(): - if c is GenerationLayer: - (c as GenerationLayer).probe() - elif c is GenerationFeature: - (c as GenerationFeature).probe() + for lod in WorldGen.LOD_LIST: + for element: GeneratedElement in elements[lod]: + if element.generate(lod): + elements[lod].erase(element) diff --git a/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.gd b/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.gd index 106375d..3895e0f 100644 --- a/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.gd +++ b/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.gd @@ -1,37 +1,19 @@ -class_name ArcologyGridLayer extends GenerationLayer +class_name ArcologyGridLayer extends GridLayer ## Grid with each cell possibly generating an arcology -const GRID_SIZE := Vector2(100000, 100000) - @export var arcology_feature_scene: PackedScene @export var empty_feature_scene: PackedScene -@export var sample_scale := 2.0 @export var arcology_probability := 0.1 var features: Dictionary[Vector2, GenerationFeature] = {} -func probe() -> void: - var world_pos := WorldGenManager.get_generation_point() - probe_radius(world_pos, WorldGenManager.low_detail_radius) - # TODO high-detail & medium-detail probes - - -func probe_radius(center: Vector3, radius: float) -> void: - var rad_diff := Vector3(radius, 0, radius) - var grid_low := world_to_local(center - rad_diff).floor() - var grid_high := world_to_local(center + rad_diff).floor() - for i in range(grid_low.x, grid_high.x + 1): - for j in range(grid_low.y, grid_high.y + 1): - probe_grid(Vector2(i, j)) - - -func probe_grid(grid_pos: Vector2) -> void: +func generate_grid(lod: WorldGen.LOD, grid_pos: Vector2) -> bool: # Generate if needed if grid_pos not in features: var feature: GenerationFeature - var sample := absf(WorldGenManager.noise.get_noise_2dv(sample_scale * grid_pos)) + var sample := absf(sample_noise_at_grid(grid_pos)) print_debug("Sampled ", sample, " at ", grid_pos) if sample < arcology_probability: print_debug("Generating arcology at ", grid_pos) @@ -42,13 +24,4 @@ func probe_grid(grid_pos: Vector2) -> void: feature.global_position = local_to_world(grid_pos) features[grid_pos] = feature # Probe feature - features[grid_pos].probe() - - -func local_to_world(local_pos: Vector2) -> Vector3: - return global_transform * Vector3(local_pos.x * GRID_SIZE.x, 0, local_pos.y * GRID_SIZE.y) - - -func world_to_local(world_pos: Vector3) -> Vector2: - var rel_pos := world_pos * global_transform - return Vector2(rel_pos.x / GRID_SIZE.x, rel_pos.z / GRID_SIZE.y) + return features[grid_pos].generate(lod) diff --git a/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.tscn b/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.tscn index 98979b8..43551bc 100644 --- a/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.tscn +++ b/src/world/generation/layer/arcology_grid_layer/arcology_grid_layer.tscn @@ -8,4 +8,6 @@ script = ExtResource("1_85eh3") arcology_feature_scene = ExtResource("2_pxyh5") empty_feature_scene = ExtResource("3_p6bke") +grid_size = Vector2(100000, 100000) +noise_scale = Vector3(2, 2, 2) metadata/_custom_type_script = "uid://kp8r23lsylpk" diff --git a/src/world/generation/layer/generation_layer.gd b/src/world/generation/layer/generation_layer.gd index e256b62..184648f 100644 --- a/src/world/generation/layer/generation_layer.gd +++ b/src/world/generation/layer/generation_layer.gd @@ -1,7 +1,4 @@ -class_name GenerationLayer extends Node3D +class_name GenerationLayer extends GeneratedElement ## A composite layer of world generation logic. - -## Probe this layer and any sub-layers at the world generation point, generating features as needed. -func probe() -> void: - pass # Implement in derived type +# TODO what else goes here? diff --git a/src/world/generation/layer/grid_layer/bounded_grid_layer.gd b/src/world/generation/layer/grid_layer/bounded_grid_layer.gd new file mode 100644 index 0000000..26ee6b1 --- /dev/null +++ b/src/world/generation/layer/grid_layer/bounded_grid_layer.gd @@ -0,0 +1,104 @@ +@tool +class_name BoundedGridLayer extends GridLayer +## A layer that generates tiles in a locally-constrained grid. + +## Bounding box for the grid on the local XZ plane. +## +## Note that only feature handles are checked to be within the bounding box. +## The Y component of this AABB is not used. +@export var bounding_box: AABB: + set(value): + bounding_box = value + if _debug_box: + _set_debug_box_shape(bounding_box) + +@export_group("Debug Draw") +@export var draw_debug := true +@export var debug_color := Color("#a486006b"): + set(value): + debug_color = value + if _debug_box: + _set_debug_box_color(debug_color) + +var _debug_meshinstance: MeshInstance3D +var _debug_box: BoxMesh + + +func _ready() -> void: + if Engine.is_editor_hint(): + _init_debug_draw() + else: + var grid := _plane_size() + for lod in WorldGen.LOD_LIST: + for i in grid.x: + for j in grid.y: + _generation_grid[lod][Vector2(i, j)] = false + + +func _generate(lod: WorldGen.LOD) -> bool: + var center := WorldGenManager.get_generation_point() + var radius := WorldGenManager.get_lod_radius(lod) + var rad_diff := Vector3(radius, 0, radius) + + # Translate probe box limits to grid space + var a := world_to_local(center - rad_diff).floor() + var b := world_to_local(center + rad_diff).floor() + var grid_low := Vector2(minf(a.x, b.x), minf(a.y, b.y)) + var grid_high := Vector2(maxf(a.x, b.x), maxf(a.y, b.y)) + var grid_max := _plane_size().floor() + + # Constrain to bounding box + var x_min := maxf(grid_low.x, 0) + var x_max := minf(grid_high.x + 1, grid_max.x) + var y_min := maxf(grid_low.y, 0) + var y_max := minf(grid_high.y + 1, grid_max.y) + + # Probe everything within radius + for i in range(x_min, x_max): + for j in range(y_min, y_max): + var pt := Vector2(i, j) + if pt in _generation_grid[lod]: + if generate_grid(lod, pt): + _generation_grid[lod].erase(pt) + + # Return false if there are still grid points to be generated + return not _generation_grid[lod] + + +func _plane_size() -> Vector2: + return Vector2(bounding_box.size.x / grid_size.x, bounding_box.size.z / grid_size.y) + + +func local_to_world(local_pos: Vector2) -> Vector3: + var v3_pos := Vector3(local_pos.x * grid_size.x, 0, local_pos.y * grid_size.y) + return global_transform * (bounding_box.position + v3_pos) + + +func world_to_local(world_pos: Vector3) -> Vector2: + var rel_pos := world_pos * global_transform - bounding_box.position + return Vector2(rel_pos.x / grid_size.x, rel_pos.z / grid_size.y) + + +#region debug draw +func _init_debug_draw() -> void: + _debug_box = BoxMesh.new() + var mat := StandardMaterial3D.new() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + _debug_box.material = mat + _debug_meshinstance = MeshInstance3D.new() + _debug_meshinstance.mesh = _debug_box + add_child(_debug_meshinstance) + _set_debug_box_color(debug_color) + _set_debug_box_shape(bounding_box) + + +func _set_debug_box_color(color: Color) -> void: + var mat := _debug_box.material as StandardMaterial3D + mat.albedo_color = color + + +func _set_debug_box_shape(aabb: AABB) -> void: + _debug_meshinstance.position = aabb.position + aabb.size / 2 + _debug_box.size = aabb.size +#endregion diff --git a/src/world/generation/layer/grid_layer/bounded_grid_layer.gd.uid b/src/world/generation/layer/grid_layer/bounded_grid_layer.gd.uid new file mode 100644 index 0000000..994f559 --- /dev/null +++ b/src/world/generation/layer/grid_layer/bounded_grid_layer.gd.uid @@ -0,0 +1 @@ +uid://bo2pmuy3dn5y4 diff --git a/src/world/generation/layer/grid_layer/grid_layer.gd b/src/world/generation/layer/grid_layer/grid_layer.gd index cbd1570..d3a1b8e 100644 --- a/src/world/generation/layer/grid_layer/grid_layer.gd +++ b/src/world/generation/layer/grid_layer/grid_layer.gd @@ -1,44 +1,19 @@ -@tool class_name GridLayer extends GenerationLayer -## A layer that generates tiles in a locally-constrained grid. - -## Bounding box for the grid on the local XZ plane. -## -## Note that only feature handles are checked to be within the bounding box. -## The Y component of this AABB is not used. -@export var bounding_box: AABB: - set(value): - bounding_box = value - if _debug_box: - _set_debug_box_shape(bounding_box) +## A layer that generates tiles on an infinite grid ## Size of a grid tile in the local XZ plane. @export var grid_size: Vector2 -@export_group("Debug Draw") -@export var draw_debug := true -@export var debug_color := Color("#a486006b"): - set(value): - debug_color = value - if _debug_box: - _set_debug_box_color(debug_color) - -var _debug_meshinstance: MeshInstance3D -var _debug_box: BoxMesh +var _generation_grid: Dictionary[WorldGen.LOD, Dictionary] = { + WorldGen.LOD.LOW: {}, + WorldGen.LOD.MEDIUM: {}, + WorldGen.LOD.HIGH: {}, +} -func _ready() -> void: - if Engine.is_editor_hint(): - _init_debug_draw() - - -func probe() -> void: - var world_pos := WorldGenManager.get_generation_point() - probe_radius(world_pos, WorldGenManager.med_detail_radius) - # TODO high-detail & low-detail probes - - -func probe_radius(center: Vector3, radius: float) -> void: +func _generate(lod: WorldGen.LOD) -> bool: + var center := WorldGenManager.get_generation_point() + var radius := WorldGenManager.get_lod_radius(lod) var rad_diff := Vector3(radius, 0, radius) # Translate probe box limits to grid space @@ -46,73 +21,35 @@ func probe_radius(center: Vector3, radius: float) -> void: var b := world_to_local(center + rad_diff).floor() var grid_low := Vector2(minf(a.x, b.x), minf(a.y, b.y)) var grid_high := Vector2(maxf(a.x, b.x), maxf(a.y, b.y)) - var grid_max := _plane_size().floor() - - # Constrain to bounding box - var x_min := maxf(grid_low.x, 0) - var x_max := minf(grid_high.x + 1, grid_max.x) - var y_min := maxf(grid_low.y, 0) - var y_max := minf(grid_high.y + 1, grid_max.y) # Probe everything within radius - for i in range(x_min, x_max): - for j in range(y_min, y_max): - probe_grid(Vector2(i, j)) + for i in range(grid_low.x, grid_high.x + 1): + for j in range(grid_low.y, grid_high.y + 1): + var pt := Vector2(i, j) + if pt not in _generation_grid[lod]: + if generate_grid(lod, pt): + _generation_grid[lod][pt] = true + + # The grid is infinite, so there will always be more to generate + return false -func probe_grid(_grid_pos: Vector2) -> void: - pass # Implement in derived type - - -func _plane_size() -> Vector2: - return Vector2(bounding_box.size.x / grid_size.x, bounding_box.size.z / grid_size.y) - - -func is_bounding_box_valid() -> bool: - return bounding_box.size.length_squared() > 0 - - -func is_point_in_bounds(local_pos: Vector2) -> bool: - local_pos = local_pos.floor() - var grid_max := _plane_size() - return ( - 0 <= local_pos.x - and local_pos.x < grid_max.x - and 0 <= local_pos.y - and local_pos.y < grid_max.y - ) +## Generate the given grid position. +## +## Like `generate`, this returns `true` when generation is complete. +func generate_grid(_lod: WorldGen.LOD, _grid_pos: Vector2) -> bool: + return true # Implement in derived type func local_to_world(local_pos: Vector2) -> Vector3: var v3_pos := Vector3(local_pos.x * grid_size.x, 0, local_pos.y * grid_size.y) - return global_transform * (bounding_box.position + v3_pos) + return global_transform * v3_pos func world_to_local(world_pos: Vector3) -> Vector2: - var rel_pos := world_pos * global_transform - bounding_box.position + var rel_pos := world_pos * global_transform return Vector2(rel_pos.x / grid_size.x, rel_pos.z / grid_size.y) -#region debug draw -func _init_debug_draw() -> void: - _debug_box = BoxMesh.new() - var mat := StandardMaterial3D.new() - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - _debug_box.material = mat - _debug_meshinstance = MeshInstance3D.new() - _debug_meshinstance.mesh = _debug_box - add_child(_debug_meshinstance) - _set_debug_box_color(debug_color) - _set_debug_box_shape(bounding_box) - - -func _set_debug_box_color(color: Color) -> void: - var mat := _debug_box.material as StandardMaterial3D - mat.albedo_color = color - - -func _set_debug_box_shape(aabb: AABB) -> void: - _debug_meshinstance.position = aabb.position + aabb.size / 2 - _debug_box.size = aabb.size -#endregion +func sample_noise_at_grid(grid_pos: Vector2) -> float: + return sample_noise_at(local_to_world(grid_pos)) diff --git a/src/world/generation/layer/grid_layer/grid_layer.gd.uid b/src/world/generation/layer/grid_layer/grid_layer.gd.uid index 994f559..ef6fb77 100644 --- a/src/world/generation/layer/grid_layer/grid_layer.gd.uid +++ b/src/world/generation/layer/grid_layer/grid_layer.gd.uid @@ -1 +1 @@ -uid://bo2pmuy3dn5y4 +uid://nq1cuemdrtn2 diff --git a/src/world/generation/layer/metro_grid_layer/metro_grid_layer.gd b/src/world/generation/layer/metro_grid_layer/metro_grid_layer.gd index c6e7c2c..5c9c565 100644 --- a/src/world/generation/layer/metro_grid_layer/metro_grid_layer.gd +++ b/src/world/generation/layer/metro_grid_layer/metro_grid_layer.gd @@ -1,13 +1,10 @@ @tool -extends GridLayer +extends BoundedGridLayer ## Procedural cityscape generation @export var empty_scene: PackedScene @export var simple_construct_scene: PackedScene -@export var noise_scale := Vector3.ONE -@export var noise_offset := Vector3.ZERO - @export_group("Construct Parameters") @export var simple_construct_threshold := 0.3 @export var construct_height_factor_x: Curve @@ -26,11 +23,10 @@ func generate_simple_construct(grid_pos: Vector2) -> MetroConstructSimple: return instance -func probe_grid(grid_pos: Vector2) -> void: +func generate_grid(lod: WorldGen.LOD, grid_pos: Vector2) -> bool: if grid_pos not in features: var feature: GenerationFeature - var sample_point := local_to_world(grid_pos) * noise_scale + noise_offset - var sample := absf(WorldGenManager.noise.get_noise_3dv(sample_point)) + var sample := absf(sample_noise_at(local_to_world(grid_pos))) if sample > simple_construct_threshold: feature = generate_simple_construct(grid_pos) else: @@ -38,4 +34,4 @@ func probe_grid(grid_pos: Vector2) -> void: add_child(feature) feature.global_position = local_to_world(grid_pos) features[grid_pos] = feature - features[grid_pos].probe() + return features[grid_pos].generate(lod) diff --git a/src/world/generation/layer/metro_grid_layer/metro_grid_layer.tscn b/src/world/generation/layer/metro_grid_layer/metro_grid_layer.tscn index 61fb63e..29b196c 100644 --- a/src/world/generation/layer/metro_grid_layer/metro_grid_layer.tscn +++ b/src/world/generation/layer/metro_grid_layer/metro_grid_layer.tscn @@ -9,3 +9,4 @@ script = ExtResource("1_ng6r6") empty_scene = ExtResource("2_47xjc") simple_construct_scene = ExtResource("3_woad2") grid_size = Vector2(64, 64) +generated_lods = 6 diff --git a/src/world/generation/worldgen_manager/worldgen_manager.gd b/src/world/generation/worldgen_manager/worldgen_manager.gd index 71555d9..65e9ce1 100644 --- a/src/world/generation/worldgen_manager/worldgen_manager.gd +++ b/src/world/generation/worldgen_manager/worldgen_manager.gd @@ -1,6 +1,16 @@ -class_name WorldGenManagerType extends Node +class_name WorldGen extends Node ## Global autoloaded singleton controller for worldgen parameters +## Worldgen uses custom levels of detail, distinct from mesh LODs. +enum LOD { + LOW = 0b001, + MEDIUM = 0b010, + HIGH = 0b100, +} + +## List of LODs, for iteration convenience. +const LOD_LIST: Array[WorldGen.LOD] = [LOD.LOW, LOD.MEDIUM, LOD.HIGH] + @export var noise: FastNoiseLite ## Generate features with the lowest detail, like large distant structures, within this radius @@ -19,3 +29,15 @@ func get_generation_point() -> Vector3: if camera: return camera.global_position return Vector3.ZERO + + +## Get the generation radius for a given LOD. +func get_lod_radius(lod: LOD) -> float: + match lod: + LOD.LOW: + return low_detail_radius + LOD.MEDIUM: + return med_detail_radius + LOD.HIGH: + return high_detail_radius + return -1