Cesium实现雾气效果:按钮一键控制打开/关闭雾气效果,滑块拖动实时控制雾气浓度

前言:

Cesium数字孪生、智慧城市三维大屏项目中,雾气效果是非常常用的环境模拟效果,可以极大提升三维场景的真实感、氛围感。

本篇文章带来一套可通过按钮点击来控制雾气效果的打开和关闭,并且通过slider滑块控制雾气的浓度。

实现效果:

  1. 点击按钮 开启雾气 / 关闭雾气
  2. 雾气开启状态下,通过 Slider 滑块 实时拖拽调节雾气浓度

演示动态图示如下:

演示地址如下:

web3d-demo-collection (xiazhi.tech)

实现原理

Cesium 想要实现自定义屏幕雾效,最佳方案是PostProcessStage屏幕后处理着色器:

  1. 通过自定义片元着色器,对整个屏幕画面做二次渲染混合,生成大气雾效果。
  2. 将雾气浓度抽取为 uniform 全局变量,支持 JS 外部动态修改。
  3. 使用按钮控制「后处理阶段的添加/移除」,实现雾气开关效果。
  4. 使用Slider滑块双向绑定数值,拖动实时更新雾气浓度。

完整代码

1. template 结构:

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

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

        <div class="btn-border" v-if="isLoading">

            <div class="slider-border" v-if="isFog">
                <div class="slider-label">雾气浓度:{{ Number(fogDensity).toFixed(2) }}</div>

                <!-- 雾气浓度滑块容器,用于控制雾气的浓度,仅开启雾气时有效 -->
                <el-slider
                    class="fog-slider"
                    v-model.number="fogDensity"
                    :min="0"
                    :max="1"
                    :step="0.01"
                    @input="updateFogDensity"
                />
            </div>

            <!-- 镜头复位按钮,点击回到预设城市视角 -->
            <el-button type="primary" size="default" class="btn" @click="flyTo">初始位置</el-button>

             <!-- 雾气开关按钮,控制是否打开雾气效果 -->
            <el-button type="primary" size="default" class="btn" @click="fogControl">
                {{ isFog ? '关闭雾气' : '开启雾气' }}
            </el-button>
        </div>

<!-- 加载提示文字,地图未初始化完成时显示 -->
        <div class="loading" v-if="!isLoading">Loading...</div>
    </div>
</template>

2. script 代码:

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

// 雾气效果开关状态,true时为开启雾气效果,false时为关闭雾气效果
const isFog = ref(false);

// 雾气浓度值,初始值为1,此值用来控制雾气的浓度
const fogDensity = ref(1);

const isLoading = ref(false);

let myMar = null;

onUnmounted(() => {
    // 如果雾气效果开着,则从场景后处理管线移除雾气效果,以及释放WebGL资源
    if (isFog.value && window.fogStage) {
        window.viewer.scene.postProcessStages.remove(window.fogStage);
        window.fogStage = null;
        isFog.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  // 不保留绘图缓冲(节省内存)
            }
        }
    });
	
    // 设置模拟时间(可选,用于光影效果)
    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 updateFogDensity = (val) => {
    if (window.fogStage) {  // 校验雾效后处理阶段实例是否存在
        window.fogStage.uniforms.fogStrength = Number(val);  // // 将滑块数值转为数字,赋值给着色器的fogStrength变量,用以控制雾气浓度
    }
};

// 雾气开关控制方法,开启/销毁雾后处理着色器
const fogControl = () => {
    if (isFog.value) {  // 当前雾气为开启状态时,执行关闭雾气的逻辑
        window.viewer.scene.postProcessStages.remove(window.fogStage);  // 从场景后处理管线移除雾效果
        window.fogStage = null;  // 全局雾效实例置空释放资源
        isFog.value = false;  // 标记雾气关闭
    } else {
        // 雾气为关闭状态时,创建雾气片元着色器
        const FogShader = `
            // Cesium后处理内置纹理:存储场景渲染完成后的整张画面(地形、3DTiles、底图全部合成纹理)
            uniform sampler2D colorTexture;
            // 外部JS传入的雾气浓度控制变量,滑块拖动实时修改该值,范围0~1
            uniform float fogStrength;
            // 内置插值纹理坐标,代表当前片元在屏幕上的xy坐标,取值范围[0,1]
            in vec2 v_textureCoordinates;
            // 输出:最终叠加雾气后的像素RGBA颜色,渲染到屏幕
            out vec4 fragColor;

            void main(void) {
                // 1、根据当前像素纹理坐标,采样获取原始场景的像素颜色(未叠加雾气)
                vec4 originColor = texture(colorTexture, v_textureCoordinates);

                // 定义雾气基础色调:浅灰白蓝色,模拟城市日常薄雾/雾霾视觉效果
                vec3 fogColor = vec3(0.88, 0.9, 0.92);
                
                // 提取当前像素垂直纹理坐标Y
                // screenY=0 画面最底部(近处建筑,雾淡),screenY=1 画面最顶部(远景天际,雾浓)
                float screenY = v_textureCoordinates.y;
                
                // 计算雾气混合权重系数
                // smoothstep(下限,上限,输入值):0.15以下无雾,0.15~1区间平滑渐变,避免分层生硬边界
                // 乘以fogStrength外部浓度,实现滑块整体控制雾的轻重
                float fogFactor = smoothstep(0.15, 1.0, screenY) * fogStrength;
                
                // mix(原色,雾色,混合系数):根据fogFactor融合原图与雾色
                // fogFactor=0 完全显示原图;fogFactor=1 完全覆盖为雾色
                vec3 finalRGB = mix(originColor.rgb, fogColor, fogFactor);
                
                // 拼接原图透明度通道,输出最终带雾效果的像素
                fragColor = vec4(finalRGB, originColor.a);
            }
        `;

        // 实例化后处理阶段,挂载雾气着色器与uniform初始值
        window.fogStage = new Cesium.PostProcessStage({
            name: 'fog_effect',  // 后处理阶段唯一标识名称
            fragmentShader: FogShader,  // 传入自定义的雾气片元着色器代码(之前定义好的FogShader)
            uniforms: {
                fogStrength: fogDensity.value  // 初始化定义好的uniform雾气浓度的变量fogStrength,值为ref变量fogDensity的值
            }
        });
        // 将雾后处理阶段加入场景渲染管线,实时生效
        window.viewer.scene.postProcessStages.add(window.fogStage);
        // 标记雾气为开启状态
        isFog.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: stretch;
}

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

.slider-label {
    font-size: 14px;
}

.slider-border {
    width: 260px;
    margin-right: 20px;
    position: relative;
    top: -9px;
}

核心逻辑深度解析

1. 雾气开关实现逻辑:

很多开发者实现雾效只会叠加,不会销毁,导致多次开关后场景卡顿、图层叠加错乱。 本文采用按需创建、即时销毁的思路:

开启雾气 → 新建后处理阶段加入渲染管线

关闭雾气 → 从渲染管线彻底移除实例、置空对象

保证场景永远只会存在一个雾效实例,性能最优。

2.滑块实时调节原理:

着色器中的 fogStrength 是外部可读写 Uniform 变量。

滑块拖动时实时赋值,GPU 立即参与片元计算,真正做到毫秒级动态更新,无卡顿、无延迟。

3. 分层雾真实感原理:

利用屏幕 Y 轴坐标 screenY 配合 smoothstep 平滑插值:

画面上方(远景)雾浓度高,画面下方(近景建筑)雾浓度低。

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

相关推荐
锋行天下3 小时前
如何用Vite实现Vue组件的按需打包和远程加载
前端·vue.js·前端框架
用户900463370404 小时前
用Gemini搞定Vue报错和语法异常的问题
vue.js
小兔崽子去哪了7 小时前
Vue3 + Pinia 集成 IGV.js 实现 BAM 文件在线浏览
javascript·vue.js·后端
OpenTiny社区1 天前
🎨 看完 GenUI SDK 源码我悟了!
前端·vue.js·github
mqcode1 天前
你项目里的 axios,封对了吗?从裸用到生产级的四步进化
vue.js·axios
Linsk1 天前
组件 = 模板 + 业务逻辑
java·前端·vue.js
前端啊1 天前
告别 el-table 打印难题,vue3-print-el-table 来了!
前端·vue.js
AprChell1 天前
低代码设计器和低代码设计引擎架构综述
前端·vue.js·低代码
Ruihong1 天前
🎉 VuReact 1.9.0 发布,支持 Vue 3.4 defineModel 编译到 React
vue.js·react.js·面试