Appearance
三维
用 MapLibre 与 CesiumJS 加载平台的三维服务:栅格地形、网格地形、3D Tiles 实景、三维模型、点云、高斯泼溅,以及把它们叠加在一起的综合场景。除栅格地形用 MapLibre 外,其余示例都用 Cesium 的 CesiumWidget,不使用 Cesium ion,影像层用平台地图服务的切片或纯色地球。
实验功能
三维服务需要平台开启三维开关(GEOBRIDGE_FEATURE_3D=true,见功能开关)。开关关闭或平台没有对应的示例服务时,示例会提示服务不存在。示例数据以在线系统实际提供为准,数据来源与许可见示例数据。
沙箱里的 Cesium
在线沙箱在代码里用到 Cesium 时自动加载 CesiumJS 1.146,作为全局变量 Cesium 使用;第一次运行三维示例需要下载约 6 MB 的脚本。三维渲染需要浏览器支持 WebGL 2,集成显卡上首次绘制可能稍有停顿。拷到自己的页面时自行引入 CesiumJS,见在 Cesium 中使用 SDK。
栅格地形(MapLibre)
用 client.maplibre.setRasterTerrain() 把栅格地形服务设为 MapLibre 的三维地形,同时加山体阴影,底图用平台的栅格切片,斜视香港太平山。面板的滑块调夸张系数、复选框开关山体阴影;点击地图输出该点的高程(queryTerrainElevation() 返回的值乘过夸张系数,示例除回去)。加载后先读出太平山顶与海旁的高程。
需要: 一个由单波段高程数据发布的栅格地形服务,一个地图服务(切片)作底图。
js
在沙箱中打开/**
* @title 栅格地形(MapLibre)
* @description 实验功能,需平台开启三维开关。用 setRasterTerrain 把栅格地形服务设为 MapLibre 的三维地形,斜视香港太平山;面板调夸张系数、开关山体阴影,点击地图读取该点高程。
* @category 三维
* @service demo/terrain-raster
* @service demo/basemap
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的栅格地形服务(RasterTerrainServer)与底图
const dem = services['demo/terrain-raster']
const basemap = services['demo/basemap']
const map = new maplibregl.Map({
container,
style: { version: 8, sources: {}, layers: [] },
transformRequest: client.transformRequest,
// 从西北方向斜视太平山与港岛山脊
center: [114.152, 22.27],
zoom: 13.2,
pitch: 70,
bearing: 140,
maxPitch: 85,
})
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }))
await new Promise((resolve) => map.once('load', resolve))
// 底图铺在地形上;setRasterTerrain 加 raster-dem source、调用 map.setTerrain,可选山体阴影图层
client.maplibre.addRasterService(map, basemap)
let exaggeration = 1.5
const terrain = await client.maplibre.setRasterTerrain(map, dem, { exaggeration, hillshade: true, hillshadeExaggeration: 0.6 })
// 高程读取:queryTerrainElevation 返回的是乘过夸张系数的值,除回去得到真实高程
const elevation = (lngLat) => {
const h = map.queryTerrainElevation(lngLat)
return h === null ? null : Math.round(h / exaggeration)
}
// 面板:夸张系数滑块、山体阴影开关
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = `<label>夸张系数 <input type="range" min="0" max="4" step="0.1" value="${exaggeration}" style="vertical-align:middle"> <b>${exaggeration}</b></label><br><label><input type="checkbox" checked> 山体阴影</label>`
container.appendChild(panel)
const [slider, shade] = panel.querySelectorAll('input')
slider.oninput = () => {
exaggeration = Number(slider.value)
panel.querySelector('b').textContent = exaggeration
map.setTerrain({ source: terrain.sourceId, exaggeration })
}
slider.onchange = () => console.log('夸张系数改为', exaggeration)
shade.onchange = () => {
for (const id of terrain.layerIds) map.setLayoutProperty(id, 'visibility', shade.checked ? 'visible' : 'none')
console.log('山体阴影', shade.checked ? '开' : '关')
}
// 点击地图:输出经纬度与高程
map.on('click', (e) => {
const { lng, lat } = e.lngLat
console.log(`点击 ${lng.toFixed(4)}, ${lat.toFixed(4)}:高程约 ${elevation(e.lngLat)} 米`)
})
// 地形瓦片加载完后读太平山顶(约 552 米)与维多利亚港岸边的高程
map.once('idle', () => {
console.log(`地形 source:${terrain.sourceId},夸张系数 ${exaggeration}`)
console.log(`太平山顶(114.1455, 22.2759)高程约 ${elevation([114.1455, 22.2759])} 米`)
console.log(`中环海旁(114.1600, 22.2850)高程约 ${elevation([114.16, 22.285])} 米`)
})
} catch (e) {
console.error(friendly(e))
}网格地形(Cesium)
用 client.cesium.terrainProvider() 加载网格地形服务作为 Cesium 的场景地形,地球按高程着色并固定在正午打光。滑块调地形夸张(scene.verticalExaggeration);点击地形,射线与地形求交,输出经纬度与高程。加载后沿一条东西向剖面用 sampleTerrainMostDetailed() 采样高程。
需要: 一个网格地形服务(量化网格)。
js
在线系统暂未提供本示例的数据,可在自己的平台上发布同类服务后运行/**
* @title 网格地形(Cesium)
* @description 实验功能,需平台开启三维开关。用 client.cesium.terrainProvider 加载网格地形服务(量化网格),按高程着色;滑块调地形夸张,点击地形输出经纬度与高程。
* @category 三维
* @service demo/terrain-mesh
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的网格地形服务(MeshTerrainServer)
const path = services['demo/terrain-mesh']
// 只用 CesiumWidget(不用 Viewer 的控件);不用 Cesium ion 的影像,地球按高程着色
const terrainProvider = await client.cesium.terrainProvider(Cesium, path)
const widget = new Cesium.CesiumWidget(container, { baseLayer: false, terrainProvider })
const { scene, camera } = widget
// 高程配色(示例地形一带约 1000–1500 米):低处绿、中间黄褐、高处灰白
const ramp = document.createElement('canvas')
ramp.width = 256
ramp.height = 1
const ctx = ramp.getContext('2d')
const grad = ctx.createLinearGradient(0, 0, 256, 0)
for (const [t, c] of [[0, '#2f6b3a'], [0.3, '#7fa65a'], [0.55, '#c9b37a'], [0.8, '#9c7a54'], [1, '#f4f1ea']]) grad.addColorStop(t, c)
ctx.fillStyle = grad
ctx.fillRect(0, 0, 256, 1)
// 着色按显示高度计算(含夸张),夸张系数变化时配色范围同步缩放
scene.globe.material = Cesium.Material.fromType('ElevationRamp', { image: ramp, minimumHeight: 800, maximumHeight: 1900 })
const exaggerate = (k) => {
scene.verticalExaggeration = k
Object.assign(scene.globe.material.uniforms, { minimumHeight: 800 * k, maximumHeight: 1900 * k })
}
// 固定在当地正午打光,地形起伏有明暗
scene.globe.enableLighting = true
widget.clock.currentTime = Cesium.JulianDate.fromIso8601('2026-06-21T04:30:00Z')
widget.clock.shouldAnimate = false
// 示例地形中的一片山地:从南向北斜视,地形夸张 2 倍(拷到自己页面时换成你的地形范围)
exaggerate(2)
camera.setView({
destination: Cesium.Cartesian3.fromDegrees(106.7, 26.2, 6000),
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-22), roll: 0 },
})
// 面板:地形夸张滑块(scene.verticalExaggeration)
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = '<label>地形夸张 <input type="range" min="1" max="5" step="0.5" value="2" style="vertical-align:middle"> <b>2</b></label><div style="color:#868e96">点击地形读取经纬度与高程</div>'
container.appendChild(panel)
const slider = panel.querySelector('input')
slider.oninput = () => {
exaggerate(Number(slider.value))
panel.querySelector('b').textContent = slider.value
}
slider.onchange = () => console.log('地形夸张改为', slider.value)
// 点击:射线与地形求交得到经纬度与高度;拾取到的高度含夸张,除以夸张系数还原
const fmt = (c) => `${Cesium.Math.toDegrees(c.longitude).toFixed(4)}, ${Cesium.Math.toDegrees(c.latitude).toFixed(4)}`
new Cesium.ScreenSpaceEventHandler(scene.canvas).setInputAction((e) => {
const p = scene.globe.pick(camera.getPickRay(e.position), scene)
if (!p) return console.log('没有点到地形')
const c = Cesium.Cartographic.fromCartesian(p)
console.log(`点击 ${fmt(c)}:高程约 ${Math.round(c.height / scene.verticalExaggeration)} 米`)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
// 演示:沿一条东西向剖面按最精细的地形瓦片采样高程
const line = [106.62, 106.66, 106.7, 106.74, 106.78].map((lng) => Cesium.Cartographic.fromDegrees(lng, 26.3))
const samples = await Cesium.sampleTerrainMostDetailed(terrainProvider, line)
console.log('剖面(纬度 26.3)高程:' + samples.map((c) => `${fmt(c).split(',')[0]}° ${Math.round(c.height)} 米`).join(','))
} catch (e) {
console.error(friendly(e))
}3D Tiles 实景(Cesium)
用 client.cesium.addTileset() 加载实景三维 3D Tiles,影像层用平台地图服务的切片,client.cesium.flyToExtent() 按服务的数据范围斜视飞行。滑块调最大屏幕误差(越小越精细、请求的瓦片越多),每次视野内瓦片加载完成时输出累计加载的瓦片数;点击建筑输出点中位置的经纬度与高度。
需要: 一个 3D Tiles 服务,一个地图服务(切片)作影像层。
js
在沙箱中打开/**
* @title 3D Tiles 实景(Cesium)
* @description 实验功能,需平台开启三维开关。用 client.cesium.addTileset 加载香港实景三维 3D Tiles,平台栅格底图作影像,按服务范围飞行;滑块调最大屏幕误差,日志统计已加载的瓦片。
* @category 三维
* @service demo/tiles3d
* @service demo/basemap
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的 3D Tiles 服务(Cesium3DTileServer)与底图
const path = services['demo/tiles3d']
const basemap = services['demo/basemap']
// 只用 CesiumWidget;影像用平台 MapServer 的切片({z}/{y}/{x},Web 墨卡托),不用 Cesium ion
const widget = new Cesium.CesiumWidget(container, { baseLayer: false })
const { scene, camera } = widget
const tiles = client.cesium.resource(Cesium, client.services.tileUrl(basemap))
widget.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({ url: tiles, maximumLevel: 19 }))
// 切片里没有要素的地方(海面等)是透明的,地球底色取与地图方案相同的浅色
widget.scene.globe.baseColor = Cesium.Color.fromCssColorString('#f5f3ee')
// 加载瓦片集并加入场景;按服务的数据范围斜视飞行(范围来自 3D Tiles 元数据)
const tileset = await client.cesium.addTileset(Cesium, widget, path, { maximumScreenSpaceError: 16 })
const box = await client.cesium.flyToExtent(Cesium, widget, path, { type: 'Cesium3DTileServer', duration: 0, pitch: -40 })
const c = Cesium.Cartographic.fromCartesian(tileset.boundingSphere.center)
console.log(`瓦片集已加载:中心 ${Cesium.Math.toDegrees(c.longitude).toFixed(4)}, ${Cesium.Math.toDegrees(c.latitude).toFixed(4)},包围球半径 ${Math.round(tileset.boundingSphere.radius)} 米`)
if (box) console.log('服务范围(经纬度):' + box.map((v) => v.toFixed(4)).join(', '))
// 统计:每次视野内瓦片全部加载完成时,输出累计加载的瓦片数
let loaded = 0
tileset.tileLoad.addEventListener(() => loaded++)
tileset.allTilesLoaded.addEventListener(() => console.log(`最大屏幕误差 ${tileset.maximumScreenSpaceError}:视野内瓦片加载完成,累计加载 ${loaded} 个`))
// 面板:最大屏幕误差越小越精细(请求的瓦片越多)
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = '<label>最大屏幕误差 <input type="range" min="1" max="64" step="1" value="16" style="vertical-align:middle"> <b>16</b></label><br><button>飞到数据范围</button>'
container.appendChild(panel)
const slider = panel.querySelector('input')
slider.oninput = () => {
tileset.maximumScreenSpaceError = Number(slider.value)
panel.querySelector('b').textContent = slider.value
}
panel.querySelector('button').onclick = () => client.cesium.flyToExtent(Cesium, camera, path, { type: 'Cesium3DTileServer' })
// 点击建筑:输出点中位置的经纬度与高度(pickPosition 读深度缓冲)
new Cesium.ScreenSpaceEventHandler(scene.canvas).setInputAction((e) => {
const p = scene.pick(e.position) && scene.pickPosition(e.position)
if (!p) return console.log('没有点到实景模型')
const g = Cesium.Cartographic.fromCartesian(p)
console.log(`点中 ${Cesium.Math.toDegrees(g.longitude).toFixed(5)}, ${Cesium.Math.toDegrees(g.latitude).toFixed(5)},高度 ${g.height.toFixed(1)} 米`)
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
} catch (e) {
console.error(friendly(e))
}三维模型(Cesium)
用 client.cesium.addModels() 把三维模型服务里的 glb 按控制台设置的默认位置放进场景。列表点击飞到各模型;点击场景中的模型,用 scene.pick() 判断点中的是哪个模型并输出名称。加载后先飞到第一个模型,并在它的屏幕位置模拟一次点击。
需要: 一个三维模型服务,模型已设置默认位置。
js
在沙箱中打开/**
* @title 三维模型(Cesium)
* @description 实验功能,需平台开启三维开关。用 client.cesium.addModels 按默认位置放置三维模型服务中的 glb 模型;列表点击飞到各模型,点击场景中的模型输出名称。
* @category 三维
* @service demo/models
* @service demo/basemap
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的三维模型服务(3DModelServer,模型须在控制台设好默认位置)与底图
const path = services['demo/models']
const basemap = services['demo/basemap']
const widget = new Cesium.CesiumWidget(container, { baseLayer: false })
const { scene, camera } = widget
const tiles = client.cesium.resource(Cesium, client.services.tileUrl(basemap))
widget.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({ url: tiles, maximumLevel: 19 }))
// 切片里没有要素的地方(海面等)是透明的,地球底色取与地图方案相同的浅色
widget.scene.globe.baseColor = Cesium.Color.fromCssColorString('#f5f3ee')
// 固定在当地上午打光
widget.clock.currentTime = Cesium.JulianDate.fromIso8601('2026-06-21T02:00:00Z')
widget.clock.shouldAnimate = false
// 清单中的 glb 按默认位置(经纬高、朝向、缩放)放置;没有默认位置等原因未加载的列在 skipped
const result = await client.cesium.addModels(Cesium, widget, path, { flyTo: true, duration: 0 })
for (const s of result.skipped) console.warn(`未加载 ${s.name}:${s.message}`)
// 模型在首帧渲染后才就绪(之后才有包围球)
const ready = (m) => (m.ready ? Promise.resolve() : new Promise((r) => m.readyEvent.addEventListener(r)))
await Promise.all(result.models.map(({ model }) => ready(model)))
console.log(`已放置 ${result.models.length} 个模型:${result.models.map((m) => m.name).join('、')}`)
// 列表:点击飞到该模型(斜视,距离按包围球半径)
const flyTo = (model, duration) =>
camera.flyToBoundingSphere(model.boundingSphere, { duration, offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-20), model.boundingSphere.radius * 4) })
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:6px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = '<b>模型</b>'
for (const { name, model } of result.models) {
const row = document.createElement('div')
row.innerHTML = `<a href="#" style="color:#1c7ed6">${name}</a>`
row.onclick = (e) => {
e.preventDefault()
flyTo(model)
console.log('飞到', name)
}
panel.appendChild(row)
}
container.appendChild(panel)
// 点击:scene.pick 得到被点中的图元,与本次加入的模型比对
const nameOf = (picked) => result.models.find((m) => picked && m.model === picked.primitive)?.name
new Cesium.ScreenSpaceEventHandler(scene.canvas).setInputAction((e) => {
console.log('点击了', nameOf(scene.pick(e.position)) ?? '空白处')
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
// 演示:飞到第一个模型,再在它的包围球中心在屏幕上的位置模拟一次点击
const first = result.models[0]
if (first) {
flyTo(first.model, 0)
// 等两帧:相机就位并重绘之后再拾取
const nextFrame = () => new Promise((r) => { const off = scene.postRender.addEventListener(() => (off(), r())) })
await nextFrame()
await nextFrame()
const pos = Cesium.SceneTransforms.worldToWindowCoordinates(scene, first.model.boundingSphere.center)
if (pos) console.log(`演示点击屏幕 (${Math.round(pos.x)}, ${Math.round(pos.y)}):点中`, nameOf(scene.pick(pos)) ?? '空白处')
}
} catch (e) {
console.error(friendly(e))
}点云着色(Cesium)
加载由 LAS / LAZ 发布的点云 3D Tiles,用 client.cesium.pointCloudStyle() 在 RGB、分类、强度、高度四种着色之间切换,滑块调点大小,图例随着色方式变化(分类图例用 SDK 导出的 GeoBridge.CLASSIFICATION_COLORS)。加载后输出平台写在 tileset.json 里的点数、坐标系与各属性的取值范围。
需要: 一个由 LAS / LAZ 发布、已完成转换的点云服务。
js
在沙箱中打开/**
* @title 点云着色(Cesium)
* @description 实验功能,需平台开启三维开关。加载由 LAS / LAZ 发布的点云 3D Tiles,用 client.cesium.pointCloudStyle 按 RGB、分类、强度、高度切换着色,滑块调点大小,图例随着色方式变化。
* @category 三维
* @service demo/pointcloud
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的点云服务(由 LAS / LAZ 发布的 Cesium3DTileServer)
const path = services['demo/pointcloud']
const widget = new Cesium.CesiumWidget(container, { baseLayer: false })
widget.scene.globe.baseColor = Cesium.Color.fromCssColorString('#2b2f36')
const tileset = await client.cesium.addTileset(Cesium, widget, path, { flyTo: true, duration: 0 })
// 平台写在 tileset.json 里的点云信息:点数、坐标系、属性及取值范围
const info = tileset.extras?.geobridge ?? {}
console.log(`点云 ${info.source ?? ''}:${(info.pointCount ?? 0).toLocaleString()} 点,坐标系 ${info.crs},有颜色:${info.hasRgb ? '是' : '否'}`)
console.log('逐点属性:' + Object.entries(tileset.properties ?? {}).map(([k, v]) => `${k} ${v.minimum}–${v.maximum}`).join(','))
// 图例:分类用 SDK 的 ASPRS 默认配色;强度、高度为渐变
const NAMES = { 1: '未分类', 2: '地面', 3: '低植被', 4: '中植被', 5: '高植被', 6: '建筑', 7: '噪点', 9: '水体', 17: '桥梁' }
const [hMin, hMax] = info.heightRange ?? [0, 100]
const swatch = (c) => `<span style="display:inline-block;width:12px;height:12px;border-radius:2px;vertical-align:middle;margin-right:4px;background:${c}"></span>`
const bar = (css, a, b) => `<div style="height:10px;width:160px;background:linear-gradient(90deg,${css})"></div><div style="display:flex;justify-content:space-between;width:160px">${a}<span>${b}</span></div>`
const LEGENDS = {
rgb: '点自带的颜色',
classification: Object.entries(GeoBridge.CLASSIFICATION_COLORS).map(([k, c]) => `${swatch(c)}${k} ${NAMES[k] ?? ''}`).join('<br>'),
intensity: bar('#141414,#f2f2f2', '弱', '强'),
height: bar('blue,cyan,lime,yellow,red', `${Math.round(hMin)} 米`, `${Math.round(hMax)} 米`),
}
// 面板:四种着色按钮、点大小滑块、图例
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:12px/1.8 sans-serif'
const BY = [['rgb', 'RGB'], ['classification', '分类'], ['intensity', '强度'], ['height', '高度']]
panel.innerHTML = BY.map(([by, t]) => `<button data-by="${by}">${t}</button> `).join('') +
'<br><label>点大小 <input type="range" min="1" max="8" step="1" value="2" style="vertical-align:middle"> <b>2</b></label><div class="legend"></div>'
container.appendChild(panel)
const slider = panel.querySelector('input')
let by = info.hasRgb ? 'rgb' : 'height'
const apply = () => {
tileset.style = client.cesium.pointCloudStyle(Cesium, tileset, by, { pointSize: Number(slider.value) })
panel.querySelector('b').textContent = slider.value
panel.querySelector('.legend').innerHTML = LEGENDS[by]
for (const b of panel.querySelectorAll('button')) b.style.fontWeight = b.dataset.by === by ? 'bold' : ''
}
for (const b of panel.querySelectorAll('button')) {
b.onclick = () => {
by = b.dataset.by
apply()
console.log('着色方式:', b.textContent)
}
}
slider.oninput = apply
apply()
// 视野内瓦片加载完成后输出一次统计
let tiles = 0
tileset.tileLoad.addEventListener(() => tiles++)
tileset.allTilesLoaded.addEventListener(() => console.log(`视野内点云瓦片加载完成:累计 ${tiles} 个,当前着色 ${by}`))
} catch (e) {
console.error(friendly(e))
}高斯泼溅(Cesium)
三维模型服务里的 .ply、.splat、.spz 发布后由平台转为高斯泼溅 3D Tiles。示例先列出各文件的转换状态,再用 client.cesium.addModels() 加载转换好的瓦片集,按钮飞到各个泼溅。泼溅每帧都要排序与混合,示例开启了按需渲染(requestRenderMode),画面静止时不重绘。
需要: 一个含高斯泼溅文件的三维模型服务,泼溅已设置默认位置并转换完成;CesiumJS 1.146 及以上。
js
在线系统暂未提供本示例的数据,可在自己的平台上发布同类服务后运行/**
* @title 高斯泼溅(Cesium)
* @description 实验功能,需平台开启三维开关。三维模型服务里的 .ply / .splat / .spz 发布后由平台转为高斯泼溅 3D Tiles,用 client.cesium.addModels 加载;按钮飞到各个泼溅。
* @category 三维
* @service demo/splat
* @service demo/basemap
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问。泼溅渲染需要 CesiumJS 1.146 及以上
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的三维模型服务(含泼溅文件,须设好默认位置并转换完成)与底图
const path = services['demo/splat']
const basemap = services['demo/basemap']
// 按需渲染:画面静止时不重绘(泼溅每帧要排序与混合,开销大)
const widget = new Cesium.CesiumWidget(container, { baseLayer: false, requestRenderMode: true, maximumRenderTimeChange: Infinity })
const { camera } = widget
const tiles = client.cesium.resource(Cesium, client.services.tileUrl(basemap))
widget.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({ url: tiles, maximumLevel: 19 }))
// 切片里没有要素的地方(海面等)是透明的,地球底色取与地图方案相同的浅色
widget.scene.globe.baseColor = Cesium.Color.fromCssColorString('#f5f3ee')
// 先看清单:每个泼溅文件的转换状态(ready 才有 tilesetUrl)
const list = await client.services.models(path)
for (const m of list) console.log(`${m.name}:${m.kind},3D Tiles 状态 ${m.tilesStatus ?? '无'}`)
// addModels 对泼溅加载转换好的 3D Tiles(已按默认位置定位),glb 按默认位置放置;其余列在 skipped
const result = await client.cesium.addModels(Cesium, widget, path, { flyTo: true, duration: 0 })
for (const s of result.skipped) console.warn(`未加载 ${s.name}:${s.message}`)
console.log(`已加载 ${result.tilesets.length} 个泼溅瓦片集:${result.tilesets.map((t) => t.name).join('、')}`)
// 面板:每个泼溅一个按钮,飞到它的包围球(斜视,距离为半径的 2 倍)
const flyTo = ({ name, tileset }) => {
const s = tileset.boundingSphere
camera.flyToBoundingSphere(s, { offset: new Cesium.HeadingPitchRange(Cesium.Math.toRadians(20), Cesium.Math.toRadians(-20), s.radius * 2) })
console.log(`飞到 ${name}:包围球半径 ${s.radius.toFixed(1)} 米`)
}
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = '<b>飞到</b> '
for (const t of result.tilesets) {
const b = document.createElement('button')
b.textContent = t.name.replace(/\.\w+$/, '')
b.style.marginRight = '4px'
b.onclick = () => flyTo(t)
panel.appendChild(b)
}
container.appendChild(panel)
// 统计:瓦片集首次全部加载完成时输出泼溅瓦片数
for (const t of result.tilesets) {
let n = 0
t.tileset.tileLoad.addEventListener(() => n++)
t.tileset.initialTilesLoaded.addEventListener(() => console.log(`${t.name} 首屏瓦片加载完成:${n} 个`))
}
// 演示:飞到第一个泼溅
if (result.tilesets[0]) flyTo(result.tilesets[0])
} catch (e) {
console.error(friendly(e))
}WMTS 影像(Cesium)
client.cesium.wmtsImageryProvider() 读取 WMTS 能力文档,生成 WebMapTileServiceImageryProvider:经纬度矩阵集(WorldCRS84Quad、CGCS2000Quad)用 GeographicTilingScheme(第 0 级 2×1),Web 墨卡托用 WebMercatorTilingScheme,级别、矩阵标识与范围都由能力文档推出。左上角切换两种矩阵集。WMTS 是二维服务,不需要平台的三维开关。
需要: 一个开启了 WMTS 的切片地图服务。
js
在沙箱中打开/**
* @title WMTS 影像(Cesium)
* @description 用 cesium.wmtsImageryProvider 把香港底图的 WMTS 作为 Cesium 地球的影像,可在经纬度矩阵集(WorldCRS84Quad,GeographicTilingScheme)与 Web 墨卡托(WebMercatorQuad)之间切换。WMTS 不需要平台的三维开关。
* @category 三维
* @service demo/basemap
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 WMTS,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你开启了 WMTS 的地图服务路径
const path = services['demo/basemap']
// 只用 CesiumWidget,不用 Cesium ion;地球底色取与地图方案相同的浅色
const widget = new Cesium.CesiumWidget(container, { baseLayer: false })
widget.scene.globe.baseColor = Cesium.Color.fromCssColorString('#f5f3ee')
// 按矩阵集建影像提供者:SDK 按能力文档推出级别、瓦片方案与范围,瓦片请求按 Key 的传递方式带 Key
let layer = null
const use = async (tileMatrixSet) => {
const provider = await client.cesium.wmtsImageryProvider(Cesium, path, { tileMatrixSet })
if (layer) widget.imageryLayers.remove(layer)
layer = widget.imageryLayers.addImageryProvider(provider)
const scheme = provider.tilingScheme instanceof Cesium.GeographicTilingScheme ? '经纬度(第 0 级 2×1)' : 'Web 墨卡托'
console.log(`矩阵集 ${tileMatrixSet}:${scheme},级别 ${provider.minimumLevel}–${provider.maximumLevel}`)
}
await use('WorldCRS84Quad')
// 斜视维多利亚港
widget.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(114.17, 22.24, 9000),
orientation: { heading: 0, pitch: Cesium.Math.toRadians(-45), roll: 0 },
})
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:6px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.9 sans-serif'
for (const set of ['WorldCRS84Quad', 'WebMercatorQuad']) {
const label = document.createElement('label')
label.style.display = 'block'
const radio = document.createElement('input')
radio.type = 'radio'
radio.name = 'tms'
radio.checked = set === 'WorldCRS84Quad'
radio.onchange = () => use(set).catch((e) => console.error(friendly(e)))
label.append(radio, ' ' + set)
panel.appendChild(label)
}
container.appendChild(panel)
} catch (e) {
console.error(friendly(e))
}综合三维场景(Cesium)
在同一个 Cesium 场景中叠加平台地图服务的切片(影像层,限定在服务范围内)、实景 3D Tiles 与三维模型;两类三维数据各自独立加载,一个失败不影响另一个。「开始漫游」按钮依次飞过几个视点,每到一站输出一条日志。
需要: 一个地图服务(切片)、一个 3D Tiles 服务与一个三维模型服务,位置相近。
js
在沙箱中打开/**
* @title 综合三维场景(Cesium)
* @description 实验功能,需平台开启三维开关。在同一个 Cesium 场景中叠加平台栅格底图(影像层)、香港实景 3D Tiles 与三维模型;漫游按钮依次飞过金钟、维港与尖沙咀。
* @category 三维
* @service demo/basemap
* @service demo/tiles3d
* @service demo/models
*/
// 三维服务是实验功能:平台须开启三维开关(GEOBRIDGE_FEATURE_3D=true)才能发布与访问
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径(或平台未开启三维开关)' : e && e.message)
try {
// 拷到自己页面时:换成你的底图(MapServer 切片)、3D Tiles 与三维模型服务
const basemap = services['demo/basemap']
const tiles3d = services['demo/tiles3d']
const models = services['demo/models']
const widget = new Cesium.CesiumWidget(container, { baseLayer: false })
const { scene, camera } = widget
widget.clock.currentTime = Cesium.JulianDate.fromIso8601('2026-06-21T02:00:00Z')
widget.clock.shouldAnimate = false
// 影像层:平台 MapServer 切片({z}/{y}/{x},Web 墨卡托),限定在服务范围内,范围外不发请求
const box = await client.services.extent(basemap)
const imagery = new Cesium.UrlTemplateImageryProvider({
url: client.cesium.resource(Cesium, client.services.tileUrl(basemap)),
maximumLevel: 19,
...(box ? { rectangle: Cesium.Rectangle.fromDegrees(...box) } : {}),
})
widget.imageryLayers.addImageryProvider(imagery)
// 切片里没有要素的地方(海面等)是透明的,地球底色取与地图方案相同的浅色
scene.globe.baseColor = Cesium.Color.fromCssColorString('#f5f3ee')
// 实景 3D Tiles 与按默认位置放置的模型;各自独立,一个失败不影响另一个
const [tileset, placed] = await Promise.all([
client.cesium.addTileset(Cesium, widget, tiles3d).catch((e) => console.error('3D Tiles:', friendly(e))),
client.cesium.addModels(Cesium, widget, models).catch((e) => console.error('模型:', friendly(e))),
])
if (tileset) console.log(`3D Tiles 已加载:包围球半径 ${Math.round(tileset.boundingSphere.radius)} 米`)
if (placed) console.log(`模型 ${placed.models.length} 个:${placed.models.map((m) => m.name).join('、')}`)
// 漫游路线:经度、纬度、高度(米)、朝向、俯仰(度)
const STOPS = [
['金钟实景', 114.163, 22.268, 650, 0, -35],
['维港上空', 114.168, 22.284, 900, 330, -25],
['尖沙咀模型', 114.1722, 22.2914, 70, 0, -25],
['港岛全景', 114.165, 22.235, 3500, 0, -30],
]
const go = ([name, lng, lat, h, heading, pitch], duration) =>
new Promise((resolve) => {
camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lng, lat, h),
orientation: { heading: Cesium.Math.toRadians(heading), pitch: Cesium.Math.toRadians(pitch), roll: 0 },
duration,
complete: () => (console.log('到达', name), resolve()),
cancel: resolve,
})
})
// 面板:漫游按钮(依次飞过各站,每站停 1 秒)
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/2 sans-serif'
panel.innerHTML = '<button>开始漫游</button> <span style="color:#868e96">金钟 → 维港 → 尖沙咀 → 全景</span>'
container.appendChild(panel)
const button = panel.querySelector('button')
button.onclick = async () => {
button.disabled = true
for (const stop of STOPS) {
await go(stop, 3)
await new Promise((r) => setTimeout(r, 1000))
}
button.disabled = false
}
// 初始视角:第一站
await go(STOPS[0], 0)
} catch (e) {
console.error(friendly(e))
}