Reorganized worldgen structure hierarchy around generation LODs

This commit is contained in:
Rob Kelly 2025-10-04 16:49:49 -06:00
parent e2d75f394d
commit 64a9292f9f
23 changed files with 305 additions and 280 deletions

View File

@ -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()

View File

@ -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

View File

@ -1 +0,0 @@
uid://chqpqe4anvamd

View File

@ -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")

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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)

View File

@ -0,0 +1 @@
uid://c7x4yin558csb

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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?

View File

@ -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

View File

@ -0,0 +1 @@
uid://bo2pmuy3dn5y4

View File

@ -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))

View File

@ -1 +1 @@
uid://bo2pmuy3dn5y4
uid://nq1cuemdrtn2

View File

@ -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)

View File

@ -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

View File

@ -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