Skip to content

三维 ​

用 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))
}
在沙箱中打开