第六章 Cesium 实现简易河流效果

发布于:2025-09-05 ⋅ 阅读:(24) ⋅ 点赞:(0)

一、问题背景与核心需求

在Cesium 河流实现中,开发者常遇到以下问题:

  • 河流材质显示异常,预设蓝色却显示为白色
  • 河流线条生硬,缺乏自然流动感
  • 交互功能单一,无法动态调整视觉效果
  • 地图切换时河流元素兼容性差

针对这些问题,我们需要构建一个具备以下功能的解决方案:

  1. 稳定显示自定义颜色的河流材质
  2. 支持多种地图底图切换
  3. 提供河流颜色选择功能
  4. 实现河流数量统计与管理
  5. 优化加载体验与交互反馈

二、技术方案设计

2.1 整体架构设计

本方案采用三层架构设计:

  • 基础层:Cesium Viewer 初始化与地图服务配置
  • 核心层:河流材质系统与几何生成
  • 交互层:控制面板与用户反馈系统

2.2 关键技术点解析

2.2.1 Cesium Viewer 初始化优化

为解决地图加载速度与稳定性问题,我们采用以下配置:

viewer = new Cesium.Viewer('cesium-container', {
    // 默认使用OpenStreetMap
    imageryProvider: new Cesium.OpenStreetMapImageryProvider({
        url: 'https://a.tile.openstreetmap.org/'
    }),
    // 使用简单地形提升性能
    terrainProvider: new Cesium.EllipsoidTerrainProvider(),
    // 精简不必要控件
    baseLayerPicker: false,
    geocoder: false,
    homeButton: true,
    sceneModePicker: true,
    timeline: false,
    animation: false,
    navigationHelpButton: false,
    fullscreenButton: true,
    infoBox: false
});

优化点

  • 移除不必要的 UI 控件,提升加载速度
  • 使用公开的 OSM 地图服务,无需 API 密钥
  • 采用轻量级地形 provider,平衡性能与效果
2.2.2 河流材质问题修复核心代码

这是解决河流显示为白色问题的关键部分,我们采用PolylineOutlineMaterialProperty替代基础颜色材质:

// 修复后的河流材质实现
const riverMaterial = new Cesium.PolylineOutlineMaterialProperty({
    color: currentRiverColor.withAlpha(0.8),  // 主颜色,带透明度
    outlineColor: Cesium.Color.BLACK.withAlpha(0.3),  // 轮廓色,增强层次感
    outlineWidth: 1  // 轮廓宽度
});

// 河流实体配置
const river = viewer.entities.add({
    name: '河流 ' + (riverCount + 1),
    polyline: {
        positions: riverPoints,  // 河流路径点
        width: 15 + (riverCount * 2),  // 宽度,每条河流递增
        material: riverMaterial,  // 使用修复后的材质
        clampToGround: true  // 贴地显示
    },
    description: '这是模拟河流 #' + (riverCount + 1)
});

修复原理

  1. 使用带轮廓的材质替代单一颜色材质,增强视觉层次
  2. 明确设置透明度,避免颜色叠加导致的显示异常
  3. 添加黑色轮廓,使河流在不同底图上都能清晰显示
  4. 启用clampToGround确保河流贴合地形
2.2.3 多地图服务集成

实现三种常用地图服务的切换功能:

function changeMapType(type) {
    try {
        viewer.imageryLayers.removeAll();  // 清除现有图层
        
        switch (type) {
            case 'osm':
                // OpenStreetMap 矢量地图
                viewer.imageryLayers.addImageryProvider(new Cesium.OpenStreetMapImageryProvider({
                    url: 'https://a.tile.openstreetmap.org/'
                }));
                break;
            case 'arcgis':
                // ArcGIS 卫星影像
                viewer.imageryLayers.addImageryProvider(new Cesium.ArcGisMapServerImageryProvider({
                    url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
                }));
                break;
            case 'stamen':
                // Stamen 地形图
                viewer.imageryLayers.addImageryProvider(new Cesium.OpenStreetMapImageryProvider({
                    url: 'https://stamen-tiles.a.ssl.fastly.net/terrain/'
                }));
                break;
        }
    } catch (error) {
        // 错误处理
    }
}

三、完整实现代码

以下是包含所有功能的完整代码,可直接复制使用:

<!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>
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: 'Microsoft YaHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #1a1a1a;
            overflow: hidden;
        }

        #cesium-container {
            width: 100%;
            height: 100vh;
        }

        .control-panel {
            position: absolute;
            top: 20px;
            left: 20px;
            background: rgba(42, 42, 42, 0.95);
            padding: 20px;
            border-radius: 10px;
            color: white;
            z-index: 1000;
            max-width: 320px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(10px);
            border: 1px solid #4CAF50;
        }

        h2 {
            margin-top: 0;
            color: #4CAF50;
            border-bottom: 2px solid #4CAF50;
            padding-bottom: 10px;
            text-align: center;
        }

        .button-group {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }

        button {
            background: linear-gradient(to bottom, #4CAF50, #367c39);
            border: none;
            color: white;
            padding: 12px 15px;
            border-radius: 5px;
            cursor: pointer;
            flex: 1;
            font-weight: bold;
            transition: all 0.3s;
        }

        button:hover {
            background: linear-gradient(to bottom, #367c39, #2a5c2c);
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
        }

        button.secondary {
            background: linear-gradient(to bottom, #607D8B, #455A64);
        }

        button.secondary:hover {
            background: linear-gradient(to bottom, #455A64, #37474F);
        }

        .info {
            background: rgba(30, 30, 30, 0.8);
            padding: 15px;
            border-radius: 8px;
            margin-top: 15px;
            font-size: 14px;
            line-height: 1.5;
        }

        .river-info {
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
            padding: 10px;
            background: rgba(50, 50, 50, 0.7);
            border-radius: 5px;
            font-weight: bold;
        }

        .logo {
            text-align: center;
            margin-bottom: 15px;
            font-size: 24px;
            font-weight: bold;
            color: #4CAF50;
        }

        .loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 20px;
            z-index: 1000;
            background: rgba(0, 0, 0, 0.8);
            padding: 20px 30px;
            border-radius: 10px;
            text-align: center;
        }

        .map-type-selector {
            margin-top: 15px;
        }

        select {
            width: 100%;
            padding: 10px;
            border-radius: 5px;
            background: #2d2d2d;
            color: white;
            border: 1px solid #4CAF50;
            margin-top: 5px;
        }

        .status {
            margin-top: 10px;
            padding: 8px;
            background: rgba(50, 50, 50, 0.7);
            border-radius: 5px;
            font-size: 12px;
            text-align: center;
        }

        .color-picker {
            margin-top: 10px;
        }

        .color-option {
            display: inline-block;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            margin: 5px;
            cursor: pointer;
            border: 2px solid transparent;
        }

        .color-option.active {
            border-color: white;
        }
    </style>
</head>

<body>
    <div id="cesium-container"></div>
    <div id="loading" class="loading">正在加载地图和河流效果...</div>

    <div class="control-panel">
        <div class="logo">🌊 Cesium河流模拟</div>
        <h2>河流效果控制面板</h2>

        <div class="info">
            <p>已修复材质问题,河流现在应该显示为蓝色而不是白色。</p>
        </div>

        <div class="map-type-selector">
            <label>选择地图类型:</label>
            <select id="map-type">
                <option value="osm">OpenStreetMap (默认)</option>
                <option value="arcgis">ArcGIS 卫星影像</option>
                <option value="stamen">Stamen 地形图</option>
            </select>
        </div>

        <div class="color-picker">
            <label>河流颜色:</label>
            <div>
                <span class="color-option active" style="background:#1E90FF;"
                    onclick="changeRiverColor('#1E90FF')"></span>
                <span class="color-option" style="background:#0080FF;" onclick="changeRiverColor('#0080FF')"></span>
                <span class="color-option" style="background:#00BFFF;" onclick="changeRiverColor('#00BFFF')"></span>
                <span class="color-option" style="background:#4682B4;" onclick="changeRiverColor('#4682B4')"></span>
                <span class="color-option" style="background:#5F9EA0;" onclick="changeRiverColor('#5F9EA0')"></span>
            </div>
        </div>

        <div class="button-group">
            <button onclick="addRiverEffect()">添加河流</button>
            <button onclick="clearAllRivers()" class="secondary">清除河流</button>
        </div>

        <div class="river-info">
            <span>河流数量:</span>
            <span id="river-count">0</span>
        </div>

        <div class="status" id="status">
            系统状态: 正常
        </div>

        <div class="info">
            <p><strong>操作提示:</strong> 使用鼠标右键拖动调整视角,滚轮缩放。</p>
        </div>
    </div>

    <script>
        // 初始化Cesium Viewer - 使用无需令牌的公开地图服务
        let viewer;
        let currentRiverColor = Cesium.Color.fromCssColorString('#1E90FF');

        try {
            viewer = new Cesium.Viewer('cesium-container', {
                // 默认使用OpenStreetMap
                imageryProvider: new Cesium.OpenStreetMapImageryProvider({
                    url: 'https://a.tile.openstreetmap.org/'
                }),
                // 使用简单地形
                terrainProvider: new Cesium.EllipsoidTerrainProvider(),
                baseLayerPicker: false,
                geocoder: false,
                homeButton: true,
                sceneModePicker: true,
                timeline: false,
                animation: false,
                navigationHelpButton: false,
                fullscreenButton: true,
                infoBox: false
            });

            // 设置初始视角 - 中国北京附近
            viewer.camera.setView({
                destination: Cesium.Cartesian3.fromDegrees(116.3, 39.85, 8000),
                orientation: {
                    heading: 0,
                    pitch: -0.8,
                    roll: 0
                }
            });

            // 监听地图类型变化
            document.getElementById('map-type').addEventListener('change', function () {
                changeMapType(this.value);
            });

        } catch (error) {
            document.getElementById('status').textContent = '系统状态: 错误 - ' + error.message;
            document.getElementById('status').style.background = 'rgba(200, 50, 50, 0.7)';
        }

        // 河流计数器
        let riverCount = 0;

        // 更改地图类型
        function changeMapType(type) {
            try {
                viewer.imageryLayers.removeAll();

                switch (type) {
                    case 'osm':
                        viewer.imageryLayers.addImageryProvider(new Cesium.OpenStreetMapImageryProvider({
                            url: 'https://a.tile.openstreetmap.org/'
                        }));
                        showMessage('已切换到OpenStreetMap');
                        break;
                    case 'arcgis':
                        viewer.imageryLayers.addImageryProvider(new Cesium.ArcGisMapServerImageryProvider({
                            url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
                        }));
                        showMessage('已切换到ArcGIS卫星影像');
                        break;
                    case 'stamen':
                        viewer.imageryLayers.addImageryProvider(new Cesium.OpenStreetMapImageryProvider({
                            url: 'https://stamen-tiles.a.ssl.fastly.net/terrain/'
                        }));
                        showMessage('已切换到Stamen地形图');
                        break;
                }

                document.getElementById('status').textContent = '系统状态: 已切换地图类型';
            } catch (error) {
                document.getElementById('status').textContent = '系统状态: 切换地图失败';
            }
        }

        // 更改河流颜色
        function changeRiverColor(colorHex) {
            currentRiverColor = Cesium.Color.fromCssColorString(colorHex);

            // 更新所有活动颜色选择器
            document.querySelectorAll('.color-option').forEach(option => {
                option.classList.remove('active');
            });
            event.target.classList.add('active');

            showMessage('已选择新颜色: ' + colorHex);
        }

        // 显示消息
        function showMessage(message) {
            const entity = viewer.entities.add({
                position: Cesium.Cartesian3.fromDegrees(116.3, 40.0, 0),
                label: {
                    text: message,
                    font: '16px Microsoft YaHei',
                    pixelOffset: new Cesium.Cartesian2(0, 50),
                    fillColor: Cesium.Color.GREEN,
                    outlineColor: Cesium.Color.BLACK,
                    outlineWidth: 2,
                    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    showBackground: true,
                    backgroundColor: new Cesium.Color(0.2, 0.2, 0.2, 0.7)
                }
            });

            // 3秒后移除消息
            setTimeout(() => {
                viewer.entities.remove(entity);
            }, 3000);
        }

        // 更新河流计数
        function updateRiverCount() {
            document.getElementById('river-count').textContent = riverCount;
        }

        // 清除所有河流
        function clearAllRivers() {
            viewer.entities.removeAll();
            riverCount = 0;
            updateRiverCount();
            showMessage('所有河流已清除');
            document.getElementById('status').textContent = '系统状态: 已清除所有河流';
        }

        // 河流效果函数 - 修复材质问题
        function addRiverEffect() {
            try {
                // 河流坐标点 - 更自然的曲线
                const riverPoints = [
                    Cesium.Cartesian3.fromDegrees(116.2, 39.9, 0),
                    Cesium.Cartesian3.fromDegrees(116.22, 39.89, 0),
                    Cesium.Cartesian3.fromDegrees(116.25, 39.88, 0),
                    Cesium.Cartesian3.fromDegrees(116.28, 39.87, 0),
                    Cesium.Cartesian3.fromDegrees(116.3, 39.85, 0),
                    Cesium.Cartesian3.fromDegrees(116.32, 39.84, 0),
                    Cesium.Cartesian3.fromDegrees(116.35, 39.82, 0),
                    Cesium.Cartesian3.fromDegrees(116.38, 39.81, 0),
                    Cesium.Cartesian3.fromDegrees(116.4, 39.79, 0),
                    Cesium.Cartesian3.fromDegrees(116.43, 39.78, 0),
                    Cesium.Cartesian3.fromDegrees(116.45, 39.76, 0),
                    Cesium.Cartesian3.fromDegrees(116.48, 39.75, 0),
                    Cesium.Cartesian3.fromDegrees(116.5, 39.73, 0),
                    Cesium.Cartesian3.fromDegrees(116.53, 39.72, 0),
                    Cesium.Cartesian3.fromDegrees(116.55, 39.7, 0),
                ];

                // 方法1:使用简单的颜色材质(确保颜色正确)
                const riverMaterial = new Cesium.ColorMaterialProperty(currentRiverColor.withAlpha(0.7));

                // 方法2:使用PolylineOutlineMaterialProperty获得更好的视觉效果
                // const riverMaterial = new Cesium.PolylineOutlineMaterialProperty({
                //     color: currentRiverColor.withAlpha(0.8),
                //     outlineColor: Cesium.Color.BLACK.withAlpha(0.3),
                //     outlineWidth: 1
                // });

                // 添加河流实体
                const river = viewer.entities.add({
                    name: '河流 ' + (riverCount + 1),
                    polyline: {
                        positions: riverPoints,
                        width: 15 + (riverCount * 2),
                        material: riverMaterial,
                        clampToGround: true
                    },
                    description: '这是模拟河流 #' + (riverCount + 1)
                });

                riverCount++;
                updateRiverCount();

                // 定位到河流
                viewer.zoomTo(river);

                showMessage('河流 ' + riverCount + ' 已添加');
                document.getElementById('status').textContent = '系统状态: 已添加河流 ' + riverCount;

            } catch (error) {
                document.getElementById('status').textContent = '系统状态: 添加河流失败 - ' + error.message;
                document.getElementById('status').style.background = 'rgba(200, 50, 50, 0.7)';
            }
        }

        // 初始更新计数
        updateRiverCount();

        // 添加初始河流
        setTimeout(() => {
            addRiverEffect();
            document.getElementById('loading').style.display = 'none';
            document.getElementById('status').textContent = '系统状态: 初始化完成';
        }, 1500);
    </script>
</body>

</html>

网站公告

今日签到

点亮在社区的每一天
去签到