Vue3 + Cesium 实战:本地上传 GLB 模型、模型上叠加 GLB、点击控制透明度

前言

  • 用户要自己上传GLB 文件看效果(不经过打包目录);
  • 一个 GLB 已经放好了,要在它上面再叠一层GLB(比如先放主体建筑,再放设备层 / 管线层);
  • 点击某个模型要能变半透明 / 恢复不透明(做内部透视、叠加查看)。

这三件事本篇一次性解决,全部基于 Entity + 原生 Cesium,不引第三方。

一、核心 API 认知(先搞懂再写码)

表格

需求 用到的 API 说明
本地上传 URL.createObjectURL(file) 把本地文件转成可加载的 blob 地址
加载 GLB entities.add({ model: { uri } }) Entity 方式,uri 可以是 blob 地址
叠加第二层 相同经纬度 + model.height/heading 微调 多层模型同位置叠放
拾取 scene.pick(windowPosition) 拿到点击的 Entity
控制透明 entity.model.color = Color.WHITE.withAlpha(a) alpha=1 不透明,越小越透明
高亮选中 color.getResult() 保留原色 与透明互斥

⚠️ 关键点:控制 GLB 透明度,改的不是model.opacity,而是model.color 。设置 color 后模型整体会乘以该颜色,用withAlpha()就能实现半透明。模型自带材质会被乘掉颜色,所以要么整体透明、要么先保留原色再改 alpha。

二、第一步:本地文件上传 GLB

2.1 上传 input 组件

复制代码
<template>
  <label class="upload-btn">
    选择GLB文件
    <input type="file" accept=".glb,.gltf" @change="onUploadModel" hidden />
  </label>
</template>

2.2 处理上传并加载

复制代码
import * as Cesium from 'cesium'

// 当前已加载的模型引用
const modelEntities = []

/**
 * 本地上传GLB并加载到指定坐标
 * @param {Event} e input change事件
 */
const onUploadModel = (e) => {
  const file = e.target.files[0]
  if (!file) return
  // 本地文件转blob地址,Cesium可以直接加载
  const url = URL.createObjectURL(file)
  const entity = addModel(url, 108.94, 34.27, 0, {
    name: file.name,
    scale: 1
  })
  modelEntities.push(entity)
  // 选完重置input,方便重复上传同一文件
  e.target.value = ''
  console.log('已加载模型:', file.name)
}

/**
 * 添加模型(封装,uri支持线上地址或本地blob)
 */
const addModel = (uri, lng, lat, height = 0, opts = {}) => {
  return viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(lng, lat, height),
    model: {
      uri,
      scale: opts.scale ?? 1,
      minimumPixelSize: opts.minimumPixelSize ?? 32,
      maximumScale: opts.maximumScale ?? 20000,
      runAnimations: opts.runAnimations ?? true,
      clampToGround: opts.clampToGround ?? false
    },
    name: opts.name ?? '模型',
    // 存一个自定义属性,方便后面识别"可点击控制"
    _clickable: true
  })
}

注意:Entity 的model.uri支持 blob 地址,所以本地上传不需要后端 。用完后建议URL.revokeObjectURL(url)释放内存(在删除实体时做)。

三、第二步:在 GLB 上叠加另一层 GLB

核心思想:两个模型使用相同经纬度,靠 height(或 heading 偏转)区分层次。

复制代码
/**
 * 在指定位置叠加第二层GLB(用于分层建模)
 * @param {string} uri
 * @param {number} lng,lat 基准坐标
 * @param {number} baseHeight 第一层所在高度
 * @param {number} layerHeight 第二层相对抬升高度
 */
const addLayerModel = (uri, lng, lat, baseHeight = 0, layerHeight = 5, opts = {}) => {
  // 第二层在baseHeight基础上抬升,形成叠加效果
  return addModel(uri, lng, lat, baseHeight + layerHeight, {
    name: opts.name ?? '上层模型',
    scale: opts.scale ?? 1,
    // 可稍微偏转角度,错开看清叠放
    heading: opts.heading ?? 0
  })
}

// 使用示例:先放主体,再放设备层
const baseEntity = addModel('/models/building.glb', 108.94, 34.27, 0, { name: '主体' })
const topEntity  = addLayerModel('/models/equipment.glb', 108.94, 34.27, 0, 8, { name: '设备层' })

若模型本身带了平移,position高度叠加可能不明显;此时改用模型内部 origin 平移 更可控,但简单场景height叠加够用。

四、第三步:点击拾取 + 控制透明度(核心)

4.1 开启点击拾取监听

复制代码
let selectedEntity = null // 当前选中/透明的模型

const setupPickHandler = () => {
  // 监听鼠标左键点击
  const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  handler.setInputAction((movement) => {
    const picked = viewer.scene.pick(movement.position)
    if (!Cesium.defined(picked) || !Cesium.defined(picked.id)) {
      // 点空了:清除上一个高亮
      resetTransparency()
      return
    }
    const entity = picked.id
    // 只处理我们标记为可点击的模型
    if (entity?._clickable) {
      toggleTransparency(entity)
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

4.2 透明度控制核心函数

复制代码
let transparentEntity = null // 当前处于半透明的模型

/**
 * 切换模型透明度(半透明 ⇄ 不透明)
 * @param {Cesium.Entity} entity
 */
const toggleTransparency = (entity) => {
  // 先恢复之前透明的那个
  if (transparentEntity && transparentEntity !== entity) {
    setOpacity(transparentEntity, 1)
  }
  if (transparentEntity === entity) {
    // 再次点击同一个:恢复不透明
    setOpacity(entity, 1)
    transparentEntity = null
    console.log('已恢复不透明')
  } else {
    // 设为半透明
    setOpacity(entity, 0.3)
    transparentEntity = entity
    console.log('已半透明 0.3')
  }
}

/**
 * 设置模型透明度
 * @param {Cesium.Entity} entity
 * @param {number} alpha 0~1,1为不透明
 */
const setOpacity = (entity, alpha) => {
  if (!entity?.model) return
  // 保留原色(模型默认白色,直接乘alpha)
  entity.model.color = Cesium.Color.WHITE.withAlpha(alpha)
  // 关键:半透明后开启深度测试穿透,否则会被其他模型挡住看不见
  entity.model.disableDepthTestDistance = Number.POSITIVE_INFINITY
}

/**
 * 一键恢复所有模型不透明
 */
const resetTransparency = () => {
  if (transparentEntity) {
    setOpacity(transparentEntity, 1)
    transparentEntity = null
  }
}

4.3 半透明 + 保留原色的进阶写法

如果想让模型半透明时还保留大致颜色,不要直接用纯白:

复制代码
const setOpacityKeepColor = (entity, alpha) => {
  // 先取模型当前基色(这里用白色+alpha即可,或记录原color)
  const baseColor = entity._originColor ?? Cesium.Color.WHITE
  entity._originColor = baseColor
  entity.model.color = baseColor.withAlpha(alpha)
  entity.model.disableDepthTestDistance = Number.POSITIVE_INFINITY
}

说明:disableDepthTestDistance设为无穷大,半透明模型才不会因为深度测试被后层模型遮挡到看不见,这是透明控制最常见的坑。

五、完整可运行示例(合并版)

复制代码
<template>
  <CesiumMap ref="mapRef" />
  <div class="panel">
    <label class="btn">
      上传GLB
      <input type="file" accept=".glb,.gltf" @change="onUpload" hidden />
    </label>
    <button class="btn" @click="onAddLayer">叠加一层GLB</button>
    <button class="btn" @click="onReset">全部恢复不透明</button>
    <p class="tip">点击场景中的模型可切换半透明</p>
  </div>
</template>

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

const mapRef = ref(null)
let viewer = null
let handler = null
let modelEntities = []
let transparentEntity = null

onMounted(() => {
  viewer = mapRef.value.viewer
  setupPickHandler()
})

onBeforeUnmount(() => {
  handler?.destroy()
  viewer?.destroy()
})

// ---------- 加载模型 ----------
const addModel = (uri, lng, lat, height = 0, opts = {}) => {
  const entity = viewer.entities.add({
    position: Cesium.Cartesian3.fromDegrees(lng, lat, height),
    model: {
      uri,
      scale: opts.scale ?? 1,
      minimumPixelSize: 32,
      maximumScale: 20000,
      runAnimations: true,
      clampToGround: false
    },
    name: opts.name ?? '模型',
    _clickable: true
  })
  modelEntities.push(entity)
  return entity
}

// ---------- 上传 ----------
const onUpload = (e) => {
  const file = e.target.files[0]
  if (!file) return
  const url = URL.createObjectURL(file)
  addModel(url, 108.94, 34.27, 0, { name: file.name })
  e.target.value = ''
}

// ---------- 叠加一层 ----------
const onAddLayer = () => {
  addModel('/models/equipment.glb', 108.94, 34.27, 8, { name: '设备层', scale: 0.5 })
}

// ---------- 点击拾取 ----------
const setupPickHandler = () => {
  handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  handler.setInputAction((movement) => {
    const picked = viewer.scene.pick(movement.position)
    if (!Cesium.defined(picked) || !Cesium.defined(picked.id)) {
      resetTransparency()
      return
    }
    if (picked.id?._clickable) toggleTransparency(picked.id)
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

// ---------- 透明度控制 ----------
const setOpacity = (entity, alpha) => {
  if (!entity?.model) return
  entity.model.color = Cesium.Color.WHITE.withAlpha(alpha)
  entity.model.disableDepthTestDistance = Number.POSITIVE_INFINITY
}

const toggleTransparency = (entity) => {
  if (transparentEntity && transparentEntity !== entity) {
    setOpacity(transparentEntity, 1)
  }
  if (transparentEntity === entity) {
    setOpacity(entity, 1)
    transparentEntity = null
  } else {
    setOpacity(entity, 0.3)
    transparentEntity = entity
  }
}

const onReset = () => {
  modelEntities.forEach((en) => setOpacity(en, 1))
  transparentEntity = null
}
</script>

六、踩坑记录(重点)

  1. 透明不生效 :改的不是model.opacity,必须用model.color.withAlpha()
  2. 半透明模型看不见 :被其他模型深度测试遮挡,加disableDepthTestDistance = Infinity
  3. 上传后加载失败 :blob 地址要URL.createObjectURL;跨域场景优先转成 DataURL 或走后端
  4. 叠加两层看不到第二层 :height 不够,或者两个模型 scale 太大互相重叠,检查scale和高度
  5. 重复点击混乱 :维护一个transparentEntity单例引用,先恢复旧的再切换
  6. 模型自带动画被 color 影响 :半透明对动画材质(如闪动)可能异常,必要时用Primitive方案
  7. 销毁释放内存 :删除模型时记得URL.revokeObjectURL(url) + viewer.entities.remove(entity)

七、扩展方向(博客结尾)

  • 点击模型弹出属性面板(名称、坐标、所属图层)
  • 多选模型批量调透明度 / 隐藏
  • Primitive 方式加载高性能模型(海量)
  • 模型拖拽、旋转、贴地贴合
  • 分层模型联动(点击某层,其余半透明)

总结

这篇覆盖了三个高频能力:本地上传 GLB、GLB 上叠 GLB、点击控制透明度 。核心就一句 ------ 透明度改model.color.withAlpha(),穿透加disableDepthTestDistance。配合前面几篇的useCesiumGis封装,日常数字孪生 / 设备模型叠加场景基本够用。

相关推荐
拖孩1 小时前
流量主门槛从 500 降到 100,我的小程序上线 18 小时够到了线
前端·后端·微信小程序
深蓝AI1 小时前
编码 Agent 的界面味靠提示词压不住:Impeccable 用无模型规则拦同一套模板
前端·程序员
January12072 小时前
【AI方法笔记】2026-11-02:模型流式吐 JSON 时,前端会遇到四种崩法
前端·人工智能·笔记·json
asdzx672 小时前
C# 操作 PDF 元数据:从属性写入到字段提取的完整流程
前端·pdf·c#
吹什么轩3 小时前
constexpr 完全指南:从 const 的两种含义,到编译期计算的四次演进
java·前端·javascript
小蒜学长3 小时前
基于SpringBoot+Vue的云南地区扶贫助农系统的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·乡村振兴
Bruce_Liuxiaowei3 小时前
前端校验登录态,等于没校验——一次应急广播终端高危漏洞的发现与报送
运维·前端·安全
Meta Luo3 小时前
AI 时代,Web 自动化测试该怎么做?
前端·人工智能·playwright 替代工具·无代码自动化测试工具·低代码 ui 自动化测试·web 回归测试工具
去伪存真3 小时前
AI Agent 性能评估指标与自动化测试落地方案
前端·agent