一、方案原理说明
先理清几个容易混淆的概念:
- 3DTiles BatchTable(批量表) :倾斜单体化工具(SuperMap、ContextCapture、FME、CesiumLab)处理后,每个瓦片三角面绑定属性:楼栋 ID、楼层号、分户 ID;这是前端筛选的基础,没有 BatchTable 就无法前端单体筛选。
- Cesium Classification(分类):原生分类,适合矢量面压盖倾斜模型做高亮;优点:不需要改模型、适合没有 BatchTable 的倾斜;缺点:只能面压盖,不方便按楼层、分户属性过滤。
- CustomShader(着色器筛选) :本篇主推方案,读取
batchId拿到 BatchTable 属性,在片元着色器判断属性,实现:隐藏指定楼层、分户变色高亮、隔离显示某一层。适合 BIM、已经单体化带属性的 3DTiles。 - 楼层显隐 :着色器内判断
floorId,不符合条件直接discard; - 分户高亮:匹配 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 为空,前端无法筛选。
四、高频踩坑清单
- ❌ getBatchTableString 报错,着色器不生效
✅ 低版本 Cesium 不支持这个内置函数,建议 ≥1.100;老版本只能用数字类型getBatchTableFloat; - ❌ 筛选完全没反应,所有模型都不高亮
✅ 字段名大小写、字符串值和 BatchTable 不一致;或者你的模型根本没有做单体化,BatchTable 只有一条; - ❌ Classification 面看不到颜色
✅ classificationType 必须是CESIUM_3D_TILE,不是 TERRAIN;多边形高度要高于地形低于建筑; - ❌ discard 之后模型出现破洞、闪烁
✅ 瓦片 LOD 切换,不同批次 batchId 不一致;预处理的时候保证同一构件 batchId 统一; - ❌ BIM 构件太多,着色器卡顿
✅ 不要在 shader 写超长 if 判断;复杂筛选提前在前端整理 ID 列表,或者后端分瓦片。
五、扩展业务方案
- 楼层下拉多选:前端维护选中楼层数组,动态拼接 fragmentShader 文本,重新生成 CustomShader;
- 分户隔离 + 剖切:结合前面的 Shader 裁切,选中一户之后把周边墙体裁掉看内部;
- BIM 构件状态:给构件增加状态属性(正常 / 告警 / 检修),着色器根据状态自动变色;
- 分层透明:不 discard,而是降低 alpha 透明度,保留其他楼层半透明,看剖面效果。