Vue3 + Cesium 实现城市 3D 场景下雪特效(按钮开关控制下雪启停)

前言:

本文基于 Vue3 + Cesium,实现了3D Tiles 城市模型加载,并且可以通过按钮控制是否下雪

实现效果:

  1. 地图加载完成后,自动飞往预设的城市视角。
  2. 城市的3D Tiles模型渐进式加载
  3. 右上角按钮一键控制打开下雪 / 关闭下雪,下雪的效果自然流畅。

本案例中Cesium版本为1.141.0

演示动态图示如下:

演示地址如下:

3d.xiazhi.tech/3d-demos/in...

核心技术栈:

  • Vue3
  • CesiumJS(3D 地球)
  • 3D Tiles(城市模型加载)
  • GLSL 着色器(雪花特效实现)

完整代码

1. template结构:

xml 复制代码
<template>
    <div class="main">

        <!-- 地球容器 -->
        <div class="content" ref="content" id="earth"></div>

        <div class="btn-border" v-if="isLoading">
            <el-button type="primary" size="default" class="btn" @click="flyTo">初始位置</el-button>
            <!-- 控制是否下雪的开关 -->
            <el-button type="primary" size="default" class="btn" @click="snowControl">{{ isSnow ? '关闭下雪' : '下雪' }}</el-button>
        </div>

        <!-- 加载中提示 -->
        <div class="loading" v-if="!isLoading">Loading...</div>

    </div>
</template>

2. script代码:

ini 复制代码
<script setup>
import { onMounted, nextTick, ref, onUnmounted } from 'vue';
import { token } from '../../utils/common.js';
import { ElMessage } from 'element-plus';

// 是否在下雪
let isSnow = ref(false);

// 地图是否在加载中
let isLoading = ref(false);

let myMar = null;

onUnmounted(() => {
    // 如果正在下雪,先移除后处理阶段(PostProcessStage)
    if (isSnow.value) {
        window.viewer.scene.postProcessStages.remove(window.snow);
        window.snow = null;
        isSnow.value = false;
    }

    // 销毁 Cesium Viewer 实例,释放资源
    if (window.viewer) {
        window.viewer.destroy();
        window.viewer = null;
    }

    if (myMar) {
        clearTimeout(myMar);
        myMar = null;
    }
});


// 组件挂载后:初始化地图
onMounted(() => {
    nextTick(() => {
        initMap();
    });
});

// 初始化 Cesium 地图
const initMap = () => {

    // 设置 Cesium Ion 的token(这里替换成您的Cesium Ion token)
    Cesium.Ion.defaultAccessToken = token;

    // 设置默认视角范围(中国区域)
    Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

    // 创建 Viewer 实例
    window.viewer = new Cesium.Viewer('earth', {
        animation: false,  // 关闭动画控件
        timeline: false,  // 关闭时间轴
        infoBox: false,  // 关闭信息框
        geocoder: false,  // 关闭地理编码搜索
        homeButton: false,  // 关闭主页按钮
        sceneModePicker: false,  // 关闭场景模式切换
        baseLayerPicker: false,  // 关闭底图选择器
        navigationHelpButton: false,  // 关闭导航帮助
        fullscreenButton: false,  // 关闭全屏按钮
        selectionIndicator: false,  // 关闭选择指示器
        shouldAnimate: false,  // 关闭自动播放动画
        contextOptions: {  // WebGL 上下文配置
            webgl: {
                powerPreference: "high-performance",  // 高性能模式
                preserveDrawingBuffer: false  // 不保留绘图缓冲(节省内存)
            }
        }
    });

    // 设置模拟时间(可选,用于光影效果)
    let utc = Cesium.JulianDate.fromDate(new Date('2026/05/02 15:00:00'));

    // 加载 3D 模型
    addModel();

    myMar = setTimeout(() => {
        isLoading.value = true;
        // 飞往模型位置
        flyTo();
    }, 6000);
};

// 飞往模型位置的方法
const flyTo = () => {
    window.viewer.camera.flyTo({
        destination: Cesium.Cartesian3.fromDegrees(116.38930915426614, 39.90736899736818, 83.88926560891),
        orientation: {
            heading: Cesium.Math.toRadians(204.08974092232688),  // 航向角
            pitch: Cesium.Math.toRadians(-12.743790286888126),  // 俯仰角
            roll: Cesium.Math.toRadians(0.0004915824795912258)  // 翻滚角
        },
        duration: 6  // 飞行时长6秒
    });
};

// 加载 3D Tiles 模型
const addModel = async () => {
    try {
        const tileset = await Cesium.Cesium3DTileset.fromUrl(
            // 注:请将下方的 URL 替换为您的 3D Tiles 模型服务地址
            'YOUR_3D_TILES_URL / tileset.json',
            {
                maximumScreenSpaceError: 48,  // 屏幕空间误差(越小越精细,越大越省性能)
                maximumSimultaneousTileLoads: 16,  // 同时加载瓦片数(默认 8,可适当增加)
                preloadAncestors: false,  // 不预加载祖先瓦片(节省内存)
                preloadSiblings: true,  // 预加载兄弟瓦片(提升流畅度)
                maximumMemoryUsage: 512,  // 【关键】内存上限 512MB(防止爆内存)
                skipLevelOfDetail: true,  // 跳过细节层级(提升加载速度)
                baseScreenSpaceError: 1024  // 基础屏幕空间误差(用于 LOD)
            }
        );

        // 将模型添加到场景中
        window.tileset = window.viewer.scene.primitives.add(tileset);
    } catch (err) {
        console.error('3D Tiles 加载失败', err);
    }
};

// 控制下雪效果的方法
const snowControl = () => {
    // 如果正在下雪,则关闭下雪
    if (isSnow.value) {
        window.viewer.scene.postProcessStages.remove(window.snow);
        window.snow = null;
        isSnow.value = false;
    } else {
        // 否则,创建下雪的着色器(GLSL)相关的代码
        const Snow = `
            uniform sampler2D colorTexture;
            in vec2 v_textureCoordinates;
            out vec4 fragColor;

            float snow(vec2 uv, float scale) {
                float time = czm_frameNumber / 60.0;
                float w = smoothstep(1., 0., -uv.y * (scale / 10.));
                if (w < .1)
                                return 0.;
                uv += time / scale;
                uv.y += time * 2. / scale;
                uv.x += sin(uv.y + time * .5) / scale;
                uv *= scale;
                vec2 s = floor(uv), f = fract(uv), p;
                float k = 3., d;
                p = .5 + .35 * sin(11. * fract(sin((s + p + scale) * mat2(7, 3, 6, 5)) * 5.)) - f;
                d = length(p);
                k = min(d, k);
                k = smoothstep(0., k, sin(f.x + f.y) * 0.01);
                return k * w;
            }

            void main(void) {
                vec2 resolution = czm_viewport.zw;
                vec2 uv = (gl_FragCoord.xy * 2. - resolution.xy) / min(resolution.x, resolution.y);
                vec3 finalColor = vec3(0);
                float c = 0.0;
                c += snow(uv, 30.) * .0;
                c += snow(uv, 20.) * .0;
                c += snow(uv, 15.) * .0;
                c += snow(uv, 10.);
                c += snow(uv, 8.);
                c += snow(uv, 6.);
                c += snow(uv, 5.);
                finalColor = vec3(c);
                vec4 color = texture(colorTexture, v_textureCoordinates);
                fragColor = mix(color, vec4(finalColor, 1.0), 0.5);
            }
            `;

        // 创建后处理阶段(下雪效果)
        window.snow = new Cesium.PostProcessStage({
            name: 'czm_snow',  // 阶段名称
            fragmentShader: Snow  // 自定义着色器
        });

        // 添加到场景的后处理链中
        window.viewer.scene.postProcessStages.add(window.snow);
        isSnow.value = true;  // 标记正在下雪
    }
};
</script>

3. css样式代码:

css 复制代码
* {
    margin: 0;
    padding: 0;
}

.main {
    width: 100%;
    height: 100vh;
    position: relative;
}

.content {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.loading {
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 3;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 34px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 50px;
    color: #000000;
}

.btn-border {
    position: absolute;
    right: 24px;
    top: 24px;
    z-index: 2;
    display: flex;
    justify-content: start;
    align-items: center;
}

.btn {
    margin-left: 20px;
    cursor: pointer;
}

关键知识点总结:

  1. Cesium 后处理阶段(PostProcessStage):可实现雨雪、光斑、模糊等全屏特效,不影响 3D 模型本身。
  2. GLSL 着色器:雪花通过数学函数随机生成,配合时间实现连续飘落动画。
  3. 3D Tiles 优化:免费工具生成的瓦片可通过 maximumScreenSpaceError 等参数优化加载速度。
  4. Vue3 + Cesium 规范:必须在 onUnmounted 销毁实例、清理定时器、移除特效,避免内存泄漏。

模型说明: 文中 3D 城市模型来源于 Sketchfab免费共享库,本人仅作技术演示使用。

相关推荐
BJ-Giser6 天前
CesiumJS升级全新VFX特效粒子系统
前端·可视化·cesium
白嫖叫上我6 天前
Cesium抗锯齿处理
cesium
白嫖叫上我6 天前
Cesium地球风格切换、昼夜交替效果
cesium
用户83134859306987 天前
Vue3 + Cesium 实现热气球第一人称自动飞行(支持手机端)
cesium
青山Coding8 天前
Cesium应用(六):三维地形中坡度分析的实现过程
前端·cesium
爱喝铁观音的谷力景辉11 天前
在Cesium中实现带箭头方向路线样式的技术详解
javascript·cesium
Nian.Baikal12 天前
Cesium 3D Tiles 加载与优化实战
前端·cesium
青山Coding16 天前
Cesium应用(五):通视分析,解锁三维场景的”无遮挡“视野
前端·cesium
BJ-Giser19 天前
Cesium 体积光阴影率分析和阴影体渲染效果
前端·可视化·cesium