Cesium 建筑单体化、3DTiles 分类、楼层显隐、分户高亮(BIM + 倾斜孪生高频)

一、方案原理说明

先理清几个容易混淆的概念:

  1. 3DTiles BatchTable(批量表) :倾斜单体化工具(SuperMap、ContextCapture、FME、CesiumLab)处理后,每个瓦片三角面绑定属性:楼栋 ID、楼层号、分户 ID;这是前端筛选的基础,没有 BatchTable 就无法前端单体筛选。
  2. Cesium Classification(分类):原生分类,适合矢量面压盖倾斜模型做高亮;优点:不需要改模型、适合没有 BatchTable 的倾斜;缺点:只能面压盖,不方便按楼层、分户属性过滤。
  3. CustomShader(着色器筛选) :本篇主推方案,读取 batchId 拿到 BatchTable 属性,在片元着色器判断属性,实现:隐藏指定楼层、分户变色高亮、隔离显示某一层。适合 BIM、已经单体化带属性的 3DTiles。
  4. 楼层显隐 :着色器内判断 floorId,不符合条件直接 discard;
  5. 分户高亮:匹配 roomId,修改片元自发光 emissive 或者叠加颜色。

两种业务选型建议

  • 老倾斜、无属性 BatchTable:用Polygon 面 + Classification压盖单体高亮;
  • BIM 导出 3DTiles、CesiumLab 单体化倾斜:优先 CustomShader + BatchTable 属性过滤,支持楼层、分户、构件级筛选。

二、完整代码 CesiumBuildingSeparate.vue

复制代码
<template>
  <div class="cesium-page">
    <div ref="cesiumRef" id="cesiumContainer"></div>
    <div class="control-panel">
      <div>建筑单体控制</div>
      <button @click="highlightBuilding">高亮整栋楼(building_01)</button>
      <button @click="showOnlyFloor">只显示3层(floor_03)</button>
      <button @click="highlightRoom">高亮分户A01</button>
      <button @click="resetStyle">恢复原始样式</button>
      <button @click="toggleClassification">面压盖单体高亮(无属性倾斜方案)</button>
    </div>
  </div>
</template>

<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'

const cesiumRef = ref(null)
let viewer = null
let tileset = null
let classificationPrimitive = null
let useClassification = ref(false)

// ========= 筛选条件,和BatchTable里面字段对应 =========
const TARGET_BUILDING_ID = "building_01"
const TARGET_FLOOR_ID = "floor_03"
const TARGET_ROOM_ID = "room_A01"

window.CESIUM_BASE_URL = '/cesium/'

onMounted(() => {
  initCesium()
})

onBeforeUnmount(() => {
  if(tileset) viewer.scene.primitives.remove(tileset)
  clearClassification()
  if (viewer) viewer.destroy()
})

function initCesium() {
  viewer = new Cesium.Viewer('cesiumContainer', {
    animation: false,
    timeline: false,
    baseLayerPicker: false,
    geocoder: false,
    homeButton: false,
    sceneModePicker: false,
    navigationHelpButton: false,
    fullscreenButton: false,
    infoBox: false
  })
  viewer.creditDisplay.container.style.display = 'none'
  viewer.scene.globe.depthTestAgainstTerrain = true

  loadTileset()
}

/**
 * 加载带BatchTable属性的3DTiles
 * 替换url为你自己的tileset.json
 */
function loadTileset() {
  tileset = new Cesium.Cesium3DTileset({
    url: "/tiles/tileset.json",
    skipLevelOfDetail: true,
    baseScreenSpaceError: 2
  })
  viewer.scene.primitives.add(tileset)

  tileset.readyPromise.then(() => {
    viewer.zoomTo(tileset)
    console.log("tileset加载完成,BatchTable属性可读取")
  }).catch(err => {
    console.error("3DTiles加载失败", err)
  })
}

/**
 * 1、CustomShader:高亮整栋建筑
 */
function highlightBuilding() {
  if(!tileset) return
  const shader = new Cesium.CustomShader({
    attributeLocations: {
      batchId: Cesium.BatchTablePipelineStage.BATCH_ID_ATTRIBUTE_LOCATION
    },
    uniforms: {},
    fragmentShaderText: `
      void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
      {
        int bid = fsInput.attributes.batchId;
        // 读取batchTable里面buildingId字符串
        string buildingId = getBatchTableString(bid, "buildingId");
        if(buildingId == "${TARGET_BUILDING_ID}"){
          // 叠加自发光高亮
          material.emissive = vec3(0.9,0.4,0.15);
        }
      }
    `
  })
  tileset.customShader = shader
}

/**
 * 2、只显示指定楼层,其他全部discard隐藏(楼层显隐)
 */
function showOnlyFloor() {
  if(!tileset) return
  const shader = new Cesium.CustomShader({
    attributeLocations: {
      batchId: Cesium.BatchTablePipelineStage.BATCH_ID_ATTRIBUTE_LOCATION
    },
    fragmentShaderText: `
      void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
      {
        int bid = fsInput.attributes.batchId;
        string floorId = getBatchTableString(bid, "floorId");
        if(floorId != "${TARGET_FLOOR_ID}"){
          discard;
        }
        material.emissive = vec3(0.2,0.6,0.9);
      }
    `
  })
  tileset.customShader = shader
}

/**
 * 3、分户高亮:只高亮某一户,其余保持原色
 */
function highlightRoom() {
  if(!tileset) return
  const shader = new Cesium.CustomShader({
    attributeLocations: {
      batchId: Cesium.BatchTablePipelineStage.BATCH_ID_ATTRIBUTE_LOCATION
    },
    fragmentShaderText: `
      void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
      {
        int bid = fsInput.attributes.batchId;
        string roomId = getBatchTableString(bid, "roomId");
        if(roomId == "${TARGET_ROOM_ID}"){
          material.emissive = vec3(0.8,0.15,0.25);
        }
      }
    `
  })
  tileset.customShader = shader
}

/**
 * 恢复模型原始纹理样式
 */
function resetStyle() {
  if(tileset){
    tileset.customShader = undefined
  }
  clearClassification()
  useClassification.value = false
}

// ===================== 无属性倾斜方案:面压盖 Classification =====================
function addClassificationPolygon() {
  clearClassification()
  // 单体轮廓面,替换成你的建筑轮廓GeoJSON坐标
  const polygonHierarchy = new Cesium.PolygonHierarchy(
    Cesium.Cartesian3.fromDegreesArray([
      108.9470,34.2640,
      108.9485,34.2640,
      108.9485,34.2652,
      108.9470,34.2652
    ])
  )
  const polygon = new Cesium.ClassificationPrimitive({
    polygonHierarchy: polygonHierarchy,
    classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
    appearance: new Cesium.ColorAppearance({
      color: Cesium.Color.RED.withAlpha(0.45)
    })
  })
  classificationPrimitive = viewer.scene.primitives.add(polygon)
}
function clearClassification(){
  if(classificationPrimitive){
    viewer.scene.primitives.remove(classificationPrimitive)
    classificationPrimitive = null
  }
}
function toggleClassification(){
  if(useClassification.value){
    clearClassification()
    useClassification.value = false
  }else{
    addClassificationPolygon()
    useClassification.value = true
  }
}
</script>

<style scoped>
.cesium-page {
  width: 100%;
  height: 100vh;
  position: relative;
}
#cesiumContainer {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
.control-panel {
  position: absolute;
  top:12px;
  left:12px;
  z-index:999;
  background: rgba(0, 20, 40, 0.75);
  padding:10px;
  border-radius:6px;
  color:#fff;
}
.control-panel button {
  margin:4px;
  padding:4px 8px;
  cursor:pointer;
}
</style>

三、核心知识点详解

1. BatchTable 属性读取

GLSL 着色器内置函数:

复制代码
// 字符串类型
getBatchTableString(batchId, "keyName")
// 数字类型
getBatchTableFloat(batchId, "keyName")

重点:属性名称、类型必须和预处理导出的 BatchTable 严格一致;很多人单体化失败就是字段名大小写不匹配。

2. 两种单体化路线对比

表格

方案 依赖 能力 性能
CustomShader + BatchTable 模型预处理带属性 BatchTable 楼栋 / 楼层 / 分户 / 构件筛选、隐藏、变色、高亮 优秀,GPU 筛选
Classification 面压盖 不需要模型属性,只要轮廓面 只能面覆盖区域高亮,无法按楼层分户过滤 良好

3. 楼层显隐实现关键点

discard 直接丢弃不在目标楼层的片元;

扩展业务:做多层勾选(多选楼层),shader 里面用 || 多条件判断。

复制代码
if(floorId != "floor_02" && floorId != "floor_03") discard;

4. 分户高亮、分户弹窗联动

配合拾取:viewer.scene.pick 拿到 batchId,读取 BatchTable 里面的名称、面积、户主、房间用途,弹出信息面板,孪生项目标配。

拾取示例片段:

复制代码
const pick = viewer.scene.pick(windowPosition)
if(Cesium.defined(pick) && Cesium.defined(pick.batchTable)){
  const props = pick.batchTable.getProperties(pick.batchId)
  console.log(props.buildingId, props.roomName)
}

5. BIM 转 3DTiles 注意事项

Revit 导出 3DTiles 常用工具:FME、CesiumLab、ifc2tiles;导出时一定要勾选保留构件属性、楼层、房间参数,否则 BatchTable 为空,前端无法筛选。

四、高频踩坑清单

  1. ❌ getBatchTableString 报错,着色器不生效
    ✅ 低版本 Cesium 不支持这个内置函数,建议 ≥1.100;老版本只能用数字类型 getBatchTableFloat;
  2. ❌ 筛选完全没反应,所有模型都不高亮
    ✅ 字段名大小写、字符串值和 BatchTable 不一致;或者你的模型根本没有做单体化,BatchTable 只有一条;
  3. ❌ Classification 面看不到颜色
    ✅ classificationType 必须是 CESIUM_3D_TILE,不是 TERRAIN;多边形高度要高于地形低于建筑;
  4. ❌ discard 之后模型出现破洞、闪烁
    ✅ 瓦片 LOD 切换,不同批次 batchId 不一致;预处理的时候保证同一构件 batchId 统一;
  5. ❌ BIM 构件太多,着色器卡顿
    ✅ 不要在 shader 写超长 if 判断;复杂筛选提前在前端整理 ID 列表,或者后端分瓦片。

五、扩展业务方案

  1. 楼层下拉多选:前端维护选中楼层数组,动态拼接 fragmentShader 文本,重新生成 CustomShader;
  2. 分户隔离 + 剖切:结合前面的 Shader 裁切,选中一户之后把周边墙体裁掉看内部;
  3. BIM 构件状态:给构件增加状态属性(正常 / 告警 / 检修),着色器根据状态自动变色;
  4. 分层透明:不 discard,而是降低 alpha 透明度,保留其他楼层半透明,看剖面效果。
相关推荐
IT_陈寒18 分钟前
JavaScript项目实战经验分享:从内存泄漏到性能优化的血泪史
前端·人工智能·后端
恋猫de小郭2 小时前
Flutter 3.50 & Dart 3.14 发布,快来看看有什么更新吧
android·前端·flutter
非优秀程序员9 小时前
第10章 千团出世|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第11章 小高在对面|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第14章 只剩美团|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第09章 老乡何必为难老乡|《互联网产品恩仇录》
前端
非优秀程序员10 小时前
第03章 三大门户,烽烟起于瀛海威|《互联网产品恩仇录》
前端
曦樂~14 小时前
【Web Development】02 CSS
前端·css
风骏时光牛马14 小时前
后端服务接口开发与数据逻辑处理
前端