明天的明天 永远的永远 未知的一切 我与你一起承担 ??

是非成败转头空 青山依旧在 几度夕阳红 。。。
  博客园  :: 首页  :: 管理

Cesium 加载 3dtiles ,实现球体旋转、模型定位

Posted on 2026-04-20 10:08  且行且思  阅读(8)  评论(0)    收藏  举报
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cesium - 模型</title>
    <link href="libs/Widgets/widgets.css" rel="stylesheet">
    <script src="libs/Cesium.js"></script>
    <style>
        html,
        body,
        #cesiumContainer {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        #statusMsg {
            position: absolute;
            bottom: 20px;
            left: 20px;
            background: rgba(0, 0, 0, 0.6);
            color: white;
            padding: 8px 16px;
            border-radius: 4px;
            font-family: sans-serif;
            font-size: 14px;
            z-index: 100;
            pointer-events: none;
        }

        /* 气泡标注样式 - 简洁蓝色 */
        #bubble {
            position: absolute;
            background: linear-gradient(135deg, #0096ff 0%, #0066cc 100%);
            color: white;
            padding: 6px 14px;
            border-radius: 6px;
            font-family: 'Microsoft YaHei', sans-serif;
            font-size: 13px;
            font-weight: bold;
            z-index: 0;
            pointer-events: none;
            border: 2px solid white;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
            display: none;
            white-space: nowrap;
        }

        /* 气泡箭头 - 匹配气泡底部颜色,向上移动覆盖边框 */
        #bubble::after {
            content: '';
            position: absolute;
            bottom: -8px;
            left: 50%;
            transform: translateX(-50%);
            width: 0;
            height: 0;
            border-left: 10px solid transparent;
            border-right: 10px solid transparent;
            border-top: 10px solid #0088dd;
        }

        /* 隐藏 Cesium 水印 */
        .cesium-viewer-bottom {
            display: none !important;
        }
    </style>
</head>

<body>
    <div id="cesiumContainer"></div>
    <div id="statusMsg">🌍 正在加载地球影像...</div>
    <div id="bubble">现场编号:H29</div>

    <script>
        (async function () {
            //官网:https://ion.cesium.com/tokens?page=1   , 自行注册
            Cesium.Ion.defaultAccessToken = 'xxxxxxxxxxxxxxxxxxxxx';

            // ========== 修复点 1:在 Viewer 初始化时配置星空 ==========
            const viewer = new Cesium.Viewer('cesiumContainer', {
                infoBox: false,
                selectionIndicator: false,
                animation: false,
                timeline: false,
                geocoder: false,
                homeButton: true,
                sceneModePicker: false,
                baseLayerPicker: false,
                navigationHelpButton: false,
                // 在这里直接配置场景初始状态
                sceneOptions: {
                    skyBox: true, // 开启星空
                    skyAtmosphere: false // 关闭大气层
                }
            });

             viewer.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(105, 35, 40000000 ),
                duration: 0  // 立即跳转,不飞行
            });

            // ========== 修复点 2:地球显示配置 ==========
            viewer.scene.globe.show = true;
            viewer.scene.globe.enableLighting = false;
            // 初始先不设为透明,或者设为深灰色以便在星空中看到轮廓
            viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString('#1a1a2e').withAlpha(0.5);

           
            try {
                const terrain = await Cesium.createWorldTerrainAsync();
                viewer.terrainProvider = terrain;
            } catch (error) {
                viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
            }

            // =============================================
            // ✅ 已修改:相机高度翻倍,地球缩小一半
            // =============================================
            const cameraHeight = 40000000; // 原30000000 → 现60000000
            viewer.scene.screenSpaceCameraController.enableRotate = false;
            viewer.scene.screenSpaceCameraController.enableZoom = false;
            viewer.scene.screenSpaceCameraController.enableTranslate = false;

            // 统一计算半径:地球半径 + 相机高度(和旋转函数完全一致)
            const radius = Cesium.Ellipsoid.WGS84.maximumRadius + cameraHeight;
            viewer.camera.setView({
                // 用统一的半径计算位置
                destination: new Cesium.Cartesian3(
                    radius * Math.cos(Cesium.Math.toRadians(30)) * Math.cos(Cesium.Math.toRadians(0)),
                    radius * Math.cos(Cesium.Math.toRadians(30)) * Math.sin(Cesium.Math.toRadians(0)),
                    radius * Math.sin(Cesium.Math.toRadians(30))
                ),
                orientation: {
                    heading: 0.0,
                    pitch: -Cesium.Math.toRadians(45),
                    roll: 0.0
                }
            });

            // ========== 修复点 3:恢复启动逻辑,但不把地球变白 ==========
            setTimeout(() => {
                // 保留这行是为了让流程继续走,只是不修改 baseColor
                document.getElementById('statusMsg').innerHTML = '🌍 地球旋转中...';
                setTimeout(() => requestAnimationFrame(rotateCamera), 0);
            }, 1000); // 稍微延迟一点,确保影像先加载出来

            let rotationStop = false;
            let startTime = null;
            const ROTATION_DURATION = 3000;

            function rotateCamera() {
                if (rotationStop) return;
                if (startTime === null) startTime = performance.now();

                const now = performance.now();
                const elapsed = now - startTime;

                if (elapsed >= ROTATION_DURATION) {
                    rotationStop = true;
                    viewer.scene.screenSpaceCameraController.enableRotate = true;
                    viewer.scene.screenSpaceCameraController.enableZoom = true;
                    viewer.scene.screenSpaceCameraController.enableTranslate = true;
                    document.getElementById('statusMsg').innerHTML = '✈️ 正在加载模型...';
                    setTimeout(() => loadTileset(), 500);
                    return;
                }

                const progress = elapsed / ROTATION_DURATION;
                const longitude = progress * 360;
                const latitude = 30;
                const radians = Cesium.Math.toRadians(longitude);
                const latRadians = Cesium.Math.toRadians(latitude);
                const radius = Cesium.Ellipsoid.WGS84.maximumRadius + cameraHeight;
                const cosLat = Math.cos(latRadians);

                const x = radius * cosLat * Math.cos(radians);
                const y = radius * cosLat * Math.sin(radians);
                const z = radius * Math.sin(latRadians);

                viewer.camera.position = new Cesium.Cartesian3(x, y, z);
                const center = Cesium.Cartesian3.ZERO;
                const direction = Cesium.Cartesian3.subtract(center, viewer.camera.position, new Cesium.Cartesian3());
                Cesium.Cartesian3.normalize(direction, direction);
                viewer.camera.direction = direction;

                const north = Cesium.Cartesian3.fromDegrees(longitude, 90, 0);
                const up = Cesium.Cartesian3.subtract(north, viewer.camera.position, new Cesium.Cartesian3());
                Cesium.Cartesian3.normalize(up, up);
                viewer.camera.up = up;

                viewer.camera.right = Cesium.Cartesian3.cross(viewer.camera.direction, viewer.camera.up, new Cesium.Cartesian3());
                Cesium.Cartesian3.normalize(viewer.camera.right, viewer.camera.right);

                requestAnimationFrame(rotateCamera);
            }

             try {
                const labelLayer = viewer.imageryLayers.addImageryProvider(
                    new Cesium.UrlTemplateImageryProvider({
                        url: 'https://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8',
                        credit: '高德地图'
                    })
                );
                //labelLayer.alpha = 0.85;
            } catch (error) {
                console.warn('标注图层加载失败:', error);
            }

            const tilesetUrl = 'http://localhost:8055/3dtiles/tileset.json';
            let tileset = null;

            // 创建圆形canvas作为billboard图像
            function createCircleCanvas(size, color) {
                const canvas = document.createElement('canvas');
                canvas.width = size;
                canvas.height = size;
                const ctx = canvas.getContext('2d');

                ctx.beginPath();
                ctx.arc(size / 2, size / 2, size / 2 - 5, 0, 2 * Math.PI);
                ctx.fillStyle = color.toCssColorString();
                ctx.fill();

                ctx.strokeStyle = 'white';
                ctx.lineWidth = 5;
                ctx.stroke();

                return canvas;
            }

            // 更新气泡位置(固定大小,距离太远时隐藏)
            let bubblePosition = null;
            let bubbleUpdateListener = null;

            function updateBubblePosition(lon, lat, height) {
                bubblePosition = { lon, lat, height };
                const bubble = document.getElementById('bubble');

                // 移除旧的监听器
                if (bubbleUpdateListener) {
                    viewer.scene.postRender.removeEventListener(bubbleUpdateListener);
                }

                // 添加新的监听器
                bubbleUpdateListener = function () {
                    if (!bubblePosition) return;

                    // 获取相机高度,用于判断是否隐藏气泡
                    const cameraHeight = viewer.camera.positionCartographic.height;

                    // 当相机高度超过800米时隐藏气泡
                    if (cameraHeight > 800) {
                        bubble.style.display = 'none';
                        return;
                    }

                    const position = Cesium.Cartesian3.fromDegrees(
                        bubblePosition.lon,
                        bubblePosition.lat,
                        bubblePosition.height
                    );

                    const canvasPosition = viewer.scene.cartesianToCanvasCoordinates(position);

                    if (canvasPosition) {
                        bubble.style.display = 'block';
                        bubble.style.left = (canvasPosition.x - bubble.offsetWidth / 2) + 'px';
                        bubble.style.top = (canvasPosition.y - bubble.offsetHeight - 30) + 'px';
                    } else {
                        bubble.style.display = 'none';
                    }
                };

                viewer.scene.postRender.addEventListener(bubbleUpdateListener);
                console.log('气泡位置已设置:', lon, lat, height, '(相机高度>3000米时自动隐藏)');
            }

            // 创建带箭头的气泡canvas
            function createBubbleCanvas(text) {
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');

                // 设置字体以测量文本宽度
                ctx.font = 'bold 18px sans-serif';
                const textWidth = ctx.measureText(text).width;

                // 气泡尺寸
                const padding = 20;
                const bubbleWidth = textWidth + padding * 2;
                const bubbleHeight = 40;
                const arrowHeight = 15;
                const arrowWidth = 20;
                const borderRadius = 8;

                canvas.width = bubbleWidth + 10;
                canvas.height = bubbleHeight + arrowHeight + 5;

                // 重新设置字体(canvas大小改变后需要重设)
                ctx.font = 'bold 18px sans-serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';

                // 绘制气泡主体(圆角矩形)
                const x = 5;
                const y = 5;

                ctx.beginPath();
                ctx.moveTo(x + borderRadius, y);
                ctx.lineTo(x + bubbleWidth - borderRadius, y);
                ctx.quadraticCurveTo(x + bubbleWidth, y, x + bubbleWidth, y + borderRadius);
                ctx.lineTo(x + bubbleWidth, y + bubbleHeight - borderRadius);
                ctx.quadraticCurveTo(x + bubbleWidth, y + bubbleHeight, x + bubbleWidth - borderRadius, y + bubbleHeight);

                // 绘制箭头(指向下方)
                ctx.lineTo(x + bubbleWidth / 2 + arrowWidth / 2, y + bubbleHeight);
                ctx.lineTo(x + bubbleWidth / 2, y + bubbleHeight + arrowHeight);
                ctx.lineTo(x + bubbleWidth / 2 - arrowWidth / 2, y + bubbleHeight);

                ctx.lineTo(x + borderRadius, y + bubbleHeight);
                ctx.quadraticCurveTo(x, y + bubbleHeight, x, y + bubbleHeight - borderRadius);
                ctx.lineTo(x, y + borderRadius);
                ctx.quadraticCurveTo(x, y, x + borderRadius, y);
                ctx.closePath();

                // 填充气泡背景
                ctx.fillStyle = 'rgba(0, 150, 255, 0.95)';
                ctx.fill();

                // 绘制边框
                ctx.strokeStyle = 'white';
                ctx.lineWidth = 3;
                ctx.stroke();

                // 绘制文字
                ctx.fillStyle = 'white';
                ctx.fillText(text, x + bubbleWidth / 2, y + bubbleHeight / 2);

                return canvas;
            }

            // 使用Primitive绘制多边形 - 测试不同高度
            function drawPolygonPrimitive(targetLon, targetLat) {
                console.log('===== 使用Primitive绘制多边形 =====');

                // 清除之前的entities
                viewer.entities.removeAll();
                console.log('已清除所有entities');

                // 高度-4.4米(贴合模型表面)
                const testHeight = -4.4;
                console.log('设置高度:', testHeight, '米(相对于WGS84椭球面)');

                // 使用Primitive绘制多边形面
                const instance = new Cesium.GeometryInstance({
                    id: 'polygon-' + Date.now(), // 添加唯一ID
                    geometry: new Cesium.PolygonGeometry({
                        polygonHierarchy: new Cesium.PolygonHierarchy(
                            Cesium.Cartesian3.fromDegreesArray([
                                108.64400909549586, 19.10124543090048,
                                108.64446593935193, 19.10121679104952,
                                108.6444605616105, 19.101139271693693,
                                108.64400371796542, 19.101167911525337
                            ])
                        ),
                        height: testHeight,
                        vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
                    }),
                    attributes: {
                        color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                            Cesium.Color.YELLOW.withAlpha(0.8)
                        )
                    }
                });

                const primitive = viewer.scene.primitives.add(new Cesium.Primitive({
                    geometryInstances: instance,
                    appearance: new Cesium.PerInstanceColorAppearance({
                        translucent: true,
                        closed: false
                    }),
                    asynchronous: false // 同步创建,立即生效
                }));

                console.log('多边形Primitive已创建,高度=' + testHeight + '米:', primitive);

                // 添加边框线
                const outlineInstance = new Cesium.GeometryInstance({
                    id: 'outline-' + Date.now(),
                    geometry: new Cesium.PolygonOutlineGeometry({
                        polygonHierarchy: new Cesium.PolygonHierarchy(
                            Cesium.Cartesian3.fromDegreesArray([
                                108.64400909549586, 19.10124543090048,
                                108.64446593935193, 19.10121679104952,
                                108.6444605616105, 19.101139271693693,
                                108.64400371796542, 19.101167911525337
                            ])
                        ),
                        height: testHeight
                    }),
                    attributes: {
                        color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
                    }
                });

                const outlinePrimitive = viewer.scene.primitives.add(new Cesium.Primitive({
                    geometryInstances: outlineInstance,
                    appearance: new Cesium.PerInstanceColorAppearance({
                        flat: true,
                        renderState: {
                            lineWidth: Math.min(10, viewer.scene.maximumAliasedLineWidth)
                        }
                    }),
                    asynchronous: false
                }));

                console.log('边框Primitive已创建');

                // 使用HTML div显示气泡,调整高度并支持缩放
                updateBubblePosition(targetLon, targetLat, testHeight);

                console.log('当前entities数量:', viewer.entities.values.length);
                console.log('===== 绘制完成,高度=' + testHeight + '米,气泡高度=' + bubbleHeight + '米 =====');
            }

            async function loadTileset() {
                try {
                    tileset = await Cesium.Cesium3DTileset.fromUrl(tilesetUrl);
                    tileset.show = false;
                    viewer.scene.primitives.add(tileset);
                    await tileset.readyPromise;
                    tileset.show = true;
                    locateToModel();
                } catch (error) {
                    console.error('加载失败:', error);
                    document.getElementById('statusMsg').innerHTML = '❌ 模型加载失败';
                }
            }

            async function locateToModel() {
                if (!tileset) return;
                try {
                    tileset.show = true;

                    viewer.scene.screenSpaceCameraController.enableRotate = true;
                    viewer.scene.screenSpaceCameraController.enableZoom = true;
                    viewer.scene.screenSpaceCameraController.enableTranslate = true;
                    viewer.scene.screenSpaceCameraController.enableTilt = true;
                    viewer.scene.screenSpaceCameraController.enableLook = true;
                    viewer.scene.screenSpaceCameraController.inertiaSpin = 0.95;
                    viewer.scene.screenSpaceCameraController.inertiaTranslate = 0.95;
                    viewer.scene.screenSpaceCameraController.inertiaZoom = 0.9;
                    viewer.scene.screenSpaceCameraController.minimumZoomDistance = 10;
                    viewer.scene.screenSpaceCameraController.maximumZoomDistance = 50000000;
                    viewer.scene.screenSpaceCameraController.enableCollisionDetection = false;

                    document.getElementById('statusMsg').innerHTML = '✈️ 正在飞向模型...';

                    // 第一段:飞到模型整体俯视(4秒)
                    await viewer.flyTo(tileset, {
                        duration: 4,
                        offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-50), tileset.boundingSphere.radius * 3.5),
                        easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
                    });

                    document.getElementById('statusMsg').innerHTML = '👁️ 查看整体模型...';
                    await new Promise(resolve => setTimeout(resolve, 1000));

                    // 目标点位(多边形中心)
                    const targetLon = 108.64423515;
                    const targetLat = 19.10119236;

                    document.getElementById('statusMsg').innerHTML = '🔍 正在放大到目标点位...';

                    // 第二段:飞到点位1000米(3秒)
                    await viewer.camera.flyTo({
                        destination: Cesium.Cartesian3.fromDegrees(targetLon, targetLat, 1000),
                        orientation: {
                            heading: 0,
                            pitch: Cesium.Math.toRadians(-50),
                            roll: 0
                        },
                        duration: 3,
                        easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
                    });

                    await new Promise(resolve => setTimeout(resolve, 300));

                    // 第三段:降到200米(2秒)
                    await viewer.camera.flyTo({
                        destination: Cesium.Cartesian3.fromDegrees(targetLon, targetLat, 200),
                        orientation: {
                            heading: 0,
                            pitch: Cesium.Math.toRadians(-30),
                            roll: 0
                        },
                        duration: 2,
                        easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
                    });

                    document.getElementById('statusMsg').innerHTML = '🛬 正在降落到点位...';

                    // 第四段:降落到点位上方俯视(2秒)
                    let viewHeight = 150;
                    try {
                        const positions = [Cesium.Cartographic.fromDegrees(targetLon, targetLat)];
                        const updatedPositions = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions);
                        const terrainHeight = updatedPositions[0].height || 0;
                        viewHeight = terrainHeight + 150; // 地形高度 + 150米,避免太放大
                    } catch (e) {
                        console.warn('获取地形高度失败', e);
                    }

                    await viewer.camera.flyTo({
                        destination: Cesium.Cartesian3.fromDegrees(targetLon, targetLat, viewHeight),
                        orientation: {
                            heading: 0,
                            pitch: Cesium.Math.toRadians(-90),
                            roll: 0
                        },
                        duration: 2,
                        easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT
                    });

                    console.log('飞行完成,开始绘制多边形');
                    document.getElementById('statusMsg').innerHTML = '🎨 正在绘制标注...';

                    // 飞行完成后,使用Primitive绘制多边形
                    try {
                        drawPolygonPrimitive(targetLon, targetLat);
                        console.log('多边形绘制完成');
                    } catch (e) {
                        console.error('绘制多边形失败:', e);
                    }

                    document.getElementById('statusMsg').innerHTML = '✅ 模型已加载,可交互操作';
                    setTimeout(() => {
                        document.getElementById('statusMsg').style.opacity = '0';
                    }, 3000);

                    setupSmartZoom();
                } catch (err) {
                    console.error('flyTo 失败:', err);

                }
            }

            function setupSmartZoom() {
                const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
                handler.setInputAction(function (wheelEvent) {
                    const cameraHeight = viewer.camera.positionCartographic.height;
                    if (cameraHeight > 100000) {
                        const zoomAmount = wheelEvent > 0 ? 0.9 : 1.1;
                        const position = viewer.camera.position.clone();
                        const cartographic = Cesium.Cartographic.fromCartesian(position);
                        const newHeight = cartographic.height * zoomAmount;
                        const clampedHeight = Math.max(10, Math.min(newHeight, 50000000));
                        const newPosition = Cesium.Cartesian3.fromRadians(
                            cartographic.longitude,
                            cartographic.latitude,
                            clampedHeight
                        );
                        viewer.camera.position = newPosition;
                        return false;
                    }
                    return true;
                }, Cesium.ScreenSpaceEventType.WHEEL);
            }
        })();
    </script>
</body>

</html>

图片