导出osm数据
访问网站:https://www.openstreetmap.org/export
选择你要导出的范围,点击导出
将下载的map.osm文件 存放在 assets 文件夹下
创建环境场景
创建场景
创建 res://scenes/environment.tscn
文件,scenes 文件夹右键创建3d场景。
创建脚本
打开 environment
场景在 Environment
节点上右键 添加脚本 res://scripts/environment.gd
后续简单的基础操作不在详细描述。
添加配置参数
在 res://scripts/config.gd
中添加以下关于地图的配置。
# 地图边界和缩放因子
static var min_lon = 117.9520
static var max_lon = 118.1572 # 添加最大经度
static var min_lat = 36.7493 # 添加最小纬度
static var max_lat = 36.8557
static var scale_factor = 111.699*1000
static var map_center = Vector2.ZERO # 地图中心点(屏幕坐标)
加载osm
在 res://scripts/environment.gd
脚本中添加 osm 解析逻辑,解析出来的信息保存在 nodes 和 ways 中。
通过读取 XML 格式的地图文件,提取其中的节点(nodes)和路径(ways)信息,存储到全局变量中,并计算地图边界和中心点,最终输出加载的节点和路径数量统计。
# 存储解析后的数据
var nodes = {}
var ways = []
func load_osm_data(file_path: String):
# 使用 FileAccess 代替 File
if not FileAccess.file_exists(file_path):
push_error("File not found: " + file_path)
return
var file = FileAccess.open(file_path, FileAccess.READ)
if file == null:
push_error("Failed to open file: " + file_path)
return
var parser = XMLParser.new()
if parser.open_buffer(file.get_buffer(file.get_length())) != OK:
push_error("XML parse error")
file.close()
return
while parser.read() == OK:
match parser.get_node_type():
XMLParser.NODE_ELEMENT:
var node_name = parser.get_node_name()
var attrs = _get_attributes(parser)
if node_name == "node":
# 解析节点
nodes[attrs.id] = {
"lat": float(attrs.lat),
"lon": float(attrs.lon)
}
elif node_name == "bounds":
# 开始新路径
var minlat = float(attrs.minlat)
var minlon = float(attrs.minlon)
var maxlat = float(attrs.maxlat)
var maxlon = float(attrs.maxlon)
config.min_lat = minlat
config.min_lon = minlon
config.max_lat = maxlat
config.max_lon = maxlon
config.map_center = Vector3((minlon + maxlon) / 2, 0, (minlat + maxlat) / 2)
print("map_center:",config.map_center)
elif node_name == "way":
# 开始新路径
ways.append({
"id": int(attrs.id),
"node_ids": [],
"tags": {}
})
elif node_name == "nd" && !ways.is_empty():
# 添加节点到当前路径
ways[-1].node_ids.append(int(attrs.ref))
elif node_name == "tag" && !ways.is_empty():
# 添加属性标签
ways[-1].tags[attrs.k] = attrs.v
file = null # 在 Godot 4 中不需要显式关闭,但可以设为 null
print("OSM Data Loaded: %d nodes, %d ways" % [nodes.size(), ways.size()])
# 辅助函数:获取元素所有属性
func _get_attributes(parser: XMLParser) -> Dictionary:
var attrs = {}
for i in range(parser.get_attribute_count()):
attrs[parser.get_attribute_name(i)] = parser.get_attribute_value(i)
return attrs
坐标转换
将经纬度坐标(lat, lon)转换为 Godot 引擎中的 3D 坐标(Vector3)。其核心逻辑是通过预加载的配置文件(config.gd)获取地图中心点(map_center)、缩放因子(scale_factor)和地面高度(ground_height),计算经纬度相对于地图中心的偏移量,并乘以缩放因子后生成最终的 3D 坐标(X 轴对应经度,Z 轴对应纬度,Y 轴固定为地面高度)。适用于将地理坐标映射到游戏世界中的水平地面位置。
# 经纬度转3D坐标 - 修复后的版本
static func latlon_to_vector3(lat: float, lon: float) -> Vector3:
var config = preload("res://scripts/config.gd")
# 地图边界和缩放因子
var scale_factor = config.scale_factor
var map_center = config.map_center
var ground_height = config.ground_height
# 计算相对于地图中心的偏移
var x = (lon - map_center.x) * scale_factor
var z = (lat - map_center.z) * scale_factor
# 返回3D坐标 - 使用与地面相同的高度基准
return Vector3(
x,
ground_height, # 使用地面高度
z
)
创建地面
创建一个棱柱体背景。它首先通过调用 latlon_to_vector3()函数将地图边界(最大/最小经纬度)转换为 3D 坐标点,生成一个四边形底面,然后调用 create_prism_from_base_and_height()函数基于这些点创建棱柱体网格实例,并设置其高度和颜色。最后将生成的棱柱体添加到当前节点中。
func create_background():
# 调用函数创建棱柱体
var base_points = [
functions.latlon_to_vector3(config.max_lat,config.max_lon),
functions.latlon_to_vector3(config.max_lat,config.min_lon),
functions.latlon_to_vector3(config.min_lat,config.min_lon),
functions.latlon_to_vector3(config.min_lat,config.max_lon),
#Vector3(100, 0, 100), # 点1
#Vector3(-100, 0, 100), # 点2
#Vector3(-100, 0, -100), # 点3
#Vector3(100, 0, -100), # 点4
]
var prism_mesh_instance = functions.create_prism_from_base_and_height(base_points, config.ground_height, config.ground_color)
print(base_points)
add_child(prism_mesh_instance)
创建建筑物
根据 OSM 数据生成 3D 建筑物模型。它遍历所有路径(ways),筛选出带有 building标签且节点数大于 2 的闭合路径(多边形),然后根据 height或 building:levels标签确定建筑物高度(默认 5 米,每层按 3 米计算)。通过将路径中的经纬度节点转换为 3D 坐标点集(并轻微抬高 0.1 米避免地面贴合),调用 create_prism_from_base_and_height()生成棱柱体网格实例,最终将建筑物添加到场景中,统一使用米色(RGB 0.8, 0.7, 0.6)材质。
func create_buildings():
for way in ways:
# 检查是否为建筑物
if way.tags.get("building") and way.node_ids.size() > 2:
# 确保路径是闭合的
if way.node_ids[0] != way.node_ids[-1]:
continue
# 获取建筑物高度
var height = 5.0
if way.tags.get("height"):
height = float(way.tags.height)
elif way.tags.get("building:levels"):
height = float(way.tags["building:levels"]) * 3.0
# 收集建筑物顶点
var base_points = []
for i in range(way.node_ids.size() - 1):
var node_id = way.node_ids[i]
var node = nodes.get(str(node_id))
if node:
var point = functions.latlon_to_vector3(node.lat, node.lon)
point.y += 0.1 # 增加抬高量
base_points.append(point)
if base_points.size() < 3:
continue
var building = functions.create_prism_from_base_and_height(base_points,height, Color(0.8, 0.7, 0.6) )
add_child(building)
完善场景初始化方法
func _ready() -> void:
# Called when the node enters the scene tree for the first time.
print("loading osm")
load_osm_data("res://assets/map.osm")
create_background()
create_buildings()
在主场景中加载环境场景
func _ready() -> void:
# Called when the node enters the scene tree for the first time.
create_drone()
create_environment()
func create_environment():
var environment_scene = preload("res://scenes/environment.tscn")
var environment = environment_scene.instantiate()
add_child(environment)
environment.position = Vector3(0,0,0)
效果预览
创建道路
-
道路筛选:遍历所有路径(
ways
),筛选出带有highway
标签的路径作为道路数据。 -
道路宽度设置:
- 根据道路类型设置不同宽度:高速公路(motorway)30米,主干道(primary)25米,其他道路15米。
-
顶点生成:
- 将路径中的经纬度节点转换为3D坐标点集(轻微抬高0.1米避免地面贴合)
- 为每个路径点计算方向向量和垂直向量,生成道路两侧的顶点(形成道路宽度)
-
网格构建:
- 使用 SurfaceTool 创建三角形网格
- 每两个连续路径点生成两个三角形(形成四边形道路段)
- 自动生成法线并提交网格数据
-
材质与渲染:
- 创建灰色(RGB 0.5,0.5,0.5)标准材质
- 禁用背面剔除(CULL_DISABLED)确保双面可见
- 创建 MeshInstance3D 节点并添加到场景
该函数能够自动处理各种类型的道路数据,根据道路等级调整宽度,并生成具有正确几何形状和渲染效果的3D道路网格。
func create_roads():
for way in ways:
if way.tags.get("highway"):
# 创建道路网格
var road_mesh = ArrayMesh.new()
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
# 道路宽度
var road_width = 10.0
if way.tags.get("highway") == "motorway":
road_width = 30.0
elif way.tags.get("highway") == "primary":
road_width = 25
else:
road_width = 15
# 创建道路顶点
var points = []
for node_id in way.node_ids:
var node = nodes.get(str(node_id))
if node:
var point = functions.latlon_to_vector3(node.lat, node.lon)
point.y += 0.1 # 增加抬高量
points.append(point)
# 确保有足够的点来创建道路
if points.size() < 2:
continue
# 创建顶点数组用于道路
var vertices = []
# 为每个点创建两个顶点(形成道路宽度)
for i in range(points.size()):
var current = points[i]
var prev = points[i - 1] if i > 0 else current
var next = points[i + 1] if i < points.size() - 1 else current
# 计算方向向量
var in_vec = (current - prev).normalized()
var out_vec = (next - current).normalized()
# 计算平均方向
var direction = (in_vec + out_vec).normalized()
# 计算垂直向量
var perpendicular = Vector3(direction.z, 0, -direction.x).normalized()
# 创建道路两侧的点
var left = current + perpendicular * road_width / 2
var right = current - perpendicular * road_width / 2
vertices.append(left)
vertices.append(right)
# 创建三角形
for i in range(0, vertices.size() - 2, 2):
# 第一三角形 (左1, 右1, 左2)
st.add_vertex(vertices[i]) # 左1
st.add_vertex(vertices[i + 1]) # 右1
st.add_vertex(vertices[i + 2]) # 左2
# 第二三角形 (右1, 右2, 左2)
st.add_vertex(vertices[i + 1]) # 右1
st.add_vertex(vertices[i + 3]) # 右2
st.add_vertex(vertices[i + 2]) # 左2
# 生成法线
st.generate_normals()
# 生成网格
st.commit(road_mesh)
var material = StandardMaterial3D.new()
material.albedo_color = Color(0.5, 0.5, 0.5)
material.cull_mode = BaseMaterial3D.CULL_DISABLED # 禁用背面剔除
# 创建道路节点
var road = MeshInstance3D.new()
road.mesh = road_mesh
road.material_override = material
add_child(road)