前言
- 用户要自己上传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>
六、踩坑记录(重点)
- 透明不生效 :改的不是
model.opacity,必须用model.color.withAlpha() - 半透明模型看不见 :被其他模型深度测试遮挡,加
disableDepthTestDistance = Infinity - 上传后加载失败 :blob 地址要
URL.createObjectURL;跨域场景优先转成 DataURL 或走后端 - 叠加两层看不到第二层 :height 不够,或者两个模型 scale 太大互相重叠,检查
scale和高度 - 重复点击混乱 :维护一个
transparentEntity单例引用,先恢复旧的再切换 - 模型自带动画被 color 影响 :半透明对动画材质(如闪动)可能异常,必要时用
Primitive方案 - 销毁释放内存 :删除模型时记得
URL.revokeObjectURL(url)+viewer.entities.remove(entity)
七、扩展方向(博客结尾)
- 点击模型弹出属性面板(名称、坐标、所属图层)
- 多选模型批量调透明度 / 隐藏
- Primitive 方式加载高性能模型(海量)
- 模型拖拽、旋转、贴地贴合
- 分层模型联动(点击某层,其余半透明)
总结
这篇覆盖了三个高频能力:本地上传 GLB、GLB 上叠 GLB、点击控制透明度 。核心就一句 ------ 透明度改model.color.withAlpha(),穿透加disableDepthTestDistance。配合前面几篇的useCesiumGis封装,日常数字孪生 / 设备模型叠加场景基本够用。