现有的gis开发方向较流行的是webgis开发,其中Cesium是一款开源的WebGIS库,主要用于实时地球和空间数据的可视化和分析。它提供了丰富的地图显示和数据可视化功能,并能实现三维可视化开发。本文将使用一些特殊的材质,如视频材质、自定义材质和Cesium内置的一些特殊效果类、粒子系统等实现一些特效场景的模拟,包括云、雾、动态水面、雷达扫描、流动线、电子围栏、粒子烟花、粒子火焰及粒子天气等。
对于通过Entity方式和Primitive方式创建的几何实体,下面介绍如何给几何实体贴上一个特殊的材质,即视频材质。
视频资源网址: https://cesium.com/public/SandcastleSampleData/big-buck-bunny_trailer.mp4
(1)实现代码
6_1_视频材质.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.toolbar {
position: absolute;
top: 10px;
left: 20px;
background-color: rgb(0, 0, 0, 0);
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<div class="toolbar">
<select id="dropdown" onchange="change()">
<option value="edit1">视频材质</option>
<option value="edit2">视频重复</option>
</select>
</div>
<video id="myVideo" muted="true" autoplay="true" loop="true" style="display: none;">
<source src="./vedio/big-buck-bunny_trailer.mp4" type="video/mp4">
</video>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
timeline: false,
animation: false,
fullscreenButton: false,
});
//viewer.scene.globe.depthTestAgainstTerrain = false;
const videoElement = document.getElementById("myVideo");
//将视频元素与模拟时钟同步
let synchronizer = new Cesium.VideoSynchronizer({
clock: viewer.clock,
element: videoElement
});
viewer.clock.shouldAnimate = true;
var sphere = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(104, 39, 2200),
ellipsoid: {
radii: new Cesium.Cartesian3(1000, 1000, 1000),
material: videoElement,
},
});
//相机视角锁定sphere
viewer.trackedEntity = sphere;
//改变视频重复个数
var isRepeat = false;
sphere.ellipsoid.material.repeat = new Cesium.CallbackProperty(
function (result) {
if (isRepeat) {
result.x = 8;
result.y = 8;
} else {
result.x = 1;
result.y = 1;
}
return result;
},
false
);
var dropdown = document.getElementById('dropdown');
function change() {
switch (dropdown.value) {
case 'edit1':
isRepeat = false;
break;
case 'edit2':
isRepeat = true;
break;
default:
break;
}
}
</script>
</body>
</html>
在Cesium中,可以通过viewer.resolutionScale获取或者设置渲染分辨率的缩放比例。当该属性值小于1.0时,可以改善性能不佳的设备的显示效果,而当该属性值大于1.0时,将以更快的速度呈现分辨率,并缩小比例,从而提高视觉保真度。例如,如果窗口的尺寸为640像素×480像素,则将viewer.resolutionScale的值设置为0.5,会导致场景以320像素×240像素渲染,之后设置为2.0,会导致场景以1280像素×960像素渲染。
(1)实现代码
6_2_分辨率尺度.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>材质特效篇_分辨率尺度</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium//Widgets/widgets.css">
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.toolbar {
position: absolute;
top: 10px;
left: 20px;
background-color: rgba(0, 0, 0, 0.6);
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<div class="toolbar">
<label style="color: white;">分辨率尺度</label> <br />
<input type="range" max="2" step="0.1" oninput="change()" id="R" value="1">
<input type="text" style="width:70px; " id="resolutionValue" value="1" onchange="change2()">
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
var tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: './倾斜摄影/大雁塔3DTiles/tileset.json',
}));
viewer.zoomTo(tileset);
function change() {
//拿到滑动条当前值
var resolutionScale = Number(R.value);
//将值约束在0.1和2.0之间
resolutionScale = Cesium.Math.clamp(resolutionScale, 0.1, 2.0);
//文本框显示当前值
resolutionValue.value = resolutionScale;
//修改分辨率尺度
viewer.resolutionScale = resolutionScale;
}
function change2() {
var resolutionScale = Number(resolutionValue.value);
//将值约束在0.1和2.0之间
resolutionScale = Cesium.Math.clamp(resolutionScale, 0.1, 2.0);
R.value = resolutionScale;
change();
}
</script>
</body>
</html>
在模拟实际场景时,可以通过CloudCollection类在场景中渲染云,同时支持手动修改云的大小、亮度等来模拟积云。基本思路为先使用CloudCollection类创建一个云集合,然后在云集合中添加定义的不同样式的云。
(1)实现代码
6_3_云.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<meta name="description" content="Fog post process">
<meta name="cesium-sandcastle-labels" content="Showcases, Post Processing">
<title>材质特效篇_云</title>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script type="text/javascript" src="./Build/Cesium/Cesium.js"></script>
</head>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.toolbar {
position: absolute;
top: 10px;
left: 20px;
color: white;
background-color: rgba(0, 0, 0, 0.6);
}
</style>
<body>
<div id="cesiumContainer"></div>
<div class="toolbar">
<label>X轴尺寸</label> <br />
<input type="range" min="5" max="50" step="1" oninput="changeScale()" id="ScaleX" value="25">
<input type="text" style="width:70px; " id="ScaleXValue" value="25" onchange="changeScaleX()"> <br>
<label>Y轴尺寸</label> <br />
<input type="range" min="5" max="50" step="1" oninput="changeScale()" id="ScaleY" value="12">
<input type="text" style="width:70px; " id="ScaleYValue" value="12" onchange="changeScaleY()"> <br>
<label>亮度</label> <br />
<input type="range" min="0" max="1" step="0.01" oninput="changeBrightness()" id="Brightness" value="1">
<input type="text" style="width:70px; " id="BrightnessValue" value="1" onchange="changeBrightnessValue()"> <br>
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
//创建并添加云集合
var clouds = viewer.scene.primitives.add(
new Cesium.CloudCollection({
noiseDetail: 16.0,
})
);
//添加云
var cloud = clouds.add({
position: Cesium.Cartesian3.fromDegrees(114.39264, 30.52252, 100),
scale: new Cesium.Cartesian2(25, 12),
slice: 0.36,
brightness: 1,
})
//设置相机位置及方向
viewer.camera.lookAt(
Cesium.Cartesian3.fromDegrees(114.39264, 30.52252, 100),
new Cesium.Cartesian3(30, 30, -10)
);
var ScaleX = document.getElementById('ScaleX'); //X轴尺寸
var ScaleXValue = document.getElementById('ScaleXValue'); //ScaleX滑动条值
var ScaleY = document.getElementById('ScaleY'); //Y轴尺寸
var ScaleYValue = document.getElementById('ScaleYValue'); //ScaleY滑动条值
var Brightness = document.getElementById('Brightness'); //亮度
var BrightnessValue = document.getElementById('BrightnessValue'); //亮度滑动条值
//Scale滑动条
function changeScale() {
//拿到scaleX滑动条当前值
var sX = Number(ScaleX.value);
//文本框显示当前值
ScaleXValue.value = sX;
//拿到scaleY滑动条当前值
var sY = Number(ScaleY.value);
//x轴旋转文本框显示当前值
ScaleYValue.value = sY;
//修改云的比例
cloud.scale = new Cesium.Cartesian2(sX, sY);
}
//ScaleX文本框
function changeScaleX() {
//拿到scaleX文本框的值并赋值给滑动条
ScaleX.value = Number(ScaleXValue.value);
changeScale();
}
//ScaleY文本框
function changeScaleY() {
//拿到scaleY文本框的值并赋值给滑动条
ScaleY.value = Number(ScaleYValue.value);
changeScale();
}
//Brightness滑动条
function changeBrightness() {
//拿到Brightness滑动条滑动条当前值
var brightness = Number(Brightness.value);
//文本框显示当前值
BrightnessValue.value = brightness;
//修改云的亮度
cloud.brightness = brightness;
}
//Brightness文本框
function changeBrightnessValue() {
//拿到文本框的值并赋值给滑动条
Brightness.value = Number(BrightnessValue.value);
changeBrightness();
}
</script>
</body>
</html>
Cesium在1.46版本之后新增了场景后处理功能。所谓场景后处理,我们可以将其理解为一个不断叠加的过程。例如,我们拍了一张照片,拍完之后觉得该照片亮度不够,于是我们在该照片的基础上进行了亮度的调整,得到了一张新照片,然后觉得新照片不够好看,又在新照片的基础上添加了滤镜,此后我们可能还会进行多次处理,直到最后得到的照片满足我们的要求为止,这个过程就类似于场景后处理,即我们在绘制场景时可能会不断地对场景进行一些处理,将最终符合我们要求的处理结果绘制到屏幕上。下面通过Cesium的场景后处理功能来实现雾的效果。
(1)实现代码
6_4_雾效果.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<meta name="description" content="Fog post process">
<meta name="cesium-sandcastle-labels" content="Showcases, Post Processing">
<title>材质特效篇_雾效果</title>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script type="text/javascript" src="./Build/Cesium/Cesium.js"></script>
</head>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<body>
<div id="cesiumContainer"></div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
var tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: './倾斜摄影/大雁塔3DTiles/tileset.json',
}));
viewer.zoomTo(tileset);
var fragmentShaderSource =
`//计算每个渲染顶点和视点(相机)的距离
float getDistance(sampler2D depthTexture, vec2 texCoords)
{
float depth = czm_unpackDepth(texture2D(depthTexture, texCoords));
if (depth == 0.0) {
return czm_infinity;
}
vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, depth);
return -eyeCoordinate.z / eyeCoordinate.w;
}
//按距离进行插值
float interpolateByDistance(vec4 nearFarScalar, float distance)
{
float startDistance = nearFarScalar.x;
float startValue = nearFarScalar.y;
float endDistance = nearFarScalar.z;
float endValue = nearFarScalar.w;
float t = clamp((distance - startDistance) / (endDistance - startDistance), 0.0, 1.0);
return mix(startValue, endValue, t);
}
//计算透明度
vec4 alphaBlend(vec4 sourceColor, vec4 destinationColor)
{
return sourceColor * vec4(sourceColor.aaa, 1.0) + destinationColor * (1.0 - sourceColor.a);
}
uniform sampler2D colorTexture; //颜色纹理 内置变量
uniform sampler2D depthTexture; //深度纹理 内置变量
varying vec2 v_textureCoordinates; //屏幕采样点坐标 内置变量
uniform vec4 fogByDistance; //自定义属性 外部变量
uniform vec4 fogColor; //自定义属性 外部变量
void main(void)
{
float distance = getDistance(depthTexture, v_textureCoordinates);
vec4 sceneColor = texture2D(colorTexture, v_textureCoordinates);
float blendAmount = interpolateByDistance(fogByDistance, distance);
vec4 finalFogColor = vec4(fogColor.rgb, fogColor.a * blendAmount);
gl_FragColor = alphaBlend(finalFogColor, sceneColor);
}`;
var postProcessStage = new Cesium.PostProcessStage({
//片源着色器
fragmentShader: fragmentShaderSource,
uniforms: {
fogByDistance: new Cesium.Cartesian4(0, 0, 600, 1.0), //距离
fogColor: Cesium.Color.WHITE, //颜色
},
})
viewer.scene.postProcessStages.add(postProcessStage);
</script>
</body>
</html>
(2)结果显示
模拟水面效果也是Cesium场景中常见的功能,例如,有的项目可能通过绘制实体面,并设置材质为淡蓝色来模拟水面。但是,在实际生活中,水面往往不是静止的而是动态的,下面通过修改水面的材质来实现动态水面的效果。动态水面的具体实现思路为:先准备一张水面纹理图片,然后通过Primitive方式创建一个矩形实体,使用EllipsoidSurfaceAppearance定义一个水面材质,并给矩形实体设置该材质,即可实现简单的动态水面效果。
(1)实现代码
6_5_动态水面.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>材质特效篇_动态水面</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
terrainProvider: Cesium.createWorldTerrain()
});
viewer.scene.globe.depthTestAgainstTerrain = true;//开启深度检测后 会有高程遮挡效果
var rectangle = new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(95.0, 39.0, 100.0, 42.0),
height: 3500.0
})
});
//定义外观
var rectangleAppearance = new Cesium.EllipsoidSurfaceAppearance({
aboveGround: true,
material: new Cesium.Material({
fabric:
{
type: 'Water', //材质类型
uniforms: {
//baseWaterColor: new Cesium.Color.fromBytes(24, 173, 247, 100),//基础颜色
normalMap: './RasterImage/图片/动态水面.jpg', //法线纹理贴图
frequency: 100.0, //波的数量
animationSpeed: 0.01, //水波震动速度
amplitude: 10.0 //振幅大小
},
}
}),
//重写shader,修改水面的透明度
fragmentShaderSource: 'varying vec3 v_positionMC;\n' +
'varying vec3 v_positionEC;\n' +
'varying vec2 v_st;\n' +
'void main()\n' +
'{\n' +
'czm_materialInput materialInput;\n' +
'vec3 normalEC = normalize(czm_normal3D * czm_geodeticSurfaceNormal(v_positionMC, vec3(0.0), vec3(1.0)));\n' +
'#ifdef FACE_FORWARD\n' +
'normalEC = faceforward(normalEC, vec3(0.0, 0.0, 1.0), -normalEC);\n' +
'#endif\n' +
'materialInput.s = v_st.s;\n' +
'materialInput.st = v_st;\n' +
'materialInput.str = vec3(v_st, 0.0);\n' +
'materialInput.normalEC = normalEC;\n' +
'materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(v_positionMC, materialInput.normalEC);\n' +
'vec3 positionToEyeEC = -v_positionEC;\n' +
'materialInput.positionToEyeEC = positionToEyeEC;\n' +
'czm_material material = czm_getMaterial(materialInput);\n' +
'#ifdef FLAT\n' +
'gl_FragColor = vec4(material.diffuse + material.emission, material.alpha);\n' +
'#else\n' +
'gl_FragColor = czm_phong(normalize(positionToEyeEC), material, czm_lightDirectionEC);\n' +
'gl_FragColor.a=0.55;\n' +
'#endif\n' +
'}\n'
});
var addRectangleGeometry = new Cesium.Primitive({
geometryInstances: rectangle,
appearance: rectangleAppearance
})
viewer.scene.primitives.add(addRectangleGeometry);
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(108, 42, 6000000),
})
</script>
</body>
</html>
使用飞机或无人机沿着飞行路线进行雷达扫描的效果在实际应用中是很常见的。在Cesium中实现雷达扫描效果的方法有很多,可以通过对Entity实体贴纹理并对材质进行不断的旋转来实现,或者通过着色器重写Entity实体的材质shader来实现。比较而言,前者对于新手来说更容易实现,下面通过第一种方法来模拟雷达扫描效果。
(1)实现代码
6_6_雷达扫描.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>材质特效篇_雷达扫描</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium//Widgets/widgets.css">
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
var rotation = 0; //纹理旋转角度
var amount = 4; //旋转变化量
var rader = {
position: Cesium.Cartesian3.fromDegrees(114.40372, 30.52252),
ellipse: {
semiMajorAxis: 300.0,
semiMinorAxis: 300.0,
//指定材质
material: new Cesium.ImageMaterialProperty({
image: './RasterImage/图片/color.png',
color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
}),
// 不设置高度则无法渲染外框线
height: 0.0,
//heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
//外边框
outline: true,
outlineWidth: 2,
outlineColor: new Cesium.Color(1.0, 1.0, 0.0, 1.0),
//纹理旋转角度通过CallbackProperty回调
stRotation: new Cesium.CallbackProperty(function () {
rotation += amount;
if (rotation >= 360 || rotation <= -360) {
rotation = 0;
}
//度数转弧度
return Cesium.Math.toRadians(rotation);
}, false)
}
}
//将rader添加进entity集合
viewer.entities.add(rader)
var point = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(114.40372, 30.52252),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
}
});
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(114.40372, 30.52252, 2000)
});
</script>
</body>
</html>
Cesium中有许多封装好的内置纹理,如条纹、颜色、虚线、棋盘、水面等,但是这些内置纹理大多是静态的,并不能满足我们在实际开发中的需求,这时就需要我们通过自定义材质来达到特定的纹理效果。
自定义材质可以通过现有的内置材质派生,也可以使用Fabric和GLSL来自定义。但是在实际开发中,为了减少代码冗余,我们通常将常用的自定义材质封装成一个个Material材质类以便复用,下面将介绍如何封装一个自定义流动线材质类。
(1)实现代码
6_7_流动线.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>材质特效篇_流动线</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script src="./Build/js/jquery.min.js"></script>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<script>
Cesium.Ion.defaultAccessToken =
'你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: true, //是否显示动画工具
timeline: true, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
/* console.log('selectionIndicator',viewer.selectionIndicator);
$(".cesium-viewer-selectionIndicatorContainer").css('display','none'); */
viewer.scene.fxaa = false
viewer.scene.postProcessStages.fxaa.enabled = false;
var supportsImageRenderingPixelated = viewer.cesiumWidget._supportsImageRenderingPixelated;
if (supportsImageRenderingPixelated) {
var vtxf_dpr = window.devicePixelRatio;
while (vtxf_dpr >= 2.0) {
vtxf_dpr /= 2.0;
}
viewer.resolutionScale = vtxf_dpr;
}
//创建构造函数
function PolylineTrailLinkMaterialProperty(color, duration) {
this._definitionChanged = new Cesium.Event();
this._color = undefined;
this._colorSubscription = undefined;
this.color = color;
this.duration = duration;
this._time = (new Date()).getTime();
}
//Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
//Object.defineProperties(obj, props)
//obj:在其上定义或修改属性的对象 props:要定义其可枚举属性或修改的属性描述符的对象。
Object.defineProperties(PolylineTrailLinkMaterialProperty.prototype, {
isConstant: {
get: function () {
return false;
}
},
definitionChanged: {
get: function () {
return this._definitionChanged;
}
},
color: Cesium.createPropertyDescriptor('color')
});
PolylineTrailLinkMaterialProperty.prototype.getType = function (time) {
return 'PolylineTrailLink';
}
PolylineTrailLinkMaterialProperty.prototype.getValue = function (time, result) {
if (!Cesium.defined(result)) {
result = {};
}
result.color = Cesium.Property.getValueOrClonedDefault(this._color, time, Cesium.Color.WHITE, result.color);
result.image = Cesium.Material.PolylineTrailLinkImage;
result.time = (((new Date()).getTime() - this._time) % this.duration) / this.duration;
return result;
}
PolylineTrailLinkMaterialProperty.prototype.equals = function (other) {
return this === other || (other instanceof PolylineTrailLinkMaterialProperty && Property.equals(this._color,
other._color))
};
Cesium.PolylineTrailLinkMaterialProperty = PolylineTrailLinkMaterialProperty;
//纹理类型
Cesium.Material.PolylineTrailLinkType = 'PolylineTrailLink';
//纹理图片
Cesium.Material.PolylineTrailLinkImage = "./RasterImage/图片/color.png";
//纹理资源
Cesium.Material.PolylineTrailLinkSource =
"czm_material czm_getMaterial(czm_materialInput materialInput)\n\
{\n\
float time = czm_frameNumber/100.0;\n\
czm_material material = czm_getDefaultMaterial(materialInput);\n\
vec2 st = materialInput.st;\n\
vec4 colorImage = texture2D(image, vec2(fract(3.0*st.s - time), st.s));\n\
material.alpha = colorImage.a * color.a;\n\
material.diffuse = (colorImage.rgb+color.rgb)/2.0;\n\
return material;\n\
}";
//time越小,速度越慢
//colorImage控制纹理
//fract中 3.0是纹理个数 -time是逆时针 +time是顺时针
//alpha 透明度
//diffuse 颜色
/* "czm_material czm_getMaterial(czm_materialInput materialInput)\n\
{\n\
czm_material material = czm_getDefaultMaterial(materialInput);\n\
vec2 st = materialInput.st;\n\
vec4 colorImage = texture2D(image, vec2(fract(st.s - time), st.t));\n\
material.alpha = colorImage.a * color.a;\n\
material.diffuse = (colorImage.rgb+color.rgb)/2.0;\n\
return material;\n\
}" */
//添加自定义材质
Cesium.Material._materialCache.addMaterial(Cesium.Material.PolylineTrailLinkType, {
fabric: {
//纹理类型
type: Cesium.Material.PolylineTrailLinkType,
//传递给着色器的外部属性
uniforms: {
color: new Cesium.Color(0.0, 0.0, 0.0, 1),
image: Cesium.Material.PolylineTrailLinkImage,
time: 0
},
//纹理资源
source: Cesium.Material.PolylineTrailLinkSource
},
//是否透明
translucent: function (material) {
return true;
}
})
var line = viewer.entities.add({
name: 'PolylineTrailLink',
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray([
118.286419, 31.864436,
119.386419, 31.864436,
119.386419, 32.864436,
118.686419, 32.864436,
]),
width: 10,
//设置材质为自定义材质
material: new Cesium.PolylineTrailLinkMaterialProperty(
Cesium.Color.fromBytes(255, 0, 0).withAlpha(0.8),
/* 1000 */
),
}
});
viewer.flyTo(line)
</script>
</body>
</html>
下面封装一个自定义电子围栏材质类,能够对Entity墙体贴动态材质,实现电子围栏效果。封装自定义电子围栏材质类的流程和封装自定义流动线材质类的流程一样。
(1)实现代码
6_8_电子围栏.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>材质特效篇_电子围栏</title>
<script src="./Build/Cesium/Cesium.js"></script>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="cesiumContainer">
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
});
function DynamicWallMaterialProperty(color, duration) {
this._definitionChanged = new Cesium.Event();
this._color = undefined;
this._colorSubscription = undefined;
this.color = color;
this.duration = duration;
this._time = (new Date()).getTime();
}
Object.defineProperties(DynamicWallMaterialProperty.prototype, {
isConstant: {
get: function () {
return false;
}
},
definitionChanged: {
get: function () {
return this._definitionChanged;
}
},
color: Cesium.createPropertyDescriptor('color')
});
DynamicWallMaterialProperty.prototype.getType = function (time) {
return 'DynamicWall';
}
DynamicWallMaterialProperty.prototype.getValue = function (time, result) {
if (!Cesium.defined(result)) {
result = {};
}
result.color = Cesium.Property.getValueOrClonedDefault(this._color, time, Cesium.Color.WHITE, result.color);
result.image = Cesium.Material.DynamicWallImage;
result.time = (((new Date()).getTime() - this._time) % this.duration) / this.duration;
return result;
}
DynamicWallMaterialProperty.prototype.equals = function (other) {
return this === other || (other instanceof DynamicWallMaterialProperty && Property.equals(this._color, other._color))
};
Cesium.DynamicWallMaterialProperty = DynamicWallMaterialProperty;
Cesium.Material.DynamicWallType = 'DynamicWall';
Cesium.Material.DynamicWallImage = "./RasterImage/图片/color.png";//图片
Cesium.Material.DynamicWallSource =
`czm_material czm_getMaterial(czm_materialInput materialInput)
{
float time = czm_frameNumber/100.0;
czm_material material = czm_getDefaultMaterial(materialInput);
vec2 st = materialInput.st;
vec4 colorImage = texture2D(image, vec2(fract(1.0*st.t - time), st.t));
material.alpha = colorImage.a * color.a;
material.diffuse = (colorImage.rgb+color.rgb)/2.0;
return material;
}` //由上到下
//添加自定义材质
Cesium.Material._materialCache.addMaterial(Cesium.Material.DynamicWallType, {
fabric: {
//纹理类型
type: Cesium.Material.DynamicWallType,
//传递给着色器的外部属性
uniforms: {
color: new Cesium.Color(0.0, 0.0, 0.0, 1),
image: Cesium.Material.DynamicWallImage,
time: 0
},
//纹理资源
source: Cesium.Material.DynamicWallSource
},
//是否透明
translucent: function (material) {
return true;
}
})
var dynamicWall = viewer.entities.add({
wall: {
positions: Cesium.Cartesian3.fromDegreesArrayHeights([
118.286419, 31.864436, 20000.0,
119.386419, 31.864436, 20000.0,
119.386419, 32.864436, 20000.0,
118.286419, 32.864436, 20000.0,
118.286419, 31.864436, 20000.0,
]),
material: new Cesium.DynamicWallMaterialProperty(Cesium.Color.fromBytes(255, 200, 10).withAlpha(0.8), 3000),
}
})
viewer.flyTo(dynamicWall)
</script>
</body>
</html>
(2)结果显示
粒子系统表示三维计算机图形学中用于模拟一些特定模糊现象的技术,而这些现象用其他传统的渲染技术难以实现其真实感的物理运动规律。经常使用粒子系统模拟的现象有烟花、火焰、雨水及雪花等。简而言之,粒子系统就是一种用于模拟真实现象的图形技术,是由一个个的小图像集合而成的,从远处看会形成一个“复杂”的场景来模拟一些现象。
Cesium粒子系统不仅是多个小图像的直接集合,而且允许控制单个粒子的寿命、速度、位置等属性,也正是由于粒子的各种属性可以控制,才能够模拟各种复杂的场景。粒子系统效果在电影和电子游戏中应用广泛。下面使用粒子系统模拟烟花爆炸效果。
(1)实现代码
6_9_粒子烟花.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="Particle system fireworks.">
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases">
<title>材质特效篇_粒子烟花</title>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script src="./Build/Cesium/Cesium.js"></script>
</head>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<body>
<div id="cesiumContainer"></div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
shouldAnimate: true, //必须开启,自动播放动画
});
/* Cesium.Math.setRandomNumberSeed(315); */
//东北天到指定原点变换矩阵,将粒子系统从模型坐标转换为世界坐标
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
Cesium.Cartesian3.fromDegrees(114.39664, 30.52052)
);
//粒子发射器高度
const emitterInitialLocation = new Cesium.Cartesian3(0.0, 0.0, 100.0);
//粒子贴图
var particleCanvas;
//绘制图形
function getImage() {
if (!Cesium.defined(particleCanvas)) {
particleCanvas = document.createElement("canvas");
particleCanvas.width = 20;
particleCanvas.height = 20;
const context2D = particleCanvas.getContext("2d");
context2D.beginPath();
//圆心x 圆心y 半径 起始角度 终止角度 逆时针
context2D.arc(10, 10, 8, 0, Cesium.Math.TWO_PI, true);
context2D.closePath();
context2D.fillStyle = "rgba(255, 255, 255, 1)";
context2D.fill();
}
return particleCanvas;
}
/* var radar = viewer.entities.add({
rectangle: {
coordinates: Cesium.Rectangle.fromDegrees(114.40072, 30.51952, 114.40572, 30.52452),
material: new Cesium.ImageMaterialProperty({
//image: new Cesium.CallbackProperty(drawCanvas, false),
image:getImage(),
//transparent: true
}),
}
}); */
/* const minimumExplosionSize = 30.0; //最小爆炸尺寸
const maximumExplosionSize = 100.0; //最大爆炸尺寸 */
var particlePixelSize = new Cesium.Cartesian2(7.0, 7.0); //粒子大小
var burstNum = 400.0; //爆炸粒子个数
var lifetime = 10.0; //粒子系统发射粒子的时间
var numberOfFireworks = 20.0; //烟花个数
//创建烟花函数
function createFirework(offset, color, bursts) {
var position = Cesium.Cartesian3.add(
emitterInitialLocation,
offset,
new Cesium.Cartesian3()
);
//从发射位置创建表示转换的Matrix4
var emitterModelMatrix = Cesium.Matrix4.fromTranslation(position);
//随机设置烟花的生命周期
/* const size = Cesium.Math.randomBetween(
minimumExplosionSize,
maximumExplosionSize
);
const normalSize =
(size - minimumExplosionSize) /
(maximumExplosionSize - minimumExplosionSize);
const minLife = 0.3;
const maxLife = 1.0;
const life = normalSize * (maxLife - minLife) + minLife; */
viewer.scene.primitives.add(
new Cesium.ParticleSystem({
image: getImage(), //粒子贴图
startColor: color, //粒子在其生命初期的颜色
endColor: color.withAlpha(0.0),//粒子在其生命结束的颜色
//particleLife: life, //粒子生命周期
particleLife: 1, //粒子生命周期
speed: 100.0, //粒子扩散速度
imageSize: particlePixelSize, //粒子像素大小
emissionRate: 0, //每秒要发射的粒子数
emitter: new Cesium.SphereEmitter(0.1), //系统粒子发射器
bursts: bursts, //粒子爆炸,ParticleBurst 的数组
lifetime: lifetime, //粒子系统发射粒子的时间
//updateCallback: force, //每帧都要调用一次回调函数以更新粒子
modelMatrix: modelMatrix, //将粒子系统从模型转换为世界坐标的4x4转换矩阵。
emitterModelMatrix: emitterModelMatrix,//在粒子系统局部坐标系内转换粒子系统发射器的4x4转换矩阵
loop: true //粒子循环爆发
})
);
}
//粒子发射器偏移量范围
var xMin = -100.0;
var xMax = 100.0;
var yMin = -80.0;
var yMax = 100.0;
var zMin = -50.0;
var zMax = 50.0;
//设置随机颜色选项数组
var colorOptions = [
{
minimumRed: 0.75,
green: 0.0,
minimumBlue: 0.8,
alpha: 1.0,
},
{
red: 0.0,
minimumGreen: 0.75,
minimumBlue: 0.8,
alpha: 1.0,
},
{
red: 0.0,
green: 0.0,
minimumBlue: 0.8,
alpha: 1.0,
},
{
minimumRed: 0.75,
minimumGreen: 0.75,
blue: 0.0,
alpha: 1.0,
},
];
//创建烟花
for (let i = 0; i < numberOfFireworks; ++i) {
var x = Cesium.Math.randomBetween(xMin, xMax);
var y = Cesium.Math.randomBetween(yMin, yMax);
var z = Cesium.Math.randomBetween(zMin, zMax);
var offset = new Cesium.Cartesian3(x, y, z);
//使用提供的选项创建随机颜色
var color = Cesium.Color.fromRandom(
colorOptions[i % colorOptions.length]
);
//粒子爆炸,ParticleBurst 的数组,在周期时间发射粒子爆发
var bursts = [];
for (let j = 0; j < 3; ++j) {
bursts.push(
new Cesium.ParticleBurst({
time: Cesium.Math.nextRandomNumber() * lifetime, //粒子系统生命周期开始后以秒为单位的时间,将发生爆发
minimum: burstNum, //爆发中发射的最小粒子数。
maximum: burstNum, //爆发中发射的最大粒子数。
})
);
}
//传参,创建烟花
createFirework(offset, color, bursts);
}
viewer.scene.camera.setView({
destination:
Cesium.Cartesian3.fromDegrees(114.39664, 30.52052, 2000)
})
</script>
</body>
</html>
下面使用Cesium粒子系统模拟火焰燃烧效果。
(1)实现代码
6_10_粒子火焰.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="Particle system fireworks.">
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases">
<title>材质特效篇_粒子火焰</title>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script src="./Build/Cesium/Cesium.js"></script>
</head>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<body>
<div id="cesiumContainer"></div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
shouldAnimate: true, //必须开启 自动播放动画
});
// 加载飞机模型
var entity = viewer.entities.add({
model: {
uri: './3D格式数据/glb/Cesium_Air.glb',
minimumPixelSize: 64
},
position: Cesium.Cartesian3.fromDegrees(114.39264, 30.52252, 100)
});
//视角追踪模型
viewer.trackedEntity = entity;
//计算把粒子系统从模型坐标系转到世界坐标系指定原点的矩阵
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
Cesium.Cartesian3.fromDegrees(114.39264, 30.52252, 100)
);
//计算模型坐标系的平移矩阵
function computeEmitterModelMatrix() {
//定义粒子发射器的方向、俯仰角以及翻滚角
var hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, new Cesium.HeadingPitchRoll());
//定义一个由平移,旋转和缩放定义的仿射变换
var trs = new Cesium.TranslationRotationScale();
//火焰位置
//平移
trs.translation = Cesium.Cartesian3.fromElements(2.5, 4.0, 1.0, new Cesium.Cartesian3());
//旋转
trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, new Cesium.Quaternion());
return Cesium.Matrix4.fromTranslationRotationScale(trs, new Cesium.Matrix4());
}
var particleSystem = new Cesium.ParticleSystem({
image: './RasterImage/图片/fire.png',
startScale: 1.0, //开始比例
endScale: 4.0, //结束比例
particleLife: 1.0, //粒子生命周期
speed: 5.0, //粒子速度
imageSize: new Cesium.Cartesian2(20, 20), //粒子图形尺寸
emissionRate: 5.0, //每秒发射粒子个数
lifetime: 16.0, //粒子系统发射粒子的时间
modelMatrix: modelMatrix, //将粒子系统从模型转换为世界坐标的4x4转换矩阵
emitterModelMatrix: computeEmitterModelMatrix() //在粒子系统局部坐标系内转换粒子系统发射器的4x4转换矩阵
})
viewer.scene.primitives.add(particleSystem);
</script>
</body>
</html>
常见的粒子特效还有雨、雪等粒子天气特效,下面使用Cesium粒子系统模拟天气特效,包括下雨天与下雪天两种情况。
(1)实现代码
6_11_粒子天气.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="Particle system fireworks.">
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases">
<title>材质特效篇_粒子天气</title>
<link rel="stylesheet" href="./Build/Cesium/Widgets/widgets.css">
<script src="./Build/Cesium/Cesium.js"></script>
</head>
<style>
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.toolbar {
position: absolute;
top: 10px;
left: 20px;
background-color: rgb(0, 0, 0, 0);
}
</style>
<body>
<div id="cesiumContainer"></div>
<div class="toolbar">
<select id="dropdown" onchange="change()">
<option value="snow">雪</option>
<option value="rain">雨</option>
<option value="null">null</option>
</select>
</div>
<script>
Cesium.Ion.defaultAccessToken = '你的token';
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: false, //是否显示动画工具
timeline: false, //是否显示时间轴工具
fullscreenButton: false, //是否显示全屏按钮工具
shouldAnimate: true, //必须开启
terrainProvider: Cesium.createWorldTerrain(),
});
//粒子特效位置
var position = new Cesium.Cartesian3.fromDegrees(114.39664, 30.52052, 2000);
var modelMatrix = new Cesium.Matrix4.fromTranslation(position)
//模拟下雪天粒子特效常量定义
const snowRadius = 100000.0; //下雪的范围半径
const minimumSnowImageSize = new Cesium.Cartesian2(10, 10); //雪花最小尺寸
const maximumSnowImageSize = new Cesium.Cartesian2(20, 20); //雪花最大尺寸
//创建Cartesian3对象,用于在回调函数中实时更新粒子位置
var snowGravityScratch = new Cesium.Cartesian3();
//粒子更新回调函数
function snowUpdate (particle) {
//计算提供的笛卡尔坐标系的标准化形式
Cesium.Cartesian3.normalize(
particle.position, //要标准化的笛卡尔坐标
snowGravityScratch //结果存储对象
);
//将提供的笛卡尔分量乘以标准的标量
Cesium.Cartesian3.multiplyByScalar(
snowGravityScratch, //要缩放的笛卡尔坐标
//要与之相乘的标量,负值代表粒子位置下降即粒子从上往下落
Cesium.Math.randomBetween(-30.0, -300.0),
snowGravityScratch //结果存储对象
);
//粒子位置根据snowGravityScratch变化
Cesium.Cartesian3.add(
particle.position,
snowGravityScratch,
particle.position
);
};
// 雨
const rainRadius = 100000.0; //下雨的范围半径
const rainImageSize = new Cesium.Cartesian2(20, 35); //15,30分别代表宽高
var rainGravityScratch = new Cesium.Cartesian3();
//粒子更新回调函数
function rainUpdate (particle) {
//计算提供的笛卡尔坐标系的标准化形式
Cesium.Cartesian3.normalize(
particle.position, //要标准化的笛卡尔坐标
rainGravityScratch //结果存储对象
);
//将提供的笛卡尔分量乘以标准的标量
Cesium.Cartesian3.multiplyByScalar(
rainGravityScratch, //要缩放的笛卡尔坐标
-1000.0, //要与之相乘的标量,雨比雪下落速度快的多 所以这个值负的多点
rainGravityScratch //结果存储对象
);
//粒子位置根据rainGravityScratch变化
Cesium.Cartesian3.add(
particle.position,
rainGravityScratch,
particle.position
);
};
//粒子系统-雪配置项
var snowOption = {
modelMatrix: modelMatrix, //将粒子系统从模型转换为世界坐标的4x4转换矩阵。
lifetime: 15.0, //粒子系统发射粒子的时间(以秒为单位)
emitter: new Cesium.SphereEmitter(snowRadius), //该系统的粒子发射器
startScale: 0.5, //在粒子寿命开始时应用于粒子图像的初始比例
endScale: 1.0, //在粒子寿命结束时应用于粒子图像的最终比例。
image: "./RasterImage/图片/snowflake_particle.png", //粒子贴图
emissionRate: 7000.0, //每秒要发射的粒子数
startColor: Cesium.Color.WHITE.withAlpha(0.0), //粒子在其生命初期的颜色。
endColor: Cesium.Color.WHITE.withAlpha(1.0), //粒子寿命结束时的颜色。
minimumImageSize: minimumSnowImageSize, //设置宽度的最小范围,以高度为单位,在该范围上可以随机缩放粒子图像的尺寸(以像素为单位)
maximumImageSize: maximumSnowImageSize, //设置最大宽度边界,以高度为单位,在该边界以下可以随机缩放粒子图像的尺寸(以像素为单位)
updateCallback: snowUpdate, //每帧都要调用一次回调函数以更新粒子
}
//粒子系统-雨配置项
var rainOption = {
modelMatrix: modelMatrix,//将粒子系统从模型转换为世界坐标的4x4转换矩阵。
lifetime: 15.0,//粒子系统发射粒子的时间(以秒为单位)
emitter: new Cesium.SphereEmitter(rainRadius),//该系统的粒子发射器
startScale: 1.0,//在粒子寿命开始时应用于粒子图像的初始比例
endScale: 0.0,//在粒子寿命结束时应用于粒子图像的最终比例。
image: "./RasterImage/图片/circular_particle.png",//粒子贴图
emissionRate: 9000.0,//每秒要发射的粒子数
startColor: new Cesium.Color(1, 1, 1, 0.0),//粒子在其生命初期的颜色。
endColor: new Cesium.Color(1.0, 1.0, 1.0, 0.98),//粒子寿命结束时的颜色。
imageSize: rainImageSize,//粒子贴图尺寸
updateCallback: rainUpdate,//每帧都要调用一次回调函数以更新粒子
}
//默认下雪天
viewer.scene.primitives.add(new Cesium.ParticleSystem(snowOption));
//下拉框回调函数
var dropdown = document.getElementById('dropdown');
function change() {
switch (dropdown.value) {
case 'snow':
viewer.scene.primitives.removeAll();
viewer.scene.primitives.add(new Cesium.ParticleSystem(snowOption));
break;
case 'rain':
viewer.scene.primitives.removeAll();
viewer.scene.primitives.add(new Cesium.ParticleSystem(rainOption));
break;
case 'null':
viewer.scene.primitives.removeAll();
break;
default:
break;
}
}
//设置相机视角
/* viewer.scene.camera.setView({
destination:
Cesium.Cartesian3.fromDegrees(114.39664, 30.40052, 10000),
orientation: {
heading: 4.731089976107251,
pitch: -0.32003481981370063,
},
}) */
//设置相机初始位置
viewer.scene.camera.setView({
destination: new Cesium.Cartesian3(-2318006.190591779, 5016113.738321363,3239729.8052793955),
orientation: {
heading: 5.0433812878480655,
pitch: -0.25943108890985744,
roll: 0.000002292722656171975
},
duration: 0.0
});
</script>
</body>
</html>
(2)结果显示
cesium文章涉及数据
参考资料:
[1] 郭明强. 《WebGIS之Cesium三维软件开发》; 2023-04-01 [accessed 2024-01-27].
[2] WaqarLeaver. Cesium开源water材质和粒子效果示例代码研究; 2021-05-30 [accessed 2024-01-27].
[3] GIS兵墩墩. C2——cesium流动特效; 2020-11-04 [accessed 2024-01-27].
[4] 那那那那那么长的哲尘. Cesium实现流动线/动态纹理; 2024-01-11 [accessed 2024-01-27].