class_name OBJLoader extends RefCounted #Non si vedevano i materiali e le texture dei vagoni #Il problema era l'inversione della coordinata V nelle UV, perché: #OBJ/Blender: V=0 è in alto #Godot/OpenGL: V=0 è in basso static func load_obj(path: String) -> ArrayMesh: var file = FileAccess.open(path, FileAccess.READ) if not file: push_error("Impossibile aprire il file: " + path) return null # Directory base per i file relativi (mtl, texture) var base_dir = path.get_base_dir() # Dati geometrici globali var vertices: Array[Vector3] = [] var normals: Array[Vector3] = [] var uvs: Array[Vector2] = [] # Materiali caricati dal file .mtl var materials: Dictionary = {} # Mesh per ogni materiale: { "nome_materiale": { "v": [], "n": [], "uv": [] } } var surfaces: Dictionary = {} var current_material: String = "__default__" surfaces[current_material] = {"v": [], "n": [], "uv": []} while not file.eof_reached(): var line = file.get_line().strip_edges() if line.begins_with("#") or line.is_empty(): continue var parts = line.split(" ", false) if parts.is_empty(): continue match parts[0]: "mtllib": # File materiale var mtl_path = base_dir.path_join(parts[1]) materials = _load_mtl(mtl_path) "usemtl": # Usa materiale current_material = parts[1] if parts.size() > 1 else "__default__" if not surfaces.has(current_material): surfaces[current_material] = {"v": [], "n": [], "uv": []} "v": # Vertice vertices.append(Vector3( float(parts[1]), float(parts[2]), float(parts[3]) )) "vn": # Normale normals.append(Vector3( float(parts[1]), float(parts[2]), float(parts[3]) )) "vt": # UV uvs.append(Vector2( float(parts[1]), float(parts[2]) if parts.size() > 2 else 0.0 )) "f": # Faccia var face_vertices: Array = [] for i in range(1, parts.size()): face_vertices.append(_parse_face_vertex(parts[i], vertices, normals, uvs)) # Triangola e aggiungi alla surface corrente var surf = surfaces[current_material] for i in range(1, face_vertices.size() - 1): _add_vertex(face_vertices[0], surf["v"], surf["n"], surf["uv"]) _add_vertex(face_vertices[i], surf["v"], surf["n"], surf["uv"]) _add_vertex(face_vertices[i + 1], surf["v"], surf["n"], surf["uv"]) file.close() return _create_mesh(surfaces, materials) static func _load_mtl(path: String) -> Dictionary: var materials: Dictionary = {} var file = FileAccess.open(path, FileAccess.READ) if not file: push_warning("File MTL non trovato: " + path) return materials var current_mat: String = "" var base_dir = path.get_base_dir() while not file.eof_reached(): var line = file.get_line().strip_edges() if line.begins_with("#") or line.is_empty(): continue var parts = line.split(" ", false) if parts.is_empty(): continue match parts[0]: "newmtl": # Nuovo materiale current_mat = parts[1] materials[current_mat] = { "albedo": Color.WHITE, "metallic": 0.0, "roughness": 1.0, "emission": Color.BLACK, "alpha": 1.0, "texture": "", "normal_map": "", "roughness_map": "", "metallic_map": "" } "Kd": # Colore diffuso (albedo) if current_mat != "": materials[current_mat]["albedo"] = Color( float(parts[1]), float(parts[2]), float(parts[3]) ) "Ks": # Colore speculare → approssima metallic if current_mat != "": var spec = (float(parts[1]) + float(parts[2]) + float(parts[3])) / 3.0 materials[current_mat]["metallic"] = spec "Ns": # Esponente speculare → approssima roughness if current_mat != "": # Ns va da 0 a 1000 tipicamente, invertiamo per roughness var ns = float(parts[1]) materials[current_mat]["roughness"] = 1.0 - clampf(ns / 1000.0, 0.0, 1.0) "Ke": # Emissione if current_mat != "": materials[current_mat]["emission"] = Color( float(parts[1]), float(parts[2]), float(parts[3]) ) "d": # Opacità if current_mat != "": materials[current_mat]["alpha"] = float(parts[1]) "Tr": # Trasparenza (inverso di d) if current_mat != "": materials[current_mat]["alpha"] = 1.0 - float(parts[1]) "map_Kd": # Texture diffusa if current_mat != "": var tex_path = base_dir.path_join(parts[1]) materials[current_mat]["texture"] = tex_path "map_Bump", "bump", "map_bump": # Normal map if current_mat != "": # Gestisce anche "-bm 1.0 nomefile.png" var tex_path = parts[parts.size() - 1] materials[current_mat]["normal_map"] = base_dir.path_join(tex_path) "map_Pr", "map_Ns": # Roughness map if current_mat != "": materials[current_mat]["roughness_map"] = base_dir.path_join(parts[1]) "map_Pm": # Metallic map if current_mat != "": materials[current_mat]["metallic_map"] = base_dir.path_join(parts[1]) file.close() return materials static func _parse_face_vertex(data: String, vertices: Array, normals: Array, uvs: Array) -> Dictionary: var parts = data.split("/") var result = {"v": null, "vt": null, "vn": null} # Indice vertice (obbligatorio) var v_idx = int(parts[0]) if v_idx < 0: v_idx = vertices.size() + v_idx else: v_idx -= 1 result["v"] = vertices[v_idx] # Indice UV (opzionale) if parts.size() > 1 and not parts[1].is_empty(): var vt_idx = int(parts[1]) if vt_idx < 0: vt_idx = uvs.size() + vt_idx else: vt_idx -= 1 var uv = uvs[vt_idx] # INVERTI LA COORDINATA V result["vt"] = Vector2(uv.x, 1.0 - uv.y) # Indice normale (opzionale) if parts.size() > 2 and not parts[2].is_empty(): var vn_idx = int(parts[2]) if vn_idx < 0: vn_idx = normals.size() + vn_idx else: vn_idx -= 1 result["vn"] = normals[vn_idx] return result static func _add_vertex(data: Dictionary, mesh_v: Array, mesh_n: Array, mesh_uv: Array): mesh_v.append(data["v"]) if data["vn"]: mesh_n.append(data["vn"]) if data["vt"]: mesh_uv.append(data["vt"]) static func _create_mesh(surfaces: Dictionary, materials: Dictionary) -> ArrayMesh: var mesh = ArrayMesh.new() for mat_name in surfaces.keys(): var surf = surfaces[mat_name] var verts: Array = surf["v"] var norms: Array = surf["n"] var uv: Array = surf["uv"] if verts.is_empty(): continue var arrays = [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array(verts) if not norms.is_empty() and norms.size() == verts.size(): arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(norms) if not uv.is_empty() and uv.size() == verts.size(): arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array(uv) mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var material = _create_material(mat_name, materials) mesh.surface_set_material(mesh.get_surface_count() - 1, material) return mesh static func _create_material(mat_name: String, materials: Dictionary) -> StandardMaterial3D: var mat = StandardMaterial3D.new() # DEBUG: Disabilita culling e rendi visibile senza luce mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED # Ignora illuminazione if not materials.has(mat_name): mat.albedo_color = Color(0.8, 0.8, 0.8) return mat var data: Dictionary = materials[mat_name] mat.albedo_color = data["albedo"] mat.metallic = data["metallic"] mat.roughness = data["roughness"] if data["emission"] != Color.BLACK: mat.emission_enabled = true mat.emission = data["emission"] if data["alpha"] < 1.0: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color.a = data["alpha"] if data["texture"] != "": var tex = _load_texture(data["texture"]) if tex: mat.albedo_texture = tex return mat static func _load_texture(path: String) -> Texture2D: # Metodo 1: Risorsa importata da Godot (per res://) if ResourceLoader.exists(path): var res = ResourceLoader.load(path) if res is Texture2D: return res elif res is Image: return ImageTexture.create_from_image(res) # Metodo 2: Prova il percorso senza spazi var clean_path = path.replace(" ", "%20") if clean_path != path and ResourceLoader.exists(clean_path): var res = ResourceLoader.load(clean_path) if res is Texture2D: return res # Metodo 3: File raw (per user:// o path assoluti) if FileAccess.file_exists(path): var file = FileAccess.open(path, FileAccess.READ) if not file: push_warning("Impossibile aprire: " + path) return null var buffer = file.get_buffer(file.get_length()) file.close() var image = Image.new() var error: Error var ext = path.get_extension().to_lower() match ext: "png": error = image.load_png_from_buffer(buffer) "jpg", "jpeg": error = image.load_jpg_from_buffer(buffer) "webp": error = image.load_webp_from_buffer(buffer) "bmp": error = image.load_bmp_from_buffer(buffer) "tga": error = image.load_tga_from_buffer(buffer) _: push_warning("Formato non supportato: " + ext) return null if error != OK: push_warning("Errore caricamento: " + str(error)) return null return ImageTexture.create_from_image(image) push_warning("Texture NON trovata: " + path) return null