Cesium 加载 3D Tiles 倾斜摄影、压平、样式设置(Vue3 实战)

一、3D Tiles 基础说明

3D Tiles 是 Cesium 推出的三维瓦片规范,专门用于海量实景倾斜模型、BIM、点云。

  • 格式:瓦片文件夹包含 tileset.json 作为入口文件
  • 核心对象:Cesium.Cesium3DTileset
  • 常用能力:加载实景模型、样式修改、压平(clippingPlane / 3DTilesClipping)、选中高亮、透明度、显示隐藏

⚠️ 注意:倾斜模型资源一般很大,本地开发建议用 nginx 静态代理,不能直接本地 file 协议打开 tileset.json,会跨域报错。

二、完整组件代码 Cesium3DTiles.vue

复制代码
<template>
  <div class="cesium-page">
    <div ref="cesiumRef" id="cesiumContainer"></div>
    <!-- 简易控制面板 -->
    <div class="control-panel">
      <button @click="flyToTileset">定位到模型</button>
      <button @click="setOpacity(0.5)">半透明</button>
      <button @click="setOpacity(1)">不透明</button>
      <button @click="openFlat">开启压平</button>
      <button @click="closeFlat">关闭压平</button>
      <button @click="highlightAll">全部高亮</button>
      <button @click="clearStyle">清除样式</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 clippingPlaneCollection = null

window.CESIUM_BASE_URL = '/cesium/'

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

onBeforeUnmount(() => {
  // 销毁瓦片集,释放显存
  if (tileset) {
    tileset.destroy()
    tileset = null
  }
  if (clippingPlaneCollection) {
    clippingPlaneCollection.destroy()
    clippingPlaneCollection = null
  }
  if (viewer) {
    viewer.destroy()
    viewer = null
  }
})

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'

  // 开启深度检测,倾斜模型必开,防止闪烁(z-fighting)
  viewer.scene.globe.depthTestAgainstTerrain = true

  loadTileset()
}

/**
 * 加载3D Tiles倾斜摄影
 */
function loadTileset() {
  tileset = new Cesium.Cesium3DTileset({
    // 替换成你的 tileset.json 地址,本地静态服务或者后端服务地址
    url: 'http://localhost:8080/tiles/tileset.json',
    // 预加载优化
    maximumScreenSpaceError: 16,
    skipLevelOfDetail: true
  })

  tileset.readyPromise.then((tiles) => {
    console.log('3D Tiles 加载成功', tiles)
    // 模型加载完成自动飞到模型范围
    viewer.zoomTo(tiles, new Cesium.HeadingPitchRange(0, -0.3, 0))
  }).catch(err => {
    console.error('3D Tiles加载失败:', err)
  })

  viewer.scene.primitives.add(tileset)
}

/**
 * 相机定位到倾斜模型
 */
function flyToTileset() {
  if (!tileset || !tileset.ready) return
  viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0, -0.3, 0))
}

/**
 * 设置模型透明度
 * @param {number} val 0~1
 */
function setOpacity(val) {
  if (!tileset || !tileset.ready) return
  tileset.style = new Cesium.Cesium3DTileStyle({
    color: {
      evaluateColor: function (feature) {
        const c = feature.getProperty('color') || Cesium.Color.WHITE
        return new Cesium.Color(c.red, c.green, c.blue, val)
      }
    }
  })
}

/**
 * 全部模型高亮(红色)
 */
function highlightAll() {
  if (!tileset || !tileset.ready) return
  tileset.style = new Cesium.Cesium3DTileStyle({
    color: 'color("red",0.6)'
  })
}

/**
 * 清除自定义样式,恢复原始纹理
 */
function clearStyle() {
  if (!tileset || !tileset.ready) return
  tileset.style = undefined
}

/**
 * 开启压平(平面裁切,常用开挖、压平地面)
 * 原理:ClippingPlane 裁切平面,平面一侧的模型被隐藏
 */
function openFlat() {
  if (!tileset || !tileset.ready) return
  clippingPlaneCollection = new Cesium.ClippingPlaneCollection({
    planes: [
      // 法向量 + 距离,这里示例向下裁切,压平模型上部
      new Cesium.ClippingPlane(new Cesium.Cartesian3(0, 0, -1), 30)
    ],
    unionClippingRegions: false,
    edgeColor: Cesium.Color.fromCssColorString('#00ffff')
  })
  tileset.clippingPlanes = clippingPlaneCollection
}

/**
 * 关闭压平裁切
 */
function closeFlat() {
  if (!tileset) return
  tileset.clippingPlanes = null
  if (clippingPlaneCollection) {
    clippingPlaneCollection.destroy()
    clippingPlaneCollection = null
  }
}
</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;
}
.control-panel button {
  margin: 4px;
  padding: 4px 8px;
  cursor: pointer;
}
</style>

三、核心知识点详解

1. Cesium3DTileset 常用参数

  • url:tileset.json 服务地址,重点:必须 http/https 静态服务
  • maximumScreenSpaceError:屏幕空间误差,越小模型越精细,性能越低,一般 16~32
  • skipLevelOfDetail:跳过细节层级,提升加载速度
  • readyPromise:异步加载完成回调,很多操作必须等 ready 之后执行

加载后添加到场景:viewer.scene.primitives.add(tileset)

2. Cesium3DTileStyle 样式系统(重点)

样式支持表达式,可以根据模型自带属性动态着色,不修改原始模型:

复制代码
// 简单写法
tileset.style = new Cesium.Cesium3DTileStyle({
  color: 'color("#069BFD",0.7)'
})

进阶:根据属性条件着色,比如楼层、构件类型

复制代码
tileset.style = new Cesium.Cesium3DTileStyle({
  color: {
    conditions: [
      ['${name} === "楼房"', 'color("#ff3333")'],
      ['${name} === "道路"', 'color("#aaaaaa")'],
      ['true', 'color("#ffffff")']
    ]
  }
})
  • color:颜色 + 透明度
  • show:布尔表达式,控制要素是否显示
  • 清除样式直接赋值 tileset.style = undefined

3. 模型压平 / 裁切 ClippingPlane

很多人叫 "压平、开挖、剖切",底层是 ClippingPlaneCollection

  • Cartesian3(0,0,-1):法向量,代表平面朝向
  • 第二个参数:距离,正数 / 负数控制哪一侧保留、哪一侧裁掉
  • edgeColor:裁切边缘描边颜色,剖切的时候非常好看

局限:单平面只能一刀切;如果需要矩形框开挖、多边形压平,需要多个 ClippingPlane 组合成包围盒

4. 深度冲突 z-fighting(模型闪烁)

倾斜摄影最常见坑:模型和地形重叠闪烁

✅ 解决方案:

复制代码
viewer.scene.globe.depthTestAgainstTerrain = true

如果模型还是闪烁,可以抬高 3Dtileset 高度,或者关闭地形。

四、3D Tiles 拾取选中高亮(扩展常用功能)

在上面代码基础上增加点击拾取,选中单个构件高亮:

复制代码
function registerPick() {
  const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  handler.setInputAction(function (click) {
    const pick = viewer.scene.pick(click.position)
    if (Cesium.defined(pick) && Cesium.defined(pick.primitive) && pick.primitive instanceof Cesium.Cesium3DTileset) {
      const feature = pick.id
      // 按属性高亮
      tileset.style = new Cesium.Cesium3DTileStyle({
        color: {
          conditions: [
            ['${id} === ' + feature.getProperty('id'), 'color(#00ff00,0.6)'],
            ['true', 'color(#ffffff)']
          ]
        }
      })
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

五、高频踩坑汇总

  1. ❌ 直接本地打开 tileset.json,跨域报错
    ✅ 使用 nginx、vite-plugin-static-copy、后端静态资源服务托管瓦片
  2. ❌ 模型加载成功但是黑屏 / 看不到
    ✅ 检查坐标系!倾斜摄影一般是 EPSG:4326 / EPSG:3857,和 Cesium 一致;另外检查 maximumScreenSpaceError
  3. ❌ 压平裁切不生效
    ✅ 必须等 tileset.ready,法向量方向和距离值要调整,正负控制裁切方向
  4. ❌ 样式设置之后一直不生效
    ✅ 样式是 Cesium3DTileStyle 对象,不能直接赋值字符串,要 new;清除样式赋值 undefined
  5. ❌ 页面切换路由显存暴涨,浏览器卡死
    ✅ 组件销毁时 tileset.destroy(),瓦片会占用大量 GPU 显存,必须销毁
  6. ❌ 模型和地形重叠疯狂闪烁
    ✅ depthTestAgainstTerrain = true
相关推荐
usdoc文档预览17 小时前
USDOC系列之PPT文件在线编辑-VUE前端实现在线浏览器在线编辑PPT文档
前端·vue.js·编辑器·office365·ppt在线编辑·office文件在线编辑·文件在线编辑
Java小卷17 小时前
面向 schema 编程,轻松搞定 Element 虚拟化表格复杂应用场景
vue.js·低代码·element
仨孩子的爹19 小时前
Lodop、hiprint 与 vue-print-designer:打印控件选型对比
vue.js
ruanno_top1 天前
四个 canvas 组件,在 2 倍屏上一直是糊的
vue.js
ruanno_top1 天前
一个脏数据能让整条折线消失:图表组件的四种数据契约
vue.js
AGI_Eval2 天前
AI 3D生成技术:从“可用”走向“可靠”还有多远?
人工智能·3d
FlyWIHTSKY2 天前
vue2和vue3有什么区别,用法上有哪些区别
前端·javascript·vue.js
ruanno_top2 天前
能点,但 Tab 不到:两个组件把「可操作」做了一半
vue.js
爱吃烧烤的技术渣2 天前
别再手写 CRUD 了:我把 Element Plus 的工作量压缩成一份列 schema
前端·vue.js