前言:
在Cesium数字孪生、智慧城市三维大屏项目中,雾气效果是非常常用的环境模拟效果,可以极大提升三维场景的真实感、氛围感。
本篇文章带来一套可通过按钮点击来控制雾气效果的打开和关闭,并且通过slider滑块控制雾气的浓度。
实现效果:
- 点击按钮 开启雾气 / 关闭雾气。
- 雾气开启状态下,通过 Slider 滑块 实时拖拽调节雾气浓度。
演示动态图示如下:

演示地址如下:
web3d-demo-collection (xiazhi.tech)
实现原理
Cesium 想要实现自定义屏幕雾效,最佳方案是PostProcessStage屏幕后处理着色器:
- 通过自定义片元着色器,对整个屏幕画面做二次渲染混合,生成大气雾效果。
- 将雾气浓度抽取为
uniform全局变量,支持 JS 外部动态修改。 - 使用按钮控制「后处理阶段的添加/移除」,实现雾气开关效果。
- 使用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免费共享库,本人仅作技术演示使用。