原因很简单——模型太精细了

发布时间:2025-06-24 20:20:13  作者:北方职教升学中心  阅读量:879


  • zp_community_id:社区 ID,外键,关联到 zp_community表。
  • zp_room_name:房屋名称,例如“101”。

    为了解决这个问题,我们对模型进行了抽稀处理,但结果还是不理想,依然卡顿得让人抓狂。

  • 关系:与 zp_room表建立多对一关系,一个房屋可以包含多个窗户。
  • zp_build_name:楼栋名称,例如“A5_10”。如果你也在进行类似的3维GIS项目,或者遇到类似的技术挑战,欢迎与我分享你的经验与困惑。正常)等描述住户的其他信息。
  • 字段
    • zp_room_id:房屋 ID,主键,用于唯一标识每间房屋。
    • 字段
      • zp_community_id:社区 ID,主键,用于唯一标识每个社区。

        添加 3D 模型到 Cesium 场景函数:

        addModel(models, entities) {      if (models && models.length > 0) {        for (let i = 0; i < models.length; i++) {          let heading = Cesium.Math.toRadians(65);          var pitch = 0;          var roll = 0;          var hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);          var orientation = Cesium.Transforms.headingPitchRollQuaternion(            models[i].position,            hpr          );          this.locatEntity = entities.add({            name: models[i].name,            id: models[i].id,            position: models[i].position,            orientation: orientation,            model: {              color: null,              silhouetteColor: null,              silhouetteAlpha: 0,              silhouetteSize: 0,              show: true,              uri: models[i].url,              scale: 1.0, // 缩放比例              minimumPixelSize: 1, // 最小像素大小              maximumScale: 1, // 模型的最大比例尺大小。我实在不想绑这些东西,只好辗转用了些“特殊手段”搞了一个 Mapbox账号。
      • 字段
        • zp_build_id:楼栋 ID,主键,用于唯一标识每个楼栋。

          好在,经过一番折腾,我发现了解决之道:nvm!(https://github.com/nvm-sh/nvm)真是个好东西,堪比穿越时空的工具箱,直接把 Node.js环境切换到项目需要的版本,问题就迎刃而解了。存储楼栋信息的zp_build、然而,第一版模型导入 Cesium后,直接把界面“卡成了PPT”。而这个3维GIS系统用的是 Vue3,导致前端依赖始终拉不下来,急得我头都大了。

      4. zp_window窗户表

      • 功能:存储窗户的单体化信息。状态(如危险、
    • 关系
      • zp_build表建立多对一关系。

        以下是每张表的具体设计与作用:

        1. zp_community社区表

        • 功能:存储社区的基础信息。整个流程是:配置数据 -> 模型对象 -> 场景渲染的转换过程。

        通过主外键关联,整个数据表的结构从社区到楼栋再到房屋、虽然过程有点折腾,但也算是用汗水换来了经验。随着建筑数量的增加,前端的加载压力成倍增长,直接把界面“按在地上摩擦”。

      • zp_window表通过 zp_room_id建立一对多关系。

      3. zp_room房屋表

      • 功能:存储房屋的具体信息。
      • 字段
        • zp_window_id:窗户 ID,主键,用于唯一标识每个窗户。其流程为:

          • 遍历 buildingName 数组,获取每个建筑物的配置信息
          • 调用 initHouse() 函数,根据建筑物数据创建模型对象,包含:
          • 模型路径(gltf文件)
          • 位置信息(经纬度坐标)
          • 模型数组引用
          • 建筑物ID
          • 将创建的模型对象存入 models 数组
          • 最后调用 addModel() 函数,将所有模型一次性添加到 Cesium 场景的 entities 中进行渲染

          这是场景初始化时的重要函数,负责将所有建筑物的 3D 模型加载并显示到地图上。窗户、

        • zp_household_name:住户姓名。“租客”、其流程为:

          • 开启加载状态,设置 2 秒的加载动画
          • 清空场景中现有的实体(viewer.entities.removeAll())和模型数组(models = [])
          • 通过 axios 请求 /zz/redisWindow/windowUrls/{buildingId} 获取该建筑的窗户数据
          • 请求成功后:
          • 先加载建筑主体模型(initHouse())
          • 遍历窗户数据数组,逐个加载窗户模型(initClickWindow())
          • 将所有模型添加到场景(addModel())
          • 添加建筑标签和按钮标签
          • 最终实现建筑物从整体模型向可交互的分户模型的转换

          这是实现建筑物点击交互的核心函数,将整体建筑拆分为可独立操作的窗户单元,为后续的窗户选择和信息绑定提供基础。客户要求每个窗户都必须单体化,也就是说每扇窗户都是一个独立的要素(这就需要把建筑与窗户进行分离,如下图)。原因很简单——模型太精细了。我们为此构建了一套基于 Cesium、本文系统初衷是设计一套数据管理系统,以实现对社区特殊人群的精细化管理。

          后端代码:

          在后端我把窗户模型丢到了redis中进行存储,对应的接口为:

          public Map<String,Set> getWindowUrls(String buildBH) {        Map<String,Set>windowUrls=new HashMap<>();        Set<Object> windowBH=redisUtils.sGet(buildBH);        windowUrls.put(buildBH,windowBH);        return windowUrls;    }

          redis中存的是建筑下对用的窗户编号以及窗户编号对应的gltf模型信息:

           

          A5_10为建筑编号, A5_10_w_1为窗户编号

          4.2.3. 住户信息绑定
          4.2.3.1. 绑定初始化

          前端代码:

          bindWindow(viewer, entities, bindWindows) {    // 移除其他事件处理器    this.removeAllHandler();        // 创建绑定事件处理器    this.handlerBind = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);        // 处理 Ctrl + 点击事件    this.handlerBind.setInputAction(        function (movement) {            if (this.isInit == 2) {  // 绑定模式                var pick = viewer.scene.pick(movement.position);                if (pick && pick.id && pick.id._id.indexOf("_w_") != -1) {                    // 处理窗户选择                    windowsList.push(pick.id._id);                    // 高亮显示                    this.lightwindow(entities, windowsList, 0);                }            }        },        Cesium.ScreenSpaceEventType.LEFT_CLICK,        Cesium.KeyboardEventModifier.CTRL    );}

          项目使用 isInit 变量管理不同的交互状态:

          • isInit = 0: 初始化状态,可点击建筑
          • isInit = 1: 建筑分解状态,可查看窗户信息
          • isInit = 2: 绑定模式,可进行窗户绑定

          4.2.3.2. 绑定窗户

          前端代码:

          // 获取窗户信息this.$axios.getHouseholdInfo({ windowId: windowId })    .then(res => {        if (res.code == 200) {            // 处理返回数据        }    });// 提交绑定axios.post("/room/bindWindow", {    roomId: val.name,    windowId: [...this.windowsData]})

          这段代码实现了窗户信息的查询和绑定功能。这种按需加载的策略有效缓解了性能问题,同时也让客户的需求得到了妥善实现。去年我写了一个工作流系统,用的 Node.js是 10 的版本,Vue也还是Vue2,老得都快发灰了。行吧,登录 Mapbox官网去获取新的 token。住户,形成了一套完整的层次化管理体系:

          • 社区 -> 楼栋 -> 房屋 -> 窗户:实现从宏观到微观的空间数据管理。存储房屋信息的zp_room

  • 经过深思熟虑,我决定将这个系统开发过程中的技术经验进行总结与分享,希望为有类似需求的读者提供一些参考与启发。

    三年前,我开发了一个基于 Cesium + Mapbox + Spring Boot 的3维GIS系统项目,整体实现效果非常不错。

    整体效果:

    近距离效果:

    查看单栋建筑人员信息:

    点击特殊人员种类后对应的窗户联动: 

    点击窗户查看对应户主详情:

    楼栋信息管理:

    绑定窗户与对应房号:

    4. 核心技术讲解

    4.1. 场景构建

    整个场景是基于真实的 84地理坐标系构建的,房屋模型则基于客户提供的房屋 CAD图制作而成。

  • 其他字段:性别、
  • zp_build_info:楼栋附加信息,例如描述或备注。
  • zp_household_identity:住户身份,例如“户主”、当时,我们的建模小姐姐凭借强大的耐心和技术,按照 CAD 图 1:1 完美复刻了建筑模型。
  •  窗户 ->住户:支持窗户与业务数据(住户)的绑定。尽管如此,这个项目曾经还是成为了我面试中的重要亮点,并帮助我获得了许多工作机会。
  • isbind:房屋绑定状态,标识房屋是否与住户信息绑定。
  • zp_household_phone:联系电话。

    4.2. 表结构及核心代码讲解

    4.2.1. 表结构

    整个系统涉及到的表一共有5张,分别为存储社区信息的zp_community

  • zp_window_name:窗户名称。年龄、
  • 5. zp_household户主表

    • 功能:存储住户信息。
  • 关系:每个社区下包含多个楼栋,与 zp_build表通过 zp_community_id建立一对多关系。“子女”等。Mapbox 和 SpringBoot的数字孪生效果系统,能够通过直观的方式对特殊人群的居住情况进行有效监管。首先是Vue环境的问题:三年前,我的Node.js版本在当时算是“高配”,但后来我渐渐不写前端了,前端技术栈也开始“躺平”。

    3. 实现效果演示

    这个系统的开发初衷是客户要求实现一个具有数字孪生效果的三维GIS系统,可以查看社区下特殊人员的汇总信息和详细信息(通过点击建筑窗户来查看对应户主的详细信息)。

    最终,为了提升性能,我们不得不进行业务逻辑上的优化。

  • 4.2.2. 场景加载
    4.2.2.1. 初始场景加载

    前端代码:

    initScene() {    // 初始化 Cesium Viewer    this.viewer = new Cesium.Viewer("cesiumContainer", {        geocoder: false,        // 位置查找工具        homeButton: false,      // 复位按钮        sceneModePicker: false, // 模式切换        baseLayerPicker: false, // 图层选择        navigationHelpButton: false,        animation: false,       // 速度控制        timeline: false,        // 时间轴        fullscreenButton: false,// 全屏        infoBox: false,        selectionIndicator: false    });        // 添加 Mapbox 底图    var layer = new Cesium.MapboxStyleImageryProvider({/*...*/});    this.viewer.imageryLayers.addImageryProvider(layer);}

    mapbox底图添加代码:

      

    4.2.2.2. 加载所有建筑

    前端代码:

    加载所有房屋:

    addAllHouse() {    // 遍历建筑物数据,初始化每个建筑    for (let i = 0; i < this.buildingName.length; i++) {        this.initHouse(            "/houseUrl" + this.buildingData[this.buildingName[i]].name + ".gltf",            Cesium.Cartesian3.fromDegrees(/*经纬度位置*/),            this.models,            this.buildingName[i]        );    }    // 添加到场景    this.addModel(this.models, this.entities);}

    这个函数的功能是批量加载所有建筑物模型到场景中。存储窗户信息的zp_window,以及存储户主信息的zp_household

  • 关系:与 zp_community表建立多对一关系,同时与 zp_room表通过 zp_build_id建立一对多关系。
  • 字段
    • zp_household_id:住户 ID,主键,用于唯一标识每位住户。
    • zp_unit_name:单元名称,用于描述房屋所属单元。希望通过这篇文章的分享,能够为有类似需求的开发者提供一些实用的启示与帮助。初始场景中,加载的是所有建筑的简易模型(Box加贴图),只有在点击某栋建筑时,才会加载这栋建筑的精细模型和窗户的分离模型。其流程为:

      • 查询窗户信息:
      • 通过 getHouseholdInfo 接口,传入 windowId 参数
      • 获取该窗户关联的住户信息
      • 如果返回 code 为 200,则处理返回的住户数据
      • 绑定窗户:
      • 通过 /room/bindWindow 接口提交绑定信息
      • 传入房间ID (roomId) 和窗户ID数组 (windowId)
      • 将选中的窗户与指定房间建立关联关系

      后端代码:

      public Result bindWindow(BindReq bindReq) {        String roomId=bindReq.getRoomId();        List<String>windowsId=bindReq.getWindowId();        try {            for (int i = 0; i < windowsId.size(); i++) {                roomMapper.updateWindow(roomId,windowsId.get(i));            }        } catch (Exception e) {            e.printStackTrace();            return Result.error("绑定失败");        }        return Result.ok("绑定成功");    }

      5. 结语

      项目历时一个半月,最终呈现了本文中所展示的效果。

    2. zp_build楼栋表

    • 功能:存储楼栋的详细信息。如下图所示,建筑下面的底图是Mapbox底图。

      把项目跑起来后,我发现 Mapbox的 token 竟然过期了。

      2. 一些踩坑和吐槽(不感兴趣的可以直接跳过)

      环境的坑:

      这是三年前的项目了,光是把它跑起来就让我费了不少力气。遗憾的是,由于非技术层面的原因,该项目在即将交付时未能如期上线。

    • zp_build_id:楼栋 ID,外键,关联到 zp_build表。最近,与我一同开发该项目的前端同事联系我,表示希望获取源码用于面试演示(当时整个系统仅由我们两人开发,他负责纯前端界面,我负责全部后端逻辑和 GIS 前端代码的编写)。
  • 关系:与 zp_room表建立多对一关系,一个房屋可对应多个住户。
  • zp_room_id:房屋 ID,外键,关联到 zp_room表。好在最终项目总算跑起来了,虽然和当初的效果略有不同,但对我来说这些都不重要,关键是要把实现系统的思路分享给大家。然而,让我懵圈的是,现在居然需要绑定个人信息才能获取(三年前完全没有这种操作啊!)。
  • zp_household表通过 zp_room_id建立一对多关系。

    Mapbox的坑:

    还有就是Mapbox底图的问题。

  • zp_community_name:社区名称。

    不过,这也没能让我逃过下一个坑——之前的系统底图没了!没办法,只好换了一个现成的底图将就用。

  • zp_room_id:房屋 ID,外键,关联到 zp_room表。

  • 目录

    1. 前言

    2. 一些踩坑和吐槽(不感兴趣的可以直接跳过)

    3. 实现效果演示

    4. 核心技术讲解

    4.1. 场景构建

    4.2. 表结构及核心代码讲解

    4.2.1. 表结构

    4.2.2. 场景加载

    4.2.2.1. 初始场景加载

    4.2.2.2. 加载所有建筑

    4.2.2.3. 建筑与窗户分离场景加载(单体化)

    4.2.3. 住户信息绑定

    4.2.3.1. 绑定初始化

    4.2.3.2. 绑定窗户

    5. 结语


    1. 前言

    在数字化转型和智慧城市建设的浪潮中,数字孪生技术逐渐成为提升数据可视化与精准监管能力的重要工具,在各行业的应用前景日益广泛。 minimumPixelSize的上限 incrementallyLoadTextures: true, // 加载模型后纹理是否可以继续流入 runAnimations: true, // 是否应启动模型中指定的glTF动画 clampAnimations: true, // 指定glTF动画是否应在没有关键帧的持续时间内保持最后一个姿势 // 指定模型是否投射或接收来自光源的阴影 type:ShadowMode // DISABLED 对象不投射或接收阴影;ENABLED 对象投射并接收阴影;CAST_ONLY 对象仅投射阴影;RECEIVE_ONLY 对象仅接收阴影 shadows: Cesium.ShadowMode.ENABLED, heightReference: Cesium.HeightReference.NONE, }, }); } } }

    4.2.2.3. 建筑与窗户分离场景加载(单体化)

    前端代码:

    loadClickHouse(buildingId) {    // 清除现有实体    this.viewer.entities.removeAll();        // 加载分离的窗户模型    axios.get("/redisWindow/windowUrls/" + buildingId)        .then((res) => {            // 加载建筑主体            this.initHouse(/*...*/);                        // 加载每个窗户            const urlArr = res.data[buildingId];            for (let i = 0; i < urlArr.length; i++) {                this.initClickWindow(/*...*/);            }        });}

    这个函数实现了点击建筑物后的分户展示功能。