前言
在Cesium的学习中,学会读文档十分重要!!!在这里附上中文文档Cesium中文文档1.95。
在Cesium中,动态水体可以通过primitive来实现。因为水体有反射效果,流动效果,所以我们可以通过修改primitive的材质来模拟。
一、primitive介绍
在Cesium中,primitive是创建和操作三维几何形状的底层API。我们可以通过primitive修改物体的形状和材质,达到更加真实的效果。它由两部分组成:几何形状(Geometry)和外观(Appearance)。几何形状定义了Primitive的结构,如三角形、多边形、折线、点、标签等;外观则定义了Primitive的着色或渲染,包括GLSL(OpenGL着色语言)顶点着色器和片段着色器,以及渲染状态。
1.1、primitive与entity对比
在之前的笔记点线面绘制中,我们学习了用entity来绘制点线面,以及绘制小车,那么同样是绘制图形,primitive和entity的区别是什么呢?
简单来说,Entity是高级API,封装了常见地理实体(如点、线、面),适合快速开发,但性能开销较大。 Primitive是底层API,直接操作几何和材质,灵活性高,适合处理大规模数据或自定义渲染逻辑,性能更优。
1.2、geometry(几何)属性
在primitive中,geometry属性定义了primitive对象的形状,这个属性的数据类型是GeometryInstance,这个对象也有自己的属性,其中一个也叫geometry,geometry属性是GeometryInstance中的几何图形,利用modelMatrix属性来进行旋转,缩放等操作。geometry属性的类型是Geometry和GeometryFactory。
Geometry对象才是真正控制了primitive的形状,其中的attributes属性构成了几何的顶点,我们可以通过构造GeometryAttributes对象及其Position,color等属性来控制几何的生成。
但这是一种偏底层的写法,如果是规则的几何体,我们还可以通过Cesium自带的一些对象来进行更简单的生成。如PolygonGeometry,RectangleGeometry等,这些可以看成是Geometry对象的继承,可以当成一个Geometry来使用。
比如说,我们想要绘制多边形范围的水体,就可以用PositionGeometry对象来绘制水体的几何形状。其中,polygonHierarchy是指中间带孔洞的多边形对象。height代表其多边形距离椭球表面的高度,extrudedHeight代表多边形拉伸的高度。
接下来我们看一下GeometryInstance的另一个重要属性attributes,它的类型是obj,(这里的翻译不太好),也就是说我们可以自己构造对象,只要里面有color等可以被识别的属性就可以了。(这里确实有点绕)
这里我写了一个GeometryInstance的例子。
const instance = new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(-100.0, 20.0, -90.0, 30.0),
height: 1000
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
}
});
1.3、Appearance(外观)
接下来是primitive的另一个重要属性appearance。它可以材质与着色器,控制渲染效果(颜色、纹理、透明度等)。我们通过其绘制水体的材质。里面不仅有cesium写好的材质,还可以自定义着色器通过vertexShader
和fragmentShader
实现高级效果(如动态颜色、高光)。我们通过其属性material来定义水体效果。
那么,该如何构造一个Material对象呢?这里的文档也写的不太好,大概的意思是里面有有一个fabric属性,这个属性是一个obj(json格式),下面两张图里面的名称(如color,water,这里可以看一下英文文档)是fabric对象的type属性里面的内容,fabric对象还有一个属性是uniforms,里面内容就是下面两张图里面的剩下的属性的属性。
比如,这里是water的写法。里面有一个normalMap属性,是用来放法线贴图等,这里使用的是路径的形式引入。大家可以用自己的好看的法线贴图,也可以用我这个。
const water = new Cesium.Material({
fabric: {
type: "Water",
uniforms: {
baseWaterColor: new Cesium.Color(64 / 255, 157 / 255, 253 / 255, 0.6),
normalMap: "/waterNormals.jpg",
frequency: 1000.0,
animationSpeed: 0.1,
amplitude: 10,
specularIntensity: 8
}
}
})
有了这些前置知识,我们就可以开始写一个水体的primitive了。
二、水体的绘制
2.1、多边形的绘制
和绘制大面积的积云一样,我们可以先绘制一个多边形,将其坐标通过回调函数来绘制水体。绘制多边形调用工具在之前的文章里讲过,这里我们就直接调用了。在绘制前,我移除了过去绘制的多边形和水体,大家可以根据需要修改。
这里因为我们要等绘制完多边形再绘制水体,所以我们用回调函数来进行水体的绘制
const drawEntityGraphic = new DrawEntityGraphic(viewer);
let water=null;
drawEntityGraphic.activate('polygon', (points) => {//回调函数
drawEntityGraphic.clearAll();
polygonPoints.value = points; // 保存顶点坐标
// 移除旧的水体
if (water) {
viewer.scene.primitives.remove(water);
}
// 创建新水体
water = createWaterPrimitive(points, waterHeight.value);
viewer.scene.primitives.add(water);
viewer.scene.globe.depthTestAgainstTerrain = true;
});
2.2、水体的绘制
接下来,就是完善回调函数的createWaterPrimitive函数了,这里我们要根据多边形的坐标来绘制水体的几何形状。
const createWaterPrimitive = (points, height = 2300) => {
return new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(points),
height: 0,
extrudedHeight: Number(height),
})
}),
appearance: new Cesium.EllipsoidSurfaceAppearance({
material: new Cesium.Material({
fabric: {
type: "Water",
uniforms: {
baseWaterColor: new Cesium.Color(64 / 255, 157 / 255, 253 / 255, 0.6),
normalMap: "/waterNormals.jpg",
frequency: 1000.0,
animationSpeed: 0.1,
amplitude: 10,
specularIntensity: 8
}
}
})
})
});
};
我这里先给了一个默认高度2300,大家可以根据需要修改。
2.3、高度的修改
在实际的应用中,我们可能要模拟洪水等突发情况,因此水的高度应该是动态更新的,这里,我的界面中写了一个绘制多边形的按钮,修改高度的输入框,其绑定了waterHeight属性。为了模拟动态更新的效果,这里我通过watch来监视waterHeight的变化,当waterHeight变化时,现将原来的水体删除,再新建一个新的水体,以达到一个动态更新的效果。
watch(waterHeight, (newVal) => {
if (!polygonPoints.value) return;
// 移除旧Primitive
if (water) {
viewer.scene.primitives.remove(water);
}
// 使用新高度创建
water = createWaterPrimitive(polygonPoints.value, newVal);
viewer.scene.primitives.add(water);
});
2.4、资源释放
还是和以前一样,我们要记得释放不用的资源,不然会影响性能。我这里还是界面切换的时候和手动的按钮,会触发clearAll
const clearAll = () => {
drawEntityGraphic.clearAll();
if (water) {
viewer.scene.primitives.remove(water);
}
polygonPoints.value = null;
waterHeight.value = null;
};
2.5、最终代码
最后,完善后的代码如下。
<template>
<el-button :icon="Location" @click="drawWater()"></el-button>
<el-input v-model="waterHeight" placeholder="请输入高度" style="margin: 10px auto;"></el-input>
<el-button @click="clearAll()">清除</el-button>
</template>
<script setup>
import useStore from '@/stores';
import { Location } from '@element-plus/icons-vue';
import * as Cesium from 'cesium';
import { onUnmounted, ref, watch } from 'vue';
import DrawEntityGraphic from '../../utils/drawEntityGraphic';
const { viewer } = useStore();
const drawEntityGraphic = new DrawEntityGraphic(viewer);
let water = null;
const waterHeight = ref();
const polygonPoints = ref(null); // 保存多边形顶点
const createWaterPrimitive = (points, height = 2300) => {
return new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(points),
height: 0,
extrudedHeight: Number(height),
})
}),
appearance: new Cesium.EllipsoidSurfaceAppearance({
material: new Cesium.Material({
fabric: {
type: "Water",
uniforms: {
baseWaterColor: new Cesium.Color(64 / 255, 157 / 255, 253 / 255, 0.6),
normalMap: "/waterNormals.jpg",
frequency: 1000.0,
animationSpeed: 0.1,
amplitude: 10,
specularIntensity: 8
}
}
})
})
});
};
const drawWater = () => {
drawEntityGraphic.activate('polygon', (points) => {
drawEntityGraphic.clearAll();
polygonPoints.value = points; // 保存顶点坐标
// 移除旧的水体
if (water) {
viewer.scene.primitives.remove(water);
}
// 创建新水体
water = createWaterPrimitive(points, waterHeight.value);
viewer.scene.primitives.add(water);
viewer.scene.globe.depthTestAgainstTerrain = true;
});
};
watch(waterHeight, (newVal) => {
if (!polygonPoints.value) return;
// 移除旧Primitive
if (water) {
viewer.scene.primitives.remove(water);
}
// 使用新高度创建
water = createWaterPrimitive(polygonPoints.value, newVal);
viewer.scene.primitives.add(water);
});
const clearAll = () => {
drawEntityGraphic.clearAll();
if (water) {
viewer.scene.primitives.remove(water);
}
polygonPoints.value = null;
waterHeight.value = null;
};
onUnmounted(() => {
clearAll();
})
</script>
<style scoped></style>
三、性能优化
在cesium中,当primitive很多时,频繁进行删除和添加也会影响其性能。因此,有没有别的方法来提高性能呢?不知道大家还记不记得一开始的时候提到了modelMatrix 属性,可以对其进行旋转释放的操作。因此我们可以直接对其平移,此时输入框内输入的就变成了平移的距离了。
const waterHeight = ref(2300);
const polygonPoints = ref(null);
let water = null;
// 创建固定几何 + 动态矩阵
const createWaterPrimitive = (points) => {
return new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(points),
height: 0, // 底面高度固定
extrudedHeight: 1 // 微小高度差维持几何结构
})
}),
appearance: new Cesium.EllipsoidSurfaceAppearance({
/* 材质配置保持不变 */
}),
modelMatrix: Cesium.Matrix4.IDENTITY // 初始单位矩阵
});
};
// 绘制水面
const drawWater = () => {
drawEntityGraphic.activate('polygon', (points) => {
drawEntityGraphic.clearAll();
polygonPoints.value = points;
// 移除旧对象(如果存在)
if(water) viewer.scene.primitives.remove(water);
// 创建新Primitive并设置初始高度
water = createWaterPrimitive(points);
viewer.scene.primitives.add(water);
updateHeight(); // 应用当前高度值
});
};
// 核心优化点:仅更新矩阵
const updateHeight = () => {
if (!water || !polygonPoints.value) return;
// 通过矩阵平移控制高度
const translation = Cesium.Cartesian3.fromElements(
0,
0,
Number(waterHeight.value)
);
water.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
};
最终效果如下。如果大家喜欢我的文章的话,点一个免费的赞和关注吧!