Cesium中的倒立四棱锥:从几何结构到交互式3D可视化

在数字地球和地理空间可视化领域,Cesium凭借其强大的3D地球渲染能力,已成为开发者构建沉浸式地理空间应用的首选工具。本文将深入探讨一个创新的Cesium应用实例------倒立四棱锥场景,通过剖析其几何结构、自定义渲染和交互设计,揭示如何将基础几何体转化为富有表现力的3D可视化元素。

一、Cesium与3D可视化:从基础到创新

Cesium是一个开源的JavaScript库,专注于创建高性能、跨平台的3D地球和地图应用。它利用WebGL技术实现硬件加速的3D渲染,支持高精度地形、卫星影像和矢量数据的可视化。本案例中,Cesium不仅展示了其基础功能,更通过自定义几何体和动画效果,探索了3D可视化的新边界。

二、倒立四棱锥的几何结构:解构与重构

传统四棱锥(金字塔)的几何结构通常为底面正方形,顶部尖点。本案例的"倒立四棱锥"则采用创新的几何设计:

javascript 复制代码
// 定义5个顶点:4个底部顶点 + 1个顶部顶点
var positions = new Float64Array(5 * 3);

// 底部顶点 (z = 0) - 平的底部
positions[0] = 1.0; positions[1] = 1.0; positions[2] = 0.0; // 前右
positions[3] = -1.0; positions[4] = 1.0; positions[5] = 0.0; // 前左
positions[6] = -1.0; positions[7] = -1.0; positions[8] = 0.0; // 后左
positions[9] = 1.0; positions[10] = -1.0; positions[11] = 0.0; // 后右
positions[12] = 0.0; positions[13] = 0.0; positions[14] = 2.0; // 顶部顶点

关键创新在于倒立设计 :通过computeModelMatrix函数中的180度X轴旋转实现:

javascript 复制代码
// 创建绕X轴旋转180度的矩阵(使四棱锥倒立)
let rotationX = Cesium.Matrix4.fromRotationTranslation(
    Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(180))
);

这种几何结构使四棱锥的"尖"朝下,"底"朝上,创造出视觉上独特的倒置效果,为3D场景增添了艺术感和科技感。

三、自定义几何体与着色器:Cesium高级渲染技术

Cesium的高级可视化能力很大程度上依赖于自定义几何体和着色器。本案例中,我们实现了两个关键组件:

1. 面部渲染(Face Rendering)

javascript 复制代码
function createFaceVertexShader() {
    var vertexShader = `
        attribute vec3 position;
        attribute vec3 normal;
        attribute vec2 st;
        varying vec3 v_positionEC;
        varying vec3 v_normalEC;
        varying vec2 v_st;
        void main() {
            v_positionEC = (czm_modelView * vec4(position, 1.0)).xyz;
            v_normalEC = czm_normal * normal;
            v_st = st;
            gl_Position = czm_modelViewProjection * vec4(position, 1.0);
        }
    `;
    return vertexShader;
}

面部渲染使用Phong光照模型,确保棱角分明:

javascript 复制代码
material.alpha = color.a;
material.diffuse = color.rgb;
material.specular = 0.5;
material.shininess = 10.0;
gl_FragColor = czm_phong(normalize(positionToEyeEC), material, czm_lightDirectionEC);

2. 边线渲染(Edge Rendering)

为增强四棱锥的立体感,添加了黑色边线:

javascript 复制代码
function createEdgeVertexShader() {
    var vertexShader = `
        attribute vec3 position;
        void main() {
            gl_Position = czm_modelViewProjection * vec4(position, 1.0);
        }
    `;
    return vertexShader;
}

边线渲染禁用背面剔除(rawRenderState.cull.enabled = false),确保所有边线可见,并设置线宽为2.0:

javascript 复制代码
rawRenderState.lineWidth = 2.0;

四、动画效果:动态视觉表达

本案例的动画效果通过startAnimate方法实现,利用了Cesium的帧渲染机制:

javascript 复制代码
TetrahedronPrimitive.prototype.startAnimate = function () {
    let that = this;
    this._setInterval = setInterval(animateFunc, 5);

    function animateFunc() {
        that._angle = that._angle + 0.01 * that._speed;
        if (Math.sin(that._angle) < 0) {
            that._height = 0.01;
        } else {
            that._height = -0.01;
        }

        let translation = new Cesium.Cartesian3(0, 0, that._height);
        Cesium.Matrix4.multiplyByTranslation(that._modelMatrix, translation, that._modelMatrix);
        let rotationZ = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(that._speed)));
        Cesium.Matrix4.multiply(that._modelMatrix, rotationZ, that._modelMatrix);
    }
};

动画包含两个关键运动:

  1. 垂直摆动 :通过_height变量实现上下摆动
  2. 水平旋转:通过Z轴旋转实现缓慢旋转

这种组合运动创造出优雅的"摇摆"效果,使四棱锥在场景中更具生命力。

五、交互式控制:用户友好设计

本案例的UI设计体现了"用户中心"理念,通过精心设计的控制面板实现:

1. 控制面板设计

控制面板采用现代UI设计语言:

  • 半透明玻璃效果(backdrop-filter: blur(10px)
  • 精心设计的渐变按钮(linear-gradient
  • 平滑的动画过渡(transition: all 0.3s ease

2. 核心控制功能

控制项 功能描述 技术实现
添加/移除四棱锥 动态创建和销毁几何体 viewer.scene.primitives.add/remove
颜色选择 实时更改四棱锥颜色 Cesium.Color.fromCssColorString
动画速度 调整动画节奏 更新_speed属性并同步到所有四棱锥
大小调整 控制四棱锥尺寸 更新_scale属性
随机位置 控制四棱锥生成位置 通过useRandomPosition标志切换生成逻辑

3. 位置生成算法

javascript 复制代码
if (this.useRandomPosition) {
    const latOffset = (Math.random() - 0.5) * 0.05;
    const lonOffset = (Math.random() - 0.5) * 0.05;
    const height = Math.random() * 200 + 50;
    position = Cesium.Cartesian3.fromDegrees(
        116.39 + lonOffset,
        39.9 + latOffset,
        height
    );
} else {
    const offset = this.primitives.length * 0.01;
    position = Cesium.Cartesian3.fromDegrees(
        116.39 + offset,
        39.9,
        100
    );
}

这种位置生成逻辑提供了两种模式:随机位置(更自然的分布)和线性排列(便于观察)。

六、架构设计:模块化与可维护性

本案例采用了清晰的架构设计,确保代码的可维护性和扩展性:

  1. TetrahedronManager:管理所有四棱锥实例,提供统一的接口
  2. TetrahedronPrimitive:自定义几何体类,封装渲染和动画逻辑
  3. 控制面板:与管理器交互,更新状态

这种分层设计使代码结构清晰,便于后续扩展。例如,添加新的几何体类型只需实现新的Primitive类,而无需修改主控制逻辑。

七、代码全景

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cesium 倒立四棱锥场景</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body,
        html {
            width: 100%;
            height: 100%;
            overflow: hidden;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #000;
        }

        #cesiumContainer {
            width: 100%;
            height: 100%;
        }

        .cesium-widget-credits,
        .cesium-viewer-toolbar {
            display: none !important;
        }

        .control-panel {
            position: fixed;
            top: 20px;
            left: 20px;
            background: rgba(25, 25, 35, 0.9);
            border-radius: 12px;
            padding: 18px;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.6);
            color: white;
            min-width: 280px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            z-index: 1000;
            transition: all 0.3s ease;
        }

        .panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 18px;
            padding-bottom: 12px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
        }

        .panel-title {
            font-size: 1.3rem;
            font-weight: 600;
            color: #6aabf2;
        }

        .panel-toggle {
            background: rgba(255, 255, 255, 0.1);
            border: none;
            color: white;
            font-size: 1.2rem;
            cursor: pointer;
            padding: 6px 10px;
            border-radius: 6px;
            transition: background 0.3s;
        }

        .panel-toggle:hover {
            background: rgba(255, 255, 255, 0.2);
        }

        .control-group {
            margin-bottom: 18px;
        }

        .control-label {
            display: block;
            margin-bottom: 10px;
            font-size: 0.95rem;
            color: #ccc;
            font-weight: 500;
        }

        .button-group {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 12px;
            margin-bottom: 18px;
        }

        button {
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            border: none;
            color: white;
            padding: 12px 16px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 0.95rem;
            font-weight: 500;
            transition: all 0.3s ease;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
        }

        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
        }

        button:active {
            transform: translateY(0);
        }

        button:disabled {
            background: #555;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .remove-btn {
            background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
        }

        .clear-btn {
            background: linear-gradient(135deg, #ff9a00 0%, #ff5e00 100%);
        }

        .color-picker {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        .color-option {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid transparent;
            transition: transform 0.2s, border-color 0.2s;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
        }

        .color-option:hover {
            transform: scale(1.2);
        }

        .color-option.active {
            border-color: white;
            transform: scale(1.2);
        }

        .stats {
            position: fixed;
            bottom: 20px;
            left: 20px;
            background: rgba(25, 25, 35, 0.9);
            color: white;
            padding: 12px 18px;
            border-radius: 10px;
            font-size: 0.95rem;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
        }

        .slider-container {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .slider-value {
            min-width: 45px;
            text-align: right;
            font-weight: 500;
        }

        input[type="range"] {
            flex: 1;
            height: 6px;
            border-radius: 5px;
            background: #444;
            outline: none;
            -webkit-appearance: none;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background: #6aabf2;
            cursor: pointer;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
        }

        .toggle-switch {
            position: relative;
            display: inline-block;
            width: 54px;
            height: 28px;
        }

        .toggle-switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .toggle-slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #555;
            transition: .4s;
            border-radius: 28px;
        }

        .toggle-slider:before {
            position: absolute;
            content: "";
            height: 20px;
            width: 20px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked+.toggle-slider {
            background-color: #6aabf2;
        }

        input:checked+.toggle-slider:before {
            transform: translateX(26px);
        }

        .panel-collapsed .panel-content {
            display: none;
        }

        .info-box {
            position: fixed;
            top: 20px;
            right: 20px;
            background: rgba(25, 25, 35, 0.9);
            color: white;
            padding: 18px;
            border-radius: 12px;
            max-width: 320px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.6);
        }

        .info-title {
            font-size: 1.2rem;
            margin-bottom: 12px;
            color: #6aabf2;
            font-weight: 600;
        }

        .info-box p {
            margin-bottom: 8px;
            line-height: 1.5;
            color: #ddd;
        }

        .camera-controls {
            position: fixed;
            bottom: 80px;
            left: 20px;
            background: rgba(25, 25, 35, 0.9);
            color: white;
            padding: 12px;
            border-radius: 10px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
        }

        .camera-controls button {
            padding: 8px 12px;
            margin: 5px;
            font-size: 0.9rem;
        }

        .coordinates {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(25, 25, 35, 0.9);
            color: white;
            padding: 12px 18px;
            border-radius: 10px;
            font-size: 0.9rem;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.15);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
        }
    </style>
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.89/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.89/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>

<body>
    <div id="cesiumContainer"></div>

    <div class="control-panel" id="controlPanel">
        <div class="panel-header">
            <div class="panel-title">倒立四棱锥控制器</div>
            <button class="panel-toggle" id="togglePanel">−</button>
        </div>
        <div class="panel-content">
            <div class="button-group">
                <button id="addTetrahedron">添加倒立四棱锥</button>
                <button id="removeTetrahedron" class="remove-btn">移除四棱锥</button>
            </div>

            <button id="clearAll" class="clear-btn" style="width: 100%; margin-bottom: 18px;">清除所有</button>

            <div class="control-group">
                <div class="control-label">四棱锥颜色</div>
                <div class="color-picker">
                    <div class="color-option active" style="background-color: #ff3b30;" data-color="#ff3b30"></div>
                    <div class="color-option" style="background-color: #4cd964;" data-color="#4cd964"></div>
                    <div class="color-option" style="background-color: #007aff;" data-color="#007aff"></div>
                    <div class="color-option" style="background-color: #ffcc00;" data-color="#ffcc00"></div>
                    <div class="color-option" style="background-color: #ff9500;" data-color="#ff9500"></div>
                    <div class="color-option" style="background-color: #8e8e93;" data-color="#8e8e93"></div>
                </div>
            </div>

            <div class="control-group">
                <div class="control-label">动画速度</div>
                <div class="slider-container">
                    <input type="range" id="speedSlider" min="0.1" max="2" step="0.1" value="1">
                    <span class="slider-value" id="speedValue">1.0</span>
                </div>
            </div>

            <div class="control-group">
                <div class="control-label">四棱锥大小</div>
                <div class="slider-container">
                    <input type="range" id="sizeSlider" min="5" max="30" step="1" value="15">
                    <span class="slider-value" id="sizeValue">15</span>
                </div>
            </div>

            <div class="control-group">
                <div class="control-label">随机位置</div>
                <label class="toggle-switch">
                    <input type="checkbox" id="randomPosition" checked>
                    <span class="toggle-slider"></span>
                </label>
            </div>
        </div>
    </div>

    <div class="stats" id="stats">
        倒立四棱锥数量: <span id="tetraCount">0</span>
    </div>

    <div class="camera-controls">
        <button id="resetView">重置视角</button>
        <button id="viewAll">查看全部</button>
    </div>

    <div class="coordinates" id="coordinates">
        经度: --<br>纬度: --<br>高度: --
    </div>

    <div class="info-box">
        <div class="info-title">操作说明</div>
        <p>• 点击"添加倒立四棱锥"按钮添加新的倒立四棱锥</p>
        <p>• 点击"移除四棱锥"按钮移除最后一个四棱锥</p>
        <p>• 使用颜色选择器更改四棱锥颜色</p>
        <p>• 调整动画速度和四棱锥大小</p>
        <p>• 启用/禁用随机位置生成</p>
    </div>

    <script>
        // 使用您的Cesium Ion令牌
        Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzZDg3N2ZiZC05MzBiLTRmZmYtYTU1Yy1kOGNiZWU4ZDU4Y2IiLCJpZCI6MTQ0MDksInNjb3BlcyI6WyJhc2wiLCJhc3IiLCJhc3ciLCJnYyJdLCJpYXQiOjE1NjcyNTQ1NDh9.qCtL6_oYhN3VErce9lAiIZCXevo3kPOE4YKD7IDuGiY'

        // 初始化Cesium Viewer
        const viewer = new Cesium.Viewer('cesiumContainer', {
            baseLayerPicker: false,
            shouldAnimate: true,
            infoBox: false,
            animation: false,
            timeline: false,
            fullscreenButton: false,
            terrainProvider: Cesium.createWorldTerrain({
                requestWaterMask: true,
                requestVertexNormals: true
            }),
            selectionIndicator: false,
        });

        // 隐藏版权信息
        viewer._cesiumWidget._creditContainer.style.display = "none";

        // 设置初始相机位置
        viewer.camera.setView({
            destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000),
            orientation: {
                heading: 0,
                pitch: -0.5,
                roll: 0
            }
        });

        // 四棱锥管理
        class TetrahedronManager {
            constructor() {
                this.primitives = [];
                this.selectedColor = Cesium.Color.fromCssColorString('#ff3b30');
                this.animationSpeed = 1.0;
                this.size = 15;
                this.useRandomPosition = true;

                // 使用经纬度坐标
                this.basePosition = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0);
            }

            addTetrahedron() {
                // 计算位置
                let position;
                if (this.useRandomPosition) {
                    // 在基础位置周围随机生成位置
                    const latOffset = (Math.random() - 0.5) * 0.05;
                    const lonOffset = (Math.random() - 0.5) * 0.05;
                    const height = Math.random() * 200 + 50;

                    position = Cesium.Cartesian3.fromDegrees(
                        116.39 + lonOffset,
                        39.9 + latOffset,
                        height
                    );
                } else {
                    // 线性排列
                    const offset = this.primitives.length * 0.01;
                    position = Cesium.Cartesian3.fromDegrees(
                        116.39 + offset,
                        39.9,
                        100
                    );
                }

                // 创建四棱锥
                const primitive = new TetrahedronPrimitive({
                    position: position,
                    color: this.selectedColor,
                    scale: new Cesium.Cartesian3(this.size, this.size, this.size * 1.5),
                    speed: this.animationSpeed
                });

                // 添加到场景
                viewer.scene.primitives.add(primitive);
                primitive.startAnimate();

                // 保存引用
                this.primitives.push(primitive);

                // 更新统计信息
                this.updateStats();

                // 如果是第一个四棱锥,飞行到它
                if (this.primitives.length === 1) {
                    viewer.flyTo(primitive);
                }

                return primitive;
            }

            removeTetrahedron() {
                if (this.primitives.length === 0) return;

                // 获取最后一个四棱锥
                const primitive = this.primitives.pop();

                // 停止动画
                primitive.closeAnimate();

                // 从场景中移除
                viewer.scene.primitives.remove(primitive);

                // 更新统计信息
                this.updateStats();
            }

            clearAll() {
                while (this.primitives.length > 0) {
                    this.removeTetrahedron();
                }
            }

            updateStats() {
                document.getElementById('tetraCount').textContent = this.primitives.length;
            }

            setColor(color) {
                this.selectedColor = color;
            }

            setAnimationSpeed(speed) {
                this.animationSpeed = speed;
                // 更新所有四棱锥的速度
                this.primitives.forEach(primitive => {
                    primitive.setSpeed(speed);
                });
            }

            setSize(size) {
                this.size = size;
            }

            setRandomPosition(enabled) {
                this.useRandomPosition = enabled;
            }

            viewAll() {
                if (this.primitives.length === 0) return;

                // 计算所有四棱锥的边界
                const positions = this.primitives.map(p => p._localPosition);
                const boundingSphere = Cesium.BoundingSphere.fromPoints(positions);

                // 飞行到边界球
                viewer.camera.viewBoundingSphere(boundingSphere,
                    new Cesium.HeadingPitchRange(0, -0.5, boundingSphere.radius * 3));
            }
        }

        // 创建管理器实例
        const tetraManager = new TetrahedronManager();

        // 设置UI事件监听
        document.getElementById('addTetrahedron').addEventListener('click', () => {
            tetraManager.addTetrahedron();
        });

        document.getElementById('removeTetrahedron').addEventListener('click', () => {
            tetraManager.removeTetrahedron();
        });

        document.getElementById('clearAll').addEventListener('click', () => {
            tetraManager.clearAll();
        });

        // 颜色选择
        document.querySelectorAll('.color-option').forEach(option => {
            option.addEventListener('click', function () {
                // 移除所有active类
                document.querySelectorAll('.color-option').forEach(el => {
                    el.classList.remove('active');
                });

                // 添加active类到当前选项
                this.classList.add('active');

                // 设置颜色
                const color = Cesium.Color.fromCssColorString(this.dataset.color);
                tetraManager.setColor(color);
            });
        });

        // 动画速度滑块
        const speedSlider = document.getElementById('speedSlider');
        const speedValue = document.getElementById('speedValue');

        speedSlider.addEventListener('input', function () {
            const value = parseFloat(this.value);
            speedValue.textContent = value.toFixed(1);
            tetraManager.setAnimationSpeed(value);
        });

        // 大小滑块
        const sizeSlider = document.getElementById('sizeSlider');
        const sizeValue = document.getElementById('sizeValue');

        sizeSlider.addEventListener('input', function () {
            const value = parseInt(this.value);
            sizeValue.textContent = value;
            tetraManager.setSize(value);
        });

        // 随机位置开关
        const randomPosition = document.getElementById('randomPosition');
        randomPosition.addEventListener('change', function () {
            tetraManager.setRandomPosition(this.checked);
        });

        // 面板折叠/展开
        document.getElementById('togglePanel').addEventListener('click', function () {
            const panel = document.getElementById('controlPanel');
            panel.classList.toggle('panel-collapsed');
            this.textContent = panel.classList.contains('panel-collapsed') ? '+' : '−';
        });

        // 相机控制
        document.getElementById('resetView').addEventListener('click', function () {
            viewer.camera.setView({
                destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 1000),
                orientation: {
                    heading: 0,
                    pitch: -0.5,
                    roll: 0
                }
            });
        });

        document.getElementById('viewAll').addEventListener('click', function () {
            tetraManager.viewAll();
        });

        // 更新坐标显示
        function updateCoordinates() {
            const cartesian = viewer.camera.position;
            const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
            const longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(5);
            const latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(5);
            const height = cartographic.height.toFixed(2);

            document.getElementById('coordinates').innerHTML =
                `经度: ${longitude}<br>纬度: ${latitude}<br>高度: ${height}`;
        }

        viewer.scene.postRender.addEventListener(updateCoordinates);

        // 添加一些初始四棱锥
        setTimeout(() => {
            tetraManager.addTetrahedron();
            tetraManager.addTetrahedron();
            tetraManager.addTetrahedron();
        }, 1000);

        // 重新设计四棱锥几何结构,添加边线
        function TetrahedronPrimitive(options) {
            this.show = true;
            this._faceCommand = undefined;
            this._edgeCommand = undefined;
            this._enuMatrix = undefined;
            this._scaleMatrix = undefined;
            this._localPosition = options.position;
            this._createFaceCommand = createFaceCommand;
            this._createEdgeCommand = createEdgeCommand;
            this._angle = 0;
            this._distance = Cesium.defaultValue(options.distance, 1);
            this._setInterval = undefined;
            this._viewer = viewer;
            this._speed = Cesium.defaultValue(options.speed, 1.0);
            this._color = Cesium.defaultValue(options.color, new Cesium.Color(1.0, 0.0, 0.0, 1.0));
            this._edgeColor = new Cesium.Color(0.0, 0.0, 0.0, 1.0); // 黑色边线
            this._scale = Cesium.defaultValue(options.scale, new Cesium.Cartesian3(10, 10, 15));
            this._texture = undefined;

            // 计算模型矩阵
            this._modelMatrix = computeModelMatrix(this);
            this._height = computeHeight(this);
        }

        TetrahedronPrimitive.prototype.update = function (frameState) {
            if (!this.show) {
                return;
            }

            // 创建面命令
            if (!Cesium.defined(this._faceCommand)) {
                this._faceCommand = this._createFaceCommand(frameState.context, this);
                this._faceCommand.pickId = 'v_pickColor';
            }

            // 创建边线命令
            if (!Cesium.defined(this._edgeCommand)) {
                this._edgeCommand = this._createEdgeCommand(frameState.context, this);
                this._edgeCommand.pickId = 'v_pickColor';
            }

            // 添加到渲染列表
            if (Cesium.defined(this._faceCommand)) {
                frameState.commandList.push(this._faceCommand);
            }

            if (Cesium.defined(this._edgeCommand)) {
                frameState.commandList.push(this._edgeCommand);
            }
        };

        TetrahedronPrimitive.prototype.isDestroyed = function () {
            return false;
        };

        TetrahedronPrimitive.prototype.destroy = function () {
            if (Cesium.defined(this._faceCommand)) {
                this._faceCommand.shaderProgram = this._faceCommand.shaderProgram && this._faceCommand.shaderProgram.destroy();
            }
            if (Cesium.defined(this._edgeCommand)) {
                this._edgeCommand.shaderProgram = this._edgeCommand.shaderProgram && this._edgeCommand.shaderProgram.destroy();
            }
            return Cesium.destroyObject(this);
        };

        // 开启动画
        TetrahedronPrimitive.prototype.startAnimate = function () {
            let that = this;
            this._setInterval = setInterval(animateFunc, 5);

            function animateFunc() {
                that._angle = that._angle + 0.01 * that._speed;
                if (Math.sin(that._angle) < 0) {
                    that._height = 0.01;
                } else {
                    that._height = -0.01;
                }

                let translation = new Cesium.Cartesian3(0, 0, that._height);
                Cesium.Matrix4.multiplyByTranslation(that._modelMatrix, translation, that._modelMatrix);
                let rotationZ = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(that._speed)));
                Cesium.Matrix4.multiply(that._modelMatrix, rotationZ, that._modelMatrix);
            }
        };

        // 关闭动画
        TetrahedronPrimitive.prototype.closeAnimate = function () {
            clearInterval(this._setInterval);
        };

        // 设置动画速度
        TetrahedronPrimitive.prototype.setSpeed = function (speed) {
            this._speed = speed;
        };

        // 创建面的绘制命令
        function createFaceCommand(context, tetrahedronPrimitive) {
            var translucent = false;
            var closed = true;
            var vs = createFaceVertexShader();
            var fs = createFaceFragmentShader();
            var rawRenderState = Cesium.Appearance.getDefaultRenderState(translucent, closed, undefined);

            // 启用背面剔除,确保棱角分明
            rawRenderState.cull = {
                enabled: true
            };

            var renderState = Cesium.RenderState.fromCache(rawRenderState);
            var vertexShaderSource = new Cesium.ShaderSource({
                sources: [vs]
            });
            var fragmentShaderSource = new Cesium.ShaderSource({
                sources: [fs]
            });
            var uniformMap = {
                color: function () {
                    return tetrahedronPrimitive._color;
                }
            };
            let attributeLocations = {
                position: 0,
                textureCoordinates: 1
            };
            var shaderProgram = Cesium.ShaderProgram.fromCache({
                context: context,
                vertexShaderSource: vertexShaderSource,
                fragmentShaderSource: fragmentShaderSource,
                attributeLocations: attributeLocations
            });
            return new Cesium.DrawCommand({
                vertexArray: createFaceVertexArray(context),
                primitiveType: Cesium.PrimitiveType.TRIANGLES,
                renderState: renderState,
                shaderProgram: shaderProgram,
                uniformMap: uniformMap,
                owner: this,
                pass: Cesium.Pass.TRANSLUCENT,
                modelMatrix: tetrahedronPrimitive._modelMatrix,
            });
        }

        // 创建边线的绘制命令
        function createEdgeCommand(context, tetrahedronPrimitive) {
            var translucent = false;
            var closed = true;
            var vs = createEdgeVertexShader();
            var fs = createEdgeFragmentShader();
            var rawRenderState = Cesium.Appearance.getDefaultRenderState(translucent, closed, undefined);

            // 禁用背面剔除,确保边线可见
            rawRenderState.cull = {
                enabled: false
            };

            // 设置线宽
            rawRenderState.lineWidth = 2.0;

            var renderState = Cesium.RenderState.fromCache(rawRenderState);
            var vertexShaderSource = new Cesium.ShaderSource({
                sources: [vs]
            });
            var fragmentShaderSource = new Cesium.ShaderSource({
                sources: [fs]
            });
            var uniformMap = {
                color: function () {
                    return tetrahedronPrimitive._edgeColor;
                }
            };
            let attributeLocations = {
                position: 0
            };
            var shaderProgram = Cesium.ShaderProgram.fromCache({
                context: context,
                vertexShaderSource: vertexShaderSource,
                fragmentShaderSource: fragmentShaderSource,
                attributeLocations: attributeLocations
            });
            return new Cesium.DrawCommand({
                vertexArray: createEdgeVertexArray(context),
                primitiveType: Cesium.PrimitiveType.LINES,
                renderState: renderState,
                shaderProgram: shaderProgram,
                uniformMap: uniformMap,
                owner: this,
                pass: Cesium.Pass.TRANSLUCENT,
                modelMatrix: tetrahedronPrimitive._modelMatrix,
            });
        }

        function createFaceVertexArray(context) {
            let attributeLocations = {
                position: 0,
                textureCoordinates: 1
            };
            var positionsAndIndice = createFacePositionsAndIndice();
            var geometry = new Cesium.Geometry({
                attributes: {
                    position: new Cesium.GeometryAttribute({
                        componentDatatype: Cesium.ComponentDatatype.FLOAT,
                        componentsPerAttribute: 3,
                        values: positionsAndIndice.positions
                    }),
                    textureCoordinates: new Cesium.GeometryAttribute({
                        componentDatatype: Cesium.ComponentDatatype.FLOAT,
                        componentsPerAttribute: 2,
                        values: positionsAndIndice.sts
                    }),
                },
                indices: positionsAndIndice.indices,
                primitiveType: Cesium.PrimitiveType.TRIANGLES,
                boundingSphere: Cesium.BoundingSphere.fromVertices(positionsAndIndice.positions)
            });
            var geometryNormal = Cesium.GeometryPipeline.computeNormal(geometry);
            var vertexArray = Cesium.VertexArray.fromGeometry({
                context: context,
                geometry: geometryNormal,
                attributeLocations: attributeLocations,
                bufferUsage: Cesium.BufferUsage.STATIC_DRAW,
            });
            return vertexArray;
        }

        function createEdgeVertexArray(context) {
            let attributeLocations = {
                position: 0
            };
            var positionsAndIndice = createEdgePositionsAndIndice();
            var geometry = new Cesium.Geometry({
                attributes: {
                    position: new Cesium.GeometryAttribute({
                        componentDatatype: Cesium.ComponentDatatype.FLOAT,
                        componentsPerAttribute: 3,
                        values: positionsAndIndice.positions
                    })
                },
                indices: positionsAndIndice.indices,
                primitiveType: Cesium.PrimitiveType.LINES,
                boundingSphere: Cesium.BoundingSphere.fromVertices(positionsAndIndice.positions)
            });
            var vertexArray = Cesium.VertexArray.fromGeometry({
                context: context,
                geometry: geometry,
                attributeLocations: attributeLocations,
                bufferUsage: Cesium.BufferUsage.STATIC_DRAW,
            });
            return vertexArray;
        }

        // 创建面的顶点和索引
        function createFacePositionsAndIndice() {
            // 定义5个顶点:4个底部顶点 + 1个顶部顶点
            var positions = new Float64Array(5 * 3);

            // 底部顶点 (z = 0) - 平的底部
            // 顶点0: 前右
            positions[0] = 1.0;
            positions[1] = 1.0;
            positions[2] = 0.0;

            // 顶点1: 前左
            positions[3] = -1.0;
            positions[4] = 1.0;
            positions[5] = 0.0;

            // 顶点2: 后左
            positions[6] = -1.0;
            positions[7] = -1.0;
            positions[8] = 0.0;

            // 顶点3: 后右
            positions[9] = 1.0;
            positions[10] = -1.0;
            positions[11] = 0.0;

            // 顶点4: 顶部顶点 (尖朝上)
            positions[12] = 0.0;
            positions[13] = 0.0;
            positions[14] = 2.0;

            // 定义三角形索引 - 确保所有面都定义
            // 6个三角形:4个侧面 + 2个底部三角形
            var indices = new Uint16Array(6 * 3);

            // 侧面1: 前面 (顶点0, 1, 4)
            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 4;

            // 侧面2: 左面 (顶点1, 2, 4)
            indices[3] = 1;
            indices[4] = 2;
            indices[5] = 4;

            // 侧面3: 后面 (顶点2, 3, 4)
            indices[6] = 2;
            indices[7] = 3;
            indices[8] = 4;

            // 侧面4: 右面 (顶点3, 0, 4)
            indices[9] = 3;
            indices[10] = 0;
            indices[11] = 4;

            // 底部1: 三角形1 (顶点0, 3, 2)
            indices[12] = 0;
            indices[13] = 3;
            indices[14] = 2;

            // 底部2: 三角形2 (顶点0, 2, 1)
            indices[15] = 0;
            indices[16] = 2;
            indices[17] = 1;

            // 纹理坐标
            var sts = new Float32Array([
                // 前面
                0.0, 0.0,
                1.0, 0.0,
                0.5, 1.0,
                // 左面
                0.0, 0.0,
                1.0, 0.0,
                0.5, 1.0,
                // 后面
                0.0, 0.0,
                1.0, 0.0,
                0.5, 1.0,
                // 右面
                0.0, 0.0,
                1.0, 0.0,
                0.5, 1.0,
                // 底部1
                0.0, 0.0,
                1.0, 0.0,
                1.0, 1.0,
                // 底部2
                0.0, 0.0,
                1.0, 1.0,
                0.0, 1.0
            ]);

            return {
                indices: indices,
                positions: positions,
                sts: sts
            };
        }

        // 创建边线的顶点和索引
        function createEdgePositionsAndIndice() {
            // 使用与面相同的顶点位置
            var positions = new Float64Array(5 * 3);

            // 底部顶点 (z = 0) - 平的底部
            // 顶点0: 前右
            positions[0] = 1.0;
            positions[1] = 1.0;
            positions[2] = 0.0;

            // 顶点1: 前左
            positions[3] = -1.0;
            positions[4] = 1.0;
            positions[5] = 0.0;

            // 顶点2: 后左
            positions[6] = -1.0;
            positions[7] = -1.0;
            positions[8] = 0.0;

            // 顶点3: 后右
            positions[9] = 1.0;
            positions[10] = -1.0;
            positions[11] = 0.0;

            // 顶点4: 顶部顶点 (尖朝上)
            positions[12] = 0.0;
            positions[13] = 0.0;
            positions[14] = 2.0;

            // 定义边线索引 - 8条边
            var indices = new Uint16Array(8 * 2);

            // 底部边线
            indices[0] = 0;
            indices[1] = 1;
            indices[2] = 1;
            indices[3] = 2;
            indices[4] = 2;
            indices[5] = 3;
            indices[6] = 3;
            indices[7] = 0;

            // 侧边线
            indices[8] = 0;
            indices[9] = 4;
            indices[10] = 1;
            indices[11] = 4;
            indices[12] = 2;
            indices[13] = 4;
            indices[14] = 3;
            indices[15] = 4;

            return {
                indices: indices,
                positions: positions
            };
        }

        function createFaceVertexShader() {
            var vertexShader =
                `
        attribute vec3 position;
        attribute vec3 normal;
        attribute vec2 st;
        attribute float batchId;
        varying vec3 v_positionEC;
        varying vec3 v_normalEC;
        varying vec2 v_st;
        varying vec4 v_pickColor;
        void main()
        {
            v_positionEC = (czm_modelView * vec4(position, 1.0)).xyz;       // position in eye coordinates
            v_normalEC = czm_normal * normal;                               // normal in eye coordinates
            v_st = st;
            //v_pickColor = czm_batchTable_pickColor(batchId);
            gl_Position = czm_modelViewProjection * vec4(position, 1.0);
        }
        `;
            return vertexShader;
        }

        function createFaceFragmentShader() {
            var fragmentShader =
                `
        varying vec3 v_positionEC;
        varying vec3 v_normalEC;
        varying vec2 v_st;
        uniform vec4 color;
        varying vec4 v_pickColor;
        void main()
        {
            vec3 positionToEyeEC = -v_positionEC;
            vec3 normalEC = normalize(v_normalEC);
            
            // 使用正确的法线计算光照,确保棱角分明
            czm_materialInput materialInput;
            materialInput.normalEC = normalEC;
            materialInput.positionToEyeEC = positionToEyeEC;
            materialInput.st = v_st;
            czm_material material = czm_getDefaultMaterial(materialInput);
            
            // 使用Phong光照模型,确保棱角分明
            material.alpha = color.a;
            material.diffuse = color.rgb;
            material.specular = 0.5;
            material.shininess = 10.0;
            
            gl_FragColor = czm_phong(normalize(positionToEyeEC), material, czm_lightDirectionEC);
        }
        `;
            return fragmentShader;
        }

        function createEdgeVertexShader() {
            var vertexShader =
                `
        attribute vec3 position;
        void main()
        {
            gl_Position = czm_modelViewProjection * vec4(position, 1.0);
        }
        `;
            return vertexShader;
        }

        function createEdgeFragmentShader() {
            var fragmentShader =
                `
        uniform vec4 color;
        void main()
        {
            gl_FragColor = color;
        }
        `;
            return fragmentShader;
        }

        function computeModelMatrix(tetrahedronPrimitive) {
            let enuMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(tetrahedronPrimitive._localPosition);
            let scaleMatrix = Cesium.Matrix4.fromScale(tetrahedronPrimitive._scale);

            // 创建绕X轴旋转180度的矩阵(使四棱锥倒立)
            let rotationX = Cesium.Matrix4.fromRotationTranslation(
                Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(180))
            );

            // 先缩放,再旋转,最后定位
            let modelMatrix = Cesium.Matrix4.multiply(scaleMatrix, rotationX, new Cesium.Matrix4());
            modelMatrix = Cesium.Matrix4.multiply(enuMatrix, modelMatrix, new Cesium.Matrix4());

            tetrahedronPrimitive._scaleMatrix = scaleMatrix;
            tetrahedronPrimitive._enuMatrix = enuMatrix;
            return modelMatrix;
        }

        function computeHeight(tetrahedronPrimitive) {
            let point = Cesium.Cartesian3.fromElements(0, 0, tetrahedronPrimitive._distance, new Cesium.Cartesian3());
            let enuPoint = Cesium.Matrix4.multiplyByPoint(tetrahedronPrimitive._enuMatrix, point, new Cesium.Cartesian3());
            let upPositionEC = Cesium.Matrix4.multiplyByPoint(tetrahedronPrimitive._viewer.scene.camera._viewMatrix, enuPoint, new Cesium.Cartesian3());
            let upPositionPC = Cesium.Matrix4.multiplyByPoint(tetrahedronPrimitive._viewer.scene.camera.frustum.projectionMatrix, upPositionEC, new Cesium.Cartesian3());
            return Cesium.Cartesian3.normalize(upPositionPC, new Cesium.Cartesian3()).z;
        }
    </script>
</body>

</html>
相关推荐
Brookty3 小时前
【算法】前缀和(二)使用
java·学习·算法·前缀和·动态规划·1024程序员节
芯片SIPI设计3 小时前
面向3D IC AI芯片中UCIe 电源传输与电源完整性的系统分析挑战与解决方案
人工智能·3d
兜兜风d'3 小时前
基于 Spring Boot + RabbitMQ 实现应用通信
spring boot·rabbitmq·java-rabbitmq·1024程序员节
小范同学_3 小时前
Spring集成WebSocket
java·spring boot·websocket·spring·1024程序员节
进击的圆儿3 小时前
网络编程实战02·从零搭建Epoll服务器
1024程序员节
计算衎3 小时前
Jenkins上实现CI集成软件信息Teams群通知案例实现。
python·jenkins·1024程序员节·microsoft azure·teams消息群通知·微软 graph api
爱看老照片3 小时前
计算机端口
1024程序员节
go_bai3 小时前
Linux_基础IO(2)
linux·开发语言·经验分享·笔记·学习方法·1024程序员节
西部森林牧歌3 小时前
拒绝笨重,一款轻量、极致简洁的开源CI/CD工具 - Arbess
1024程序员节·cicd·tiklab·arbess