Files
Trinittu/scripts/track_generator.gd
JMP Games e1a95d7417 Init
2026-01-22 08:34:02 +01:00

467 lines
13 KiB
GDScript

extends Node3D
class_name TrackGenerator
## Generatore procedurale di binari ferroviari
# Configurazione binari
@export var track_width: float = 2.0
@export var rail_height: float = 0.15
@export var rail_width: float = 0.1
@export var sleeper_spacing: float = 1.0
@export var sleeper_width: float = 2.4
@export var sleeper_height: float = 0.1
@export var sleeper_depth: float = 0.25
# Configurazione generazione
@export var segment_length: float = 20.0
@export var curve_points: int = 24
@export var min_curve_radius: float = 50.0
@export var max_curve_radius: float = 80.0
@export var max_curve_angle: float = 30.0
@export var straight_probability: float = 0.5
@export var segments_ahead: int = 12
@export var segments_behind: int = 4
# Materiali
var rail_material: StandardMaterial3D
var sleeper_material: StandardMaterial3D
var ballast_material: StandardMaterial3D
# Stato del tracciato
var segments: Array = []
var total_length: float = 0.0
# Posizione corrente della "testa" del binario
var head_position: Vector3 = Vector3.ZERO
var head_direction: Vector3 = Vector3(0, 0, -1) # Avanti in Godot
# Per curve più naturali
var last_curve_dir: int = 0
func _ready() -> void:
_setup_materials()
_generate_initial_track()
func _setup_materials() -> void:
rail_material = StandardMaterial3D.new()
rail_material.albedo_color = Color(0.35, 0.35, 0.4)
rail_material.metallic = 0.8
rail_material.roughness = 0.3
sleeper_material = StandardMaterial3D.new()
sleeper_material.albedo_color = Color(0.4, 0.28, 0.18)
sleeper_material.roughness = 0.9
ballast_material = StandardMaterial3D.new()
ballast_material.albedo_color = Color(0.55, 0.5, 0.45)
ballast_material.roughness = 1.0
func _generate_initial_track() -> void:
for i in range(segments_ahead):
_add_next_segment()
func _add_next_segment() -> void:
var is_curve = randf() > straight_probability
var segment: Dictionary
if is_curve:
segment = _create_curve_segment()
else:
segment = _create_straight_segment()
# Crea la mesh visiva
_create_segment_mesh(segment)
# Aggiorna la testa del binario per il prossimo segmento
head_position = segment["end_position"]
head_direction = segment["end_direction"]
total_length += segment["length"]
segments.append(segment)
func _create_straight_segment() -> Dictionary:
var length = segment_length
var end_pos = head_position + head_direction * length
# Genera punti lungo il segmento dritto
var points: Array = []
var tangents: Array = []
var num_points = 10
for i in range(num_points + 1):
var t = float(i) / float(num_points)
var pos = head_position.lerp(end_pos, t)
points.append(pos)
tangents.append(head_direction)
return {
"start_position": head_position,
"end_position": end_pos,
"start_direction": head_direction,
"end_direction": head_direction,
"length": length,
"start_distance": total_length,
"is_curve": false,
"path_points": points,
"path_tangents": tangents,
"mesh_instance": null
}
func _create_curve_segment() -> Dictionary:
var radius = randf_range(min_curve_radius, max_curve_radius)
var angle_deg = randf_range(15.0, max_curve_angle)
# Alterna direzione curve per percorso naturale
var curve_dir: int
if last_curve_dir == 0:
curve_dir = 1 if randf() > 0.5 else -1
else:
curve_dir = -last_curve_dir if randf() > 0.3 else last_curve_dir
last_curve_dir = curve_dir
# Calcola il centro della curva
var right = head_direction.cross(Vector3.UP).normalized()
var center = head_position + right * radius * curve_dir
# Lunghezza dell'arco
var angle_rad = deg_to_rad(angle_deg)
var arc_length = radius * angle_rad
# Genera punti lungo la curva
var points: Array = []
var tangents: Array = []
for i in range(curve_points + 1):
var t = float(i) / float(curve_points)
var current_angle = angle_rad * t * curve_dir
# Posizione: ruota il vettore dal centro al punto iniziale
var start_to_center = head_position - center
var rotated_vec = _rotate_y(start_to_center, current_angle)
var pos = center + rotated_vec
# Tangente: ruota la direzione iniziale
var tangent = _rotate_y(head_direction, current_angle)
points.append(pos)
tangents.append(tangent)
var end_pos = points[points.size() - 1]
var end_dir = tangents[tangents.size() - 1]
return {
"start_position": head_position,
"end_position": end_pos,
"start_direction": head_direction,
"end_direction": end_dir,
"length": arc_length,
"start_distance": total_length,
"is_curve": true,
"path_points": points,
"path_tangents": tangents,
"mesh_instance": null
}
func _rotate_y(vec: Vector3, angle: float) -> Vector3:
var cos_a = cos(angle)
var sin_a = sin(angle)
return Vector3(
vec.x * cos_a - vec.z * sin_a,
vec.y,
vec.x * sin_a + vec.z * cos_a
)
func _create_segment_mesh(segment: Dictionary) -> void:
var mesh_instance = MeshInstance3D.new()
var array_mesh = ArrayMesh.new()
var rail_arrays = _generate_rails_mesh(segment)
if rail_arrays[Mesh.ARRAY_VERTEX].size() > 0:
array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, rail_arrays)
array_mesh.surface_set_material(array_mesh.get_surface_count() - 1, rail_material)
var sleeper_arrays = _generate_sleepers_mesh(segment)
if sleeper_arrays[Mesh.ARRAY_VERTEX].size() > 0:
array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, sleeper_arrays)
array_mesh.surface_set_material(array_mesh.get_surface_count() - 1, sleeper_material)
var ballast_arrays = _generate_ballast_mesh(segment)
if ballast_arrays[Mesh.ARRAY_VERTEX].size() > 0:
array_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, ballast_arrays)
array_mesh.surface_set_material(array_mesh.get_surface_count() - 1, ballast_material)
mesh_instance.mesh = array_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
add_child(mesh_instance)
segment["mesh_instance"] = mesh_instance
func _generate_rails_mesh(segment: Dictionary) -> Array:
var vertices = PackedVector3Array()
var normals = PackedVector3Array()
var uvs = PackedVector2Array()
var indices = PackedInt32Array()
var half_track = track_width / 2.0
var half_rail = rail_width / 2.0
var path_points: Array = segment["path_points"]
var path_tangents: Array = segment["path_tangents"]
for rail_side in [-1, 1]:
var rail_offset = half_track * rail_side
var vertex_offset = vertices.size()
for i in range(path_points.size()):
var pos: Vector3 = path_points[i]
var tangent: Vector3 = path_tangents[i]
var right = tangent.cross(Vector3.UP).normalized()
var base = pos + right * rail_offset
# 4 vertici per sezione
vertices.append(base - right * half_rail)
vertices.append(base + right * half_rail)
vertices.append(base + right * half_rail + Vector3.UP * rail_height)
vertices.append(base - right * half_rail + Vector3.UP * rail_height)
for _j in range(4):
normals.append(Vector3.UP)
var u = float(i) / float(path_points.size())
uvs.append(Vector2(0, u))
uvs.append(Vector2(1, u))
uvs.append(Vector2(1, u))
uvs.append(Vector2(0, u))
# Collega le sezioni
for i in range(path_points.size() - 1):
var base_idx = vertex_offset + i * 4
# Top
indices.append(base_idx + 2)
indices.append(base_idx + 3)
indices.append(base_idx + 7)
indices.append(base_idx + 2)
indices.append(base_idx + 7)
indices.append(base_idx + 6)
# Sides
indices.append(base_idx + 0)
indices.append(base_idx + 4)
indices.append(base_idx + 7)
indices.append(base_idx + 0)
indices.append(base_idx + 7)
indices.append(base_idx + 3)
indices.append(base_idx + 1)
indices.append(base_idx + 2)
indices.append(base_idx + 6)
indices.append(base_idx + 1)
indices.append(base_idx + 6)
indices.append(base_idx + 5)
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = indices
return arrays
func _generate_sleepers_mesh(segment: Dictionary) -> Array:
var vertices = PackedVector3Array()
var normals = PackedVector3Array()
var uvs = PackedVector2Array()
var indices = PackedInt32Array()
var seg_length: float = segment["length"]
var num_sleepers = max(1, int(seg_length / sleeper_spacing))
var path_points: Array = segment["path_points"]
var path_tangents: Array = segment["path_tangents"]
for i in range(num_sleepers):
var t = float(i) / float(num_sleepers)
var pos = _interpolate_path(path_points, t)
var tangent = _interpolate_path(path_tangents, t).normalized()
var right = tangent.cross(Vector3.UP).normalized()
var half_w = sleeper_width / 2.0
var half_d = sleeper_depth / 2.0
var base_idx = vertices.size()
var corners = [
pos + right * (-half_w) + tangent * (-half_d),
pos + right * half_w + tangent * (-half_d),
pos + right * half_w + tangent * half_d,
pos + right * (-half_w) + tangent * half_d,
]
# Bottom vertices
for c in corners:
vertices.append(c)
normals.append(Vector3.DOWN)
uvs.append(Vector2(0, 0))
# Top vertices
for c in corners:
vertices.append(c + Vector3.UP * sleeper_height)
normals.append(Vector3.UP)
uvs.append(Vector2(1, 1))
# Top face
indices.append(base_idx + 4)
indices.append(base_idx + 5)
indices.append(base_idx + 6)
indices.append(base_idx + 4)
indices.append(base_idx + 6)
indices.append(base_idx + 7)
# Side faces
for j in range(4):
var j_next = (j + 1) % 4
indices.append(base_idx + j)
indices.append(base_idx + j_next)
indices.append(base_idx + j_next + 4)
indices.append(base_idx + j)
indices.append(base_idx + j_next + 4)
indices.append(base_idx + j + 4)
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = indices
return arrays
func _generate_ballast_mesh(segment: Dictionary) -> Array:
var vertices = PackedVector3Array()
var normals = PackedVector3Array()
var uvs = PackedVector2Array()
var indices = PackedInt32Array()
var ballast_width = sleeper_width * 1.3
var ballast_y = -0.08
var path_points: Array = segment["path_points"]
var path_tangents: Array = segment["path_tangents"]
for i in range(path_points.size()):
var pos: Vector3 = path_points[i]
var tangent: Vector3 = path_tangents[i]
var right = tangent.cross(Vector3.UP).normalized()
var half_w = ballast_width / 2.0
vertices.append(pos + right * (-half_w) + Vector3.UP * ballast_y)
vertices.append(pos + right * half_w + Vector3.UP * ballast_y)
normals.append(Vector3.UP)
normals.append(Vector3.UP)
var u = float(i) / float(path_points.size())
uvs.append(Vector2(0, u))
uvs.append(Vector2(1, u))
for i in range(path_points.size() - 1):
var base_idx = i * 2
indices.append(base_idx)
indices.append(base_idx + 1)
indices.append(base_idx + 3)
indices.append(base_idx)
indices.append(base_idx + 3)
indices.append(base_idx + 2)
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_INDEX] = indices
return arrays
func _interpolate_path(points: Array, t: float) -> Vector3:
if points.size() < 2:
return points[0] if points.size() > 0 else Vector3.ZERO
t = clampf(t, 0.0, 1.0)
var idx = t * (points.size() - 1)
var i = int(idx)
var frac = idx - i
if i >= points.size() - 1:
return points[points.size() - 1]
var p1: Vector3 = points[i]
var p2: Vector3 = points[i + 1]
return p1.lerp(p2, frac)
func update_track(train_distance: float) -> void:
# Rimuovi segmenti vecchi
while segments.size() > 0:
var first: Dictionary = segments[0]
var seg_end = first["start_distance"] + first["length"]
if train_distance - seg_end > segment_length * segments_behind:
_remove_segment(first)
else:
break
# Aggiungi nuovi segmenti
while total_length - train_distance < segment_length * segments_ahead:
_add_next_segment()
func _remove_segment(segment: Dictionary) -> void:
var mesh: MeshInstance3D = segment["mesh_instance"]
if mesh:
mesh.queue_free()
segments.erase(segment)
func get_position_on_track(distance: float) -> Dictionary:
for segment in segments:
var seg_start: float = segment["start_distance"]
var seg_end = seg_start + segment["length"]
if distance >= seg_start and distance <= seg_end:
var seg_len: float = segment["length"]
var t = (distance - seg_start) / seg_len if seg_len > 0 else 0.0
var path_points: Array = segment["path_points"]
var path_tangents: Array = segment["path_tangents"]
return {
"position": _interpolate_path(path_points, t),
"direction": _interpolate_path(path_tangents, t).normalized(),
"valid": true
}
# Se oltre tutti i segmenti, usa l'ultimo punto
if segments.size() > 0:
var last: Dictionary = segments[segments.size() - 1]
return {
"position": last["end_position"],
"direction": last["end_direction"],
"valid": true
}
return {
"position": Vector3.ZERO,
"direction": Vector3(0, 0, -1),
"valid": false
}
func get_total_length() -> float:
return total_length