Godot 城市模拟 – 007 加载osm 创建筑物和道路
本文最后更新于14 天前

导出osm数据

访问网站:https://www.openstreetmap.org/export
选择你要导出的范围,点击导出

file

将下载的map.osm文件 存放在 assets 文件夹下

file

创建环境场景

创建场景

创建 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)

效果预览

file

创建道路

  1. 道路筛选:遍历所有路径(ways),筛选出带有 highway 标签的路径作为道路数据。

  2. 道路宽度设置

    • 根据道路类型设置不同宽度:高速公路(motorway)30米,主干道(primary)25米,其他道路15米。
  3. 顶点生成

    • 将路径中的经纬度节点转换为3D坐标点集(轻微抬高0.1米避免地面贴合)
    • 为每个路径点计算方向向量和垂直向量,生成道路两侧的顶点(形成道路宽度)
  4. 网格构建

    • 使用 SurfaceTool 创建三角形网格
    • 每两个连续路径点生成两个三角形(形成四边形道路段)
    • 自动生成法线并提交网格数据
  5. 材质与渲染

    • 创建灰色(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)

效果预览

file



扫码关注,及时关注技术动态


暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇