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