Implements procedural ground tile generation

Adds procedural ground tile generation with chunking for improved performance.

Includes:
- Ground tile entity with debug text and cell information
- Grass and tree placement based on cell data
- Ground shader for visual representation
- Chunk loading and unloading system based on player position
This commit is contained in:
Dan Baker 2025-06-24 13:14:21 +01:00
parent 95665f54eb
commit b5bf7619e6
21 changed files with 532 additions and 149 deletions

View file

@ -0,0 +1,16 @@
shader_type spatial;
uniform sampler2D noise_texture : filter_linear_mipmap;
uniform sampler2D gradient_texture : filter_linear;
uniform float noise_scale : hint_range(0.1, 10.0) = 1.0;
uniform float roughness_value : hint_range(0.0, 1.0) = 0.8;
void fragment() {
vec2 noise_uv = UV * noise_scale;
float noise_val = texture(noise_texture, noise_uv).r;
vec4 earth_color = texture(gradient_texture, vec2(noise_val, 0.0));
ALBEDO = earth_color.rgb;
ROUGHNESS = roughness_value;
}

View file

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

View file

@ -1,43 +1,84 @@
[gd_scene load_steps=7 format=3 uid="uid://bwcevwwphdvq"]
[gd_scene load_steps=18 format=3 uid="uid://bwcevwwphdvq"]
[ext_resource type="Script" uid="uid://bq7hia2dit80y" path="res://Entities/GroundTile/ground_tile.gd" id="1_uwxqs"]
[ext_resource type="PackedScene" uid="uid://ckesk3bs6g7tt" path="res://Stages/Test3D/assets/fish.glb" id="2_h4g11"]
[ext_resource type="ArrayMesh" uid="uid://duj6747nq4qsk" path="res://Stages/Test3D/assets/stylizedGrassMeshes/grass.res" id="3_8mhad"]
[ext_resource type="Script" uid="uid://cacp8ncwuofuj" path="res://Entities/GroundTile/scripts/grass.gd" id="3_224hx"]
[ext_resource type="Material" uid="uid://b1miqvl8lus75" path="res://Stages/Test3D/GrassMaterialOverride.tres" id="3_f37ob"]
[ext_resource type="Script" uid="uid://btju6b83mvgvk" path="res://Entities/GroundTile/scripts/grass_multimesh.gd" id="4_3wpcb"]
[ext_resource type="PackedScene" uid="uid://dgvycnw8hpebx" path="res://Stages/Test3D/assets/tree-tall.glb" id="6_7lc7k"]
[ext_resource type="Script" uid="uid://cqko4m7cbxsfb" path="res://Entities/GroundTile/scripts/trees.gd" id="7_7lc7k"]
[ext_resource type="PackedScene" uid="uid://cccqxa0y0ksju" path="res://Stages/Test3D/assets/tree-trunk.glb" id="7_jysav"]
[ext_resource type="PackedScene" uid="uid://cwfp2cf8no8fi" path="res://Stages/Test3D/assets/tree.glb" id="8_q0r4p"]
[ext_resource type="PackedScene" uid="uid://bwdibgbi3ycqn" path="res://Stages/Test3D/assets/tree-autumn-tall.glb" id="12_4hjaq"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_oqd8f"]
albedo_color = Color(0.171, 0.57, 0.24415, 1)
[sub_resource type="ViewportTexture" id="ViewportTexture_h4g11"]
viewport_path = NodePath("DebugText/DebugTextViewport")
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_f37ob"]
albedo_color = Color(0.706084, 0.408439, 0.274016, 1)
metallic = 1.0
[sub_resource type="PlaneMesh" id="PlaneMesh_oqd8f"]
[sub_resource type="BoxShape3D" id="BoxShape3D_h4g11"]
size = Vector3(2, 2, 2)
[sub_resource type="ViewportTexture" id="ViewportTexture_h4g11"]
viewport_path = NodePath("DebugText")
[sub_resource type="MultiMesh" id="MultiMesh_3wpcb"]
transform_format = 1
mesh = ExtResource("3_8mhad")
[sub_resource type="PlaneMesh" id="PlaneMesh_f37ob"]
flip_faces = true
[node name="GroundTile" type="Node3D"]
script = ExtResource("1_uwxqs")
[node name="DebugText" type="Node3D" parent="."]
visible = false
[node name="DebugTextViewport" type="SubViewport" parent="DebugText"]
size = Vector2i(50, 50)
[node name="DebugTextLabel" type="Label" parent="DebugText/DebugTextViewport"]
offset_right = 40.0
offset_bottom = 23.0
text = "Hello world"
[node name="DebugTextBillboard" type="Sprite3D" parent="DebugText"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0461426, 0.199493, 0.0660986)
texture = SubResource("ViewportTexture_h4g11")
[node name="Ground" type="MeshInstance3D" parent="."]
material_override = SubResource("StandardMaterial3D_oqd8f")
material_override = SubResource("StandardMaterial3D_f37ob")
cast_shadow = 0
mesh = SubResource("PlaneMesh_oqd8f")
[node name="GroundCollision" type="StaticBody3D" parent="."]
[node name="GroundCollision" type="StaticBody3D" parent="Ground"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="GroundCollision"]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground/GroundCollision"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
shape = SubResource("BoxShape3D_h4g11")
[node name="fish2" parent="." instance=ExtResource("2_h4g11")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00713956, 0, 0.0313604)
[node name="DebugText" type="SubViewport" parent="."]
size = Vector2i(50, 50)
[node name="Grass" type="Node3D" parent="."]
script = ExtResource("3_224hx")
[node name="DebugTextLabel" type="Label" parent="DebugText"]
offset_right = 40.0
offset_bottom = 23.0
text = "Hello world"
[node name="GrassMultimesh" type="MultiMeshInstance3D" parent="Grass"]
material_override = ExtResource("3_f37ob")
cast_shadow = 0
multimesh = SubResource("MultiMesh_3wpcb")
script = ExtResource("4_3wpcb")
[node name="Sprite3D" type="Sprite3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.0939356)
texture = SubResource("ViewportTexture_h4g11")
[node name="GrassTarget" type="MeshInstance3D" parent="Grass"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0111763, 0)
mesh = SubResource("PlaneMesh_f37ob")
[node name="Trees" type="Node3D" parent="."]
script = ExtResource("7_7lc7k")
tree_scenes = Array[PackedScene]([ExtResource("8_q0r4p"), ExtResource("6_7lc7k"), ExtResource("12_4hjaq"), ExtResource("8_q0r4p"), ExtResource("8_q0r4p"), ExtResource("8_q0r4p"), ExtResource("8_q0r4p")])
[node name="tree-trunk2" parent="." instance=ExtResource("7_jysav")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.554052, 0, 0.470964)

BIN
Entities/GroundTile/assets/Dirt_05-256x256.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b8pca7c2vlwaa"
path.s3tc="res://.godot/imported/Dirt_05-256x256.png-df3829dddfb4eeab82c63b0845e8a300.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://Entities/GroundTile/assets/Dirt_05-256x256.png"
dest_files=["res://.godot/imported/Dirt_05-256x256.png-df3829dddfb4eeab82c63b0845e8a300.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View file

@ -1,26 +1,29 @@
# GroundTile.gd
class_name GroundTile
extends Node3D
@onready var debug_text: Label = $DebugText/DebugTextLabel
@onready var debug_text: Label = $DebugText/DebugTextViewport/DebugTextLabel
var grid_x: int
var grid_z: int
var cell_info: CellDataResource
var cell_info: CellDataResource = null:
set(value):
cell_info = value
if cell_info != null:
cell_info_updated.emit(value)
var rng: RandomClass = RandomClass.new()
signal cell_info_updated(value)
func _ready() -> void:
if cell_info != null:
update_text_label()
rng.set_seed(cell_info.cell_seed)
func set_grid_location(x, z) -> void:
grid_x = x
grid_z = z
cell_info = MapData.get_map_data(grid_x, grid_z)
var debug_dict = {}
if cell_info.get_script():
var script_properties = cell_info.get_script().get_script_property_list()
for prop in script_properties:
debug_dict[prop.name] = cell_info.get(prop.name)
Log.pr(debug_dict)
func update_text_label() -> void:
debug_text.text = str(grid_x) + ', ' + str(grid_z) + ', ' + str(cell_info.cell_seed)

View file

@ -0,0 +1,50 @@
# GrassController.gd
extends Node3D
class_name GrassController
# 229379
var parent_node: GroundTile = null
var grass_density: float = 0.5
var grass_instance_range: int = 10
signal grass_data_ready()
func _ready() -> void:
# Use call_deferred to ensure parent is fully ready
call_deferred("_setup_connections")
func _setup_connections() -> void:
parent_node = get_parent() as GroundTile
if parent_node == null:
Log.pr("Error: Parent node is not a GroundTile!")
return
parent_node.cell_info_updated.connect(_on_parent_data_changed)
# Check if cell_info already exists and process it
if parent_node.cell_info != null:
_on_parent_data_changed(parent_node.cell_info)
func _on_parent_data_changed(value):
if value == null:
return
grass_density = value.vegetation_density
update_grass_density()
grass_data_ready.emit()
func update_grass_density() -> void:
if parent_node == null or parent_node.rng == null:
return
if grass_density > 0.8:
grass_instance_range = parent_node.rng.randi_range(100, 500)
elif grass_density > 0.6:
grass_instance_range = parent_node.rng.randi_range(30, 50)
elif grass_density > 0.3:
grass_instance_range = parent_node.rng.randi_range(5, 20)
else:
grass_instance_range = parent_node.rng.randi_range(0, 1)

View file

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

View file

@ -0,0 +1,66 @@
# GrassMultiMesh.gd (assuming this is your MultiMeshInstance3D script)
extends MultiMeshInstance3D
var mm: MultiMesh
@onready var parent_node: GrassController
func _ready() -> void:
# Use call_deferred to ensure proper initialization order
call_deferred("_setup_connections")
func _setup_connections() -> void:
parent_node = get_parent() as GrassController
if parent_node == null:
Log.pr("Error: Parent node is not a GrassController!")
return
parent_node.grass_data_ready.connect(_on_grass_data_ready)
Log.pr("Connected to grass_data_ready signal")
func _on_grass_data_ready():
Log.pr("Received grass_data_ready signal, setting up multimesh")
setup_multimesh()
func setup_multimesh() -> void:
if parent_node == null:
Log.pr("Error: Parent node not available in setup_multimesh")
return
# Load the mesh resource directly
var mesh = load("res://Stages/Test3D/assets/stylizedGrassMeshes/grass2_mesh.res")
if mesh == null:
Log.pr("Error: Could not load grass mesh")
return
# Create new MultiMesh instance
mm = MultiMesh.new()
# Configure the MultiMesh
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.instance_count = parent_node.grass_instance_range
mm.mesh = mesh
Log.pr("Setting up MultiMesh with " + str(mm.instance_count) + " instances")
# Generate random positions for grass
for i in range(mm.instance_count):
var random_pos = Vector3(
randf_range(-1.0, 1.0),
0.0,
randf_range(-1.0, 1.0)
)
var random_rotation = randf_range(0.0, TAU)
var basis = Basis(Vector3.UP, random_rotation)
var random_scale = randf_range(0.05, 0.3)
basis = basis.scaled(Vector3(random_scale, random_scale, random_scale))
var tx = Transform3D(basis, random_pos)
mm.set_instance_transform(i, tx)
# Assign the MultiMesh to this node
multimesh = mm
cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
Log.pr("MultiMesh setup complete with " + str(multimesh.instance_count) + " instances")

View file

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

View file

@ -0,0 +1,105 @@
extends Node3D
# Array of tree scenes to randomly choose from
@export var tree_scenes: Array[PackedScene] = []
@export var spawn_area_size: Vector2 = Vector2(2.0, 2.0) # 2x2 area
@export var max_trees: int = 3 # Maximum possible trees
@export var min_distance: float = 0.5 # Minimum distance between trees
var spawned_positions: Array[Vector3] = []
var parent_ground_tile: GroundTile
var rng: RandomClass
func _ready():
# Get reference to parent GroundTile
parent_ground_tile = get_parent() as GroundTile
if parent_ground_tile:
# Connect to the signal
parent_ground_tile.cell_info_updated.connect(_on_cell_info_updated)
# If cell_info already exists, spawn trees immediately
if parent_ground_tile.cell_info != null:
_on_cell_info_updated(parent_ground_tile.cell_info)
func _on_cell_info_updated(cell_info: CellDataResource):
# Initialize RNG with the cell's seed for consistent results
rng = RandomClass.new()
rng.set_seed(cell_info.cell_seed)
# Calculate number of trees based on vegetation_density
var tree_count = calculate_tree_count(cell_info.vegetation_density)
# Spawn the trees
spawn_trees(tree_count)
func calculate_tree_count(vegetation_density: float) -> int:
if vegetation_density < 0.7:
return 0
# vegetation_density should be between 0.0 and 1.0
# Scale it to our max_trees range
var scaled_count = vegetation_density * max_trees
# Round to nearest integer, but ensure at least 0
return max(0, int(round(scaled_count)))
func spawn_trees(tree_count: int):
if tree_scenes.is_empty():
print("No tree scenes assigned!")
return
if tree_count == 0:
print("No trees to spawn (vegetation_density too low)")
return
# Clear any existing trees
clear_trees()
# Try to place trees
var attempts = 0
var max_attempts = tree_count * 10 # Prevent infinite loops
while spawned_positions.size() < tree_count and attempts < max_attempts:
var pos = get_random_position()
if is_position_valid(pos):
spawn_tree_at_position(pos)
spawned_positions.append(pos)
attempts += 1
print("Spawned ", spawned_positions.size(), " trees")
func get_random_position() -> Vector3:
var x = rng.randf_range(-spawn_area_size.x / 2, spawn_area_size.x / 2)
var z = rng.randf_range(-spawn_area_size.y / 2, spawn_area_size.y / 2)
return Vector3(x, 0, z)
func is_position_valid(pos: Vector3) -> bool:
# Check if position is too close to existing trees
for existing_pos in spawned_positions:
if pos.distance_to(existing_pos) < min_distance:
return false
return true
func spawn_tree_at_position(pos: Vector3):
# Pick a random tree scene using the seeded RNG
var random_index = rng.randi() % tree_scenes.size()
var random_tree_scene = tree_scenes[random_index]
var tree_instance = random_tree_scene.instantiate()
add_child(tree_instance)
tree_instance.position = pos
# Optional: Add some random rotation using seeded RNG
tree_instance.rotation.y = rng.randf() * TAU
func clear_trees():
# Remove all existing tree children
for child in get_children():
child.queue_free()
spawned_positions.clear()
# Call this if you want to respawn trees manually
func respawn_trees():
if parent_ground_tile and parent_ground_tile.cell_info:
_on_cell_info_updated(parent_ground_tile.cell_info)

View file

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