1. OpenStreetMap数据解析基础想要在Godot中构建3D城市第一步就是理解OpenStreetMap的数据结构。OSM的数据就像乐高积木由三种基本模块组成节点(Node)、路径(Way)和关系(Relation)。我刚开始接触时也觉得这些概念很抽象直到用实际项目数据做了几次实验才真正掌握。节点是最基础的元素相当于地图上的一个坐标点。比如这个XML片段定义了一个路灯的位置node id123 lat39.907 lon116.391 tag khighway vstreet_lamp/ /node每个节点都有唯一的ID和经纬度坐标通过标签(tag)来描述具体属性。在实际项目中我发现路灯、交通信号灯这类点状物体都是用节点表示的。路径则是由多个节点串联而成的线。比如这段代码定义了一条道路way id456 nd ref123/ nd ref124/ nd ref125/ tag khighway vresidential/ /way有趣的是当路径的首尾节点相同时它就变成了一个闭合区域可以用来表示建筑物轮廓。我在处理建筑数据时经常遇到这种结构。关系是最复杂的结构它把多个元素组织在一起。比如这个公交路线关系relation id789 member typeway ref456 roleroute/ member typenode ref126 rolestop/ tag ktype vroute/ tag kroute vbus/ /relation2. 从OSM到Godot的坐标转换把OSM的经纬度坐标转换成Godot的3D空间坐标是个技术活。地球是圆的而游戏世界是平的这个转换过程需要考虑很多因素。我踩过几次坑后才总结出一套可靠的方法。首先需要理解墨卡托投影。OSM使用的是Web墨卡托(EPSG:3857)而Godot使用的是右手坐标系。这个转换公式我经常用func osm_to_godot(lat, lon, map_center): var earth_radius 6378137 # 地球半径(米) var x (lon - map_center.lon) * (earth_radius * PI / 180) var z -log(tan(PI/4 lat * PI/360)) * earth_radius z - -log(tan(PI/4 map_center.lat * PI/360)) * earth_radius return Vector3(x, 0, z)高度数据处理是另一个难点。OSM本身不包含精确的高度信息但可以通过这些方法获取使用ele标签如果有调用高程API如OpenElevation使用SRTM或ASTER数字高程模型我在项目中是这样实现的func get_elevation(lat, lon): var api_url https://api.open-elevation.com/api/v1/lookup var query ?locations%f,%f % [lat, lon] var response await HTTPRequest.new().request(api_url query) return parse_json(response.body).results[0].elevation3. 建筑轮廓的3D建模有了建筑轮廓的坐标数据接下来就是用SurfaceTool创建3D模型。这个过程就像用代码捏橡皮泥需要一步步构建网格。先看一个基础示例func create_building(vertices, height): var st SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) # 创建底面 for vertex in vertices: st.add_vertex(vertex) # 创建侧面 for i in vertices.size(): var j (i 1) % vertices.size() st.add_vertex(vertices[i]) st.add_vertex(vertices[j]) st.add_vertex(vertices[i] Vector3(0, height, 0)) st.add_vertex(vertices[j] Vector3(0, height, 0)) # 创建顶面 for vertex in vertices: st.add_vertex(vertex Vector3(0, height, 0)) return st.commit()实际项目中会遇到更复杂的情况比如带洞的建筑。我的解决方案是先对多边形进行三角剖分。Godot 4.2引入了Geometry2D.triangulate方法可以简化这个过程var triangles Geometry2D.triangulate_polygon(polygon_with_holes) for i in range(0, triangles.size(), 3): st.add_vertex(vertices[triangles[i]]) st.add_vertex(vertices[triangles[i1]]) st.add_vertex(vertices[triangles[i2]])4. 性能优化技巧当城市规模变大时性能问题就会凸显。经过多次测试我总结了这些优化方案LOD(细节层次)系统根据相机距离切换不同精度的模型func _process(delta): var distance camera.global_transform.origin.distance_to(building.global_transform.origin) if distance 500: building.mesh low_poly_mesh else: building.mesh high_poly_mesh合并批次将相邻建筑合并为单个网格var multi_mesh MultiMesh.new() multi_mesh.transform_format MultiMesh.TRANSFORM_3D multi_mesh.mesh building_mesh multi_mesh.instance_count buildings.size() for i in buildings.size(): multi_mesh.set_instance_transform(i, buildings[i].transform)异步加载使用后台线程处理数据var thread Thread.new() thread.start(_load_osm_data.bind(map.osm)) func _load_osm_data(path): var data parse_osm_file(path) call_deferred(_on_data_loaded, data)空间分区使用Octree或BVH加速空间查询var space PhysicsServer3D.space_create() PhysicsServer3D.space_set_active(space, true) var shape PhysicsServer3D.shape_create(PhysicsServer3D.SHAPE_BOX) PhysicsServer3D.shape_set_data(shape, Vector3(10, 20, 10))5. 真实案例生成城市街区让我们通过一个完整示例把前面讲的技术串联起来。假设我们要生成一个包含20栋建筑的街区。首先准备OSM数据way id101 nd ref1 lat39.907 lon116.391/ nd ref2 lat39.907 lon116.392/ nd ref3 lat39.908 lon116.392/ nd ref4 lat39.908 lon116.391/ nd ref1 lat39.907 lon116.391/ tag kbuilding vyes/ tag kheight v15/ /way !-- 更多建筑数据... --然后编写解析代码func generate_city_block(osm_path): var osm_data load_osm_data(osm_path) var buildings [] for way in osm_data.ways: if way.tags.get(building) yes: var vertices [] for node in way.nodes: var pos osm_to_godot(node.lat, node.lon, map_center) vertices.append(pos) var height float(way.tags.get(height, 10)) var mesh create_building(vertices, height) var instance MeshInstance3D.new() instance.mesh mesh buildings.append(instance) return buildings最后添加材质和纹理var material StandardMaterial3D.new() material.albedo_texture load(res://textures/brick.png) material.roughness 0.8 material.metallic 0.2 building.material_override material6. 常见问题解决方案在实际开发中我遇到过各种奇怪的问题。这里分享几个典型案例问题1建筑漂浮在空中这是因为高度数据不准确。我的解决方案是func snap_to_ground(building): var space_state get_world_3d().direct_space_state var query PhysicsRayQueryParameters3D.new() query.from building.global_transform.origin Vector3(0, 100, 0) query.to building.global_transform.origin Vector3(0, -100, 0) var result space_state.intersect_ray(query) if result: building.global_transform.origin.y result.position.y问题2建筑重叠使用简单的碰撞检测func check_collision(buildings): for i in buildings.size(): for j in range(i1, buildings.size()): if buildings[i].global_transform.origin.distance_to(buildings[j].global_transform.origin) 5: print(建筑 %d 和 %d 可能重叠 % [i, j])问题3性能下降使用可见性通知器var notifier VisibilityNotifier3D.new() notifier.aabb AABB(Vector3(-10,0,-10), Vector3(20,20,20)) notifier.connect(camera_entered, _on_visible) notifier.connect(camera_exited, _on_hidden) building.add_child(notifier)7. 进阶技巧添加细节要让城市更真实还需要添加这些细节屋顶类型根据roof:shape标签创建不同屋顶match roof_shape: flat: create_flat_roof() gabled: create_gabled_roof() hipped: create_hipped_roof()窗户和门使用着色器实现// 在着色器中添加窗户效果 void fragment() { vec2 uv fract(UV * 10.0); if (uv.x 0.9 || uv.y 0.9) { ALBEDO vec3(0.1); // 窗框颜色 } else { ALBEDO texture(albedo_texture, UV).rgb; } }环境光遮蔽使用BakedLightmapvar lightmap BakedLightmap.new() lightmap.bake_mode BakedLightmap.BAKE_MODE_CONE_TRACE add_child(lightmap) lightmap.bake()动态加载实现分块加载系统func _on_camera_moved(): var current_chunk get_chunk_index(camera.global_transform.origin) if current_chunk ! last_chunk: load_chunk(current_chunk) unload_chunk(last_chunk) last_chunk current_chunk