Replace Three.js globe with Cesium

Migrate the globe view to a Cesium viewer with Esri satellite
imagery and terrain, dropping the custom blue-marble texture and
dot-bead rendering. Add vite-plugin-cesium and a container element
for the viewer.

Also rework the binder leather and suede materials with dedicated
normal maps, and refine timeline labels and page hole sizing.
This commit is contained in:
2026-09-18 21:29:30 +08:00
parent ac12405bdf
commit 13d3ef180a
14 changed files with 1281 additions and 384 deletions
+161
View File
@@ -0,0 +1,161 @@
import * as Cesium from 'cesium';
import { ENTRIES, CATEGORY_INFO } from '../data/entries.js';
// 免费、无需 Ion 令牌的影像与地形源
const IMAGERY_URL =
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
const TERRAIN_URL =
'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer';
export class CesiumGlobe {
constructor({ onPick } = {}) {
this.onPick = onPick;
this.viewer = null;
this.markers = [];
this.selected = -1;
this._created = false;
this._visible = false;
}
ensureCreated() {
if (this._created) return;
this._created = true;
const container = document.getElementById('cesium-container');
this.viewer = new Cesium.Viewer(container, {
animation: false,
timeline: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
baseLayerPicker: false,
navigationHelpButton: false,
fullscreenButton: false,
infoBox: false,
selectionIndicator: false,
baseLayer: false,
requestRenderMode: true,
});
const viewer = this.viewer;
const scene = viewer.scene;
// 贴合暗色主题
scene.backgroundColor = Cesium.Color.fromCssColorString('#14100d');
scene.skyAtmosphere.show = false;
scene.skyBox.show = false;
scene.sun.show = false;
scene.moon.show = false;
scene.globe.enableLighting = false;
scene.globe.showGroundAtmosphere = false;
scene.globe.baseColor = Cesium.Color.fromCssColorString('#0a1626');
// 相机交互:限制滚轮缩放范围,避免过近/过远
const controller = scene.screenSpaceCameraController;
controller.enableCollisionDetection = true; // 必须开启:缩放距离限制在关闭时会被忽略
controller.minimumZoomDistance = 10000; // 最近约 10 km(区域尺度)
controller.maximumZoomDistance = 8e6; // 最远约 8000 km
// 精细化渲染:质量与性能的平衡
scene.globe.maximumScreenSpaceError = 2.0; // 默认细分,最省性能
scene.globe.tileCacheSize = 600; // 更大的瓦片缓存,减少细节刷新
scene.fxaa = false; // 关闭抗锯齿后处理,省一个全屏通道
viewer.resolutionScale = 1.5; // 1.5 倍超采样,替代 FXAA 提供更锐利的抗锯齿
scene.maximumRenderTimeChange = Infinity; // 配合 requestRenderMode:静止时停止重绘,大幅省电省帧
// 卫星影像
const imagery = new Cesium.UrlTemplateImageryProvider({
url: IMAGERY_URL,
credit: 'Imagery © Esri, Maxar, Earthstar Geographics',
maximumLevel: 19,
});
viewer.imageryLayers.addImageryProvider(imagery);
// 真实地形(Esri World Elevation 3D,免费无 token;失败则保持默认椭球)
const terrain = new Cesium.Terrain(
Cesium.ArcGISTiledElevationTerrainProvider.fromUrl(TERRAIN_URL),
);
terrain.errorEvent.addEventListener((err) => {
console.warn('[CesiumGlobe] 地形加载失败,使用椭球:', err);
});
scene.setTerrain(terrain);
viewer.clock.shouldAnimate = false;
this._buildMarkers();
this._setupPick();
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(35, 12, 7.2e6),
});
}
_buildMarkers() {
const viewer = this.viewer;
const gold = Cesium.Color.fromCssColorString('#e0c46a');
const ivory = Cesium.Color.fromCssColorString('#e8d9b4');
const ink = Cesium.Color.fromCssColorString('#14100d');
ENTRIES.forEach((entry, i) => {
const info = CATEGORY_INFO[entry.category] || CATEGORY_INFO['事'];
const color = Cesium.Color.fromCssColorString(info.color);
const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(entry.lng, entry.lat),
point: {
pixelSize: 8,
color,
outlineColor: gold,
outlineWidth: 1.5,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
label: {
text: entry.name,
font: '17px Georgia, "Noto Serif SC", "Songti SC", "Microsoft YaHei", serif',
fillColor: ivory,
outlineColor: ink,
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -16),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
show: false,
},
});
entity.entryIndex = i;
this.markers.push({ entity, index: i });
});
}
_setupPick() {
const handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
handler.setInputAction((movement) => {
const picked = this.viewer.scene.pick(movement.position);
if (Cesium.defined(picked) && picked.id && picked.id.entryIndex !== undefined) {
this.onPick?.(picked.id.entryIndex);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
this._handler = handler;
}
setSelected(index) {
this.selected = index;
if (!this.viewer) return;
this.markers.forEach(({ entity, index: i }) => {
const on = i === index;
entity.point.pixelSize = on ? 12 : 8;
entity.label.show = on;
});
this.viewer.scene.requestRender();
}
setVisible(visible) {
this._visible = visible;
if (visible && this.viewer) this.viewer.scene.requestRender();
}
resize() {
if (this.viewer) this.viewer.resize();
}
}