前言
Cesium是一个强大的开源JavaScript库,它被广泛用于在Web浏览器中创建3D地理空间应用程序。Cesium提供了进行可视域分析的功能,这可以帮助用户确定从给定位置可以看到哪些区域。可视域分析考虑了地形、建筑物以及其他自然或人造物体对视线的遮挡。这项技术在军事和城市规划等领域有着重要应用,如军事作战中的掩体定位和城市规划中的最佳视觉观赏点选择。由于3D模型能提供更真实的视觉效果,因此在3D模型上进行可视域分析通常比在2D图像上进行更具优势,可以更好地帮助用户理解和优化设计
一、可视域的绘制
import ViewShed from './viewshed.js'
import PlotDrawTip from "@/utils/PlotDrawTip"
export default class Draw {
constructor(viewer, handler) {
this.viewer = viewer;
this.handler = handler
this.startPosition = undefined
this.i = 0
this.drawViewshedEntity = null
}
//激活
activate() {
this.deactivate();
this.registerEvents(); //注册鼠标事件
this.viewer.enableCursorStyle = true;
this.viewer._element.style.cursor = 'crosshair';
this.viewer.enableCursorStyle = true;
this.viewer._element.style.cursor = 'crosshair';
this.plotDrawTip = new PlotDrawTip(this.viewer);
this.plotDrawTip.setContent(['左键点击开始绘制'])
}
//禁用
deactivate() {
this.i = 0
this.drawEntity = undefined;
this.endPosition = undefined
this.startPosition = undefined
this.viewer._element.style.cursor = 'default';
this.viewer.enableCursorStyle = true;
this.unRegisterEvents();
if (!this.plotDrawTip) return;
this.plotDrawTip.remove();
this.plotDrawTip = undefined;
}
//注册鼠标事件
registerEvents() {
this.leftClickEvent();
this.mouseMoveEvent();
}
leftClickEvent() {
//单击鼠标左键画点
this.handler.setInputAction(e => {
this.i++
//第一次点击
if (this.i == 1) {
let startPosition = this.viewer.scene.pickPosition(e.position);
if (startPosition && Cesium.defined(startPosition)) {
this.startPosition = startPosition
this.plotDrawTip.setContent(['左键点击结束点,完成绘制'])
this.drawViewshedEntity = new ViewShed(this.viewer, {
viewPosition: this.startPosition,
viewPositionEnd: this.startPosition,
})
}
}
//第二次点击
if (this.i == 2) {
let endPosition = this.viewer.scene.pickPosition(e.position);
if (!endPosition && Cesium.defined(endPosition)) {
this.drawViewshedEntity.updatePosition(this.endPosition)
this.drawViewshedEntity.update()
}
this.deactivate()
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
mouseMoveEvent() {
this.handler.setInputAction(e => {
this.endPosition = this.viewer.scene.pickPosition(e.endPosition)
if (!this.endPosition) return
this.plotDrawTip && this.plotDrawTip.updatePosition(this.endPosition);
if (this.i == 1) {
if (this.drawViewshedEntity) {
this.drawViewshedEntity.updatePosition(this.endPosition)
this.drawViewshedEntity.update()
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
//解除鼠标事件
unRegisterEvents() {
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
}
二、可视域的viewshed类
import glsl from './glsl';
/**
* @param {Cesium.Viewer} viewer Cesium三维视窗。
* @param {Object} options 选项。
* @param {Cesium.Cartesian3} options.viewPosition 观测点位置。
* @param {Cesium.Cartesian3} options.viewPositionEnd 最远观测点位置(如果设置了观测距离,这个属性可以不设置)。
* @param {Number} options.viewDistance 观测距离(单位`米`,默认值100)。
* @param {Number} options.viewHeading 航向角(单位`度`,默认值0)。
* @param {Number} options.viewPitch 俯仰角(单位`度`,默认值0)。
* @param {Number} options.horizontalViewAngle 可视域水平夹角(单位`度`,默认值90)。
* @param {Number} options.verticalViewAngle 可视域垂直夹角(单位`度`,默认值60)。
* @param {Cesium.Color} options.visibleAreaColor 可视区域颜色(默认值`绿色`)。
* @param {Cesium.Color} options.invisibleAreaColor 不可视区域颜色(默认值`红色`)。
* @param {Boolean} options.enabled 阴影贴图是否可用。
* @param {Boolean} options.softShadows 是否启用柔和阴影。
* @param {Boolean} options.size 每个阴影贴图的大小。
*/
class ViewShed {
constructor(viewer, options) {
this.viewer = viewer;
this.viewPosition = options.viewPosition; //开始坐标
this.viewPositionEnd = options.viewPositionEnd; //结束坐标
this.viewDistance = this.viewPositionEnd ? Cesium.Cartesian3.distance(this.viewPosition, this.viewPositionEnd) : (options.viewDistance || 100.0); //观测距离
this.viewHeading = this.viewPositionEnd ? this.getHeading(this.viewPosition, this.viewPositionEnd) : (options.viewHeading || 0.0);
this.viewPitch = this.viewPositionEnd ? this.getPitch(this.viewPosition, this.viewPositionEnd) : (options.viewPitch || 0.0);
this.horizontalViewAngle = options.horizontalViewAngle || 90.0; //可视域的水平夹角
this.verticalViewAngle = options.verticalViewAngle || 60.0; //可视域的垂直夹角
this.visibleAreaColor = options.visibleAreaColor || Cesium.Color.GREEN;
this.invisibleAreaColor = options.invisibleAreaColor || Cesium.Color.RED;
this.enabled = (typeof options.enabled === "boolean") ? options.enabled : true;
this.softShadows = (typeof options.softShadows === "boolean") ? options.softShadows : true;
this.size = options.size || 2048;
this.isSketch = true //视锥线,是否显示
}
add() {
this.createLightCamera();
this.createShadowMap();
this.drawSketch();
this.createPostStage();
}
update() {
this.clear();
this.add();
if(!this.isSketch){
this.clearSketch()
}
}
/**
* @method 更新终点坐标,从而实时更新绘制的实体的方向和半径
*
*/
updatePosition(viewPositionEnd) {
this.viewPositionEnd = viewPositionEnd
this.viewDistance = Cesium.Cartesian3.distance(this.viewPosition, this.viewPositionEnd) //观测距离
this.viewHeading = this.getHeading(this.viewPosition, this.viewPositionEnd)
this.viewPitch = this.getPitch(this.viewPosition, this.viewPositionEnd)
}
clear() {
if (this.sketch) {
this.viewer.entities.remove(this.sketch);
this.sketch = null;
}
if (this.frustumOutline) {
this.viewer.scene.primitives.destroy();
this.frustumOutline = null;
}
if (this.postStage) {
this.viewer.scene.postProcessStages.remove(this.postStage);
this.postStage = null;
}
}
clearSketch(){
if (this.sketch) {
this.viewer.entities.remove(this.sketch);
this.sketch = null;
}
}
/**
* @method 创建相机
*/
createLightCamera() {
this.lightCamera = new Cesium.Camera(this.viewer.scene);
this.lightCamera.position = this.viewPosition;
this.lightCamera.frustum.near = this.viewDistance * 0.001;
this.lightCamera.frustum.far = this.viewDistance;
const hr = Cesium.Math.toRadians(this.horizontalViewAngle);
const vr = Cesium.Math.toRadians(this.verticalViewAngle);
const aspectRatio =
(this.viewDistance * Math.tan(hr / 2) * 2) / (this.viewDistance * Math.tan(vr / 2) * 2);
this.lightCamera.frustum.aspectRatio = aspectRatio;
if (hr > vr) {
this.lightCamera.frustum.fov = hr;
} else {
this.lightCamera.frustum.fov = vr;
}
this.lightCamera.setView({
destination: this.viewPosition,
orientation: {
heading: Cesium.Math.toRadians(this.viewHeading || 0),
pitch: Cesium.Math.toRadians(this.viewPitch || 0),
roll: 0
}
});
}
/**
* @method 创建阴影贴图
*/
createShadowMap() {
this.shadowMap = new Cesium.ShadowMap({
context: (this.viewer.scene).context,
lightCamera: this.lightCamera,
enabled: this.enabled,
isPointLight: true,
pointLightRadius: this.viewDistance,
cascadesEnabled: false,
size: this.size,
softShadows: this.softShadows,
normalOffset: false,
fromLightSource: false
});
this.viewer.scene.shadowMap = this.shadowMap;
}
/**
* @method 创建PostStage
* 导入的glsl是做片元着色的,具体怎么实现的我也不太明白
*/
createPostStage() {
const fs = glsl
const postStage = new Cesium.PostProcessStage({
fragmentShader: fs,
uniforms: {
shadowMap_textureCube: () => {
this.shadowMap.update(Reflect.get(this.viewer.scene, "_frameState"));
return Reflect.get(this.shadowMap, "_shadowMapTexture");
},
shadowMap_matrix: () => {
this.shadowMap.update(Reflect.get(this.viewer.scene, "_frameState"));
return Reflect.get(this.shadowMap, "_shadowMapMatrix");
},
shadowMap_lightPositionEC: () => {
this.shadowMap.update(Reflect.get(this.viewer.scene, "_frameState"));
return Reflect.get(this.shadowMap, "_lightPositionEC");
},
shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness: () => {
this.shadowMap.update(Reflect.get(this.viewer.scene, "_frameState"));
const bias = this.shadowMap._pointBias;
return Cesium.Cartesian4.fromElements(
bias.normalOffsetScale,
this.shadowMap._distance,
this.shadowMap.maximumDistance,
0.0,
new Cesium.Cartesian4()
);
},
shadowMap_texelSizeDepthBiasAndNormalShadingSmooth: () => {
this.shadowMap.update(Reflect.get(this.viewer.scene, "_frameState"));
const bias = this.shadowMap._pointBias;
const scratchTexelStepSize = new Cesium.Cartesian2();
const texelStepSize = scratchTexelStepSize;
texelStepSize.x = 1.0 / this.shadowMap._textureSize.x;
texelStepSize.y = 1.0 / this.shadowMap._textureSize.y;
return Cesium.Cartesian4.fromElements(
texelStepSize.x,
texelStepSize.y,
bias.depthBias,
bias.normalShadingSmooth,
new Cesium.Cartesian4()
);
},
camera_projection_matrix: this.lightCamera.frustum.projectionMatrix,
camera_view_matrix: this.lightCamera.viewMatrix,
helsing_viewDistance: () => {
return this.viewDistance;
},
helsing_visibleAreaColor: this.visibleAreaColor,
helsing_invisibleAreaColor: this.invisibleAreaColor,
}
});
this.postStage = this.viewer.scene.postProcessStages.add(postStage);
}
/**
* @method 创建视锥线
*/
drawFrustumOutline() {
const scratchRight = new Cesium.Cartesian3();
const scratchRotation = new Cesium.Matrix3();
const scratchOrientation = new Cesium.Quaternion();
const position = this.lightCamera.positionWC;
const direction = this.lightCamera.directionWC;
const up = this.lightCamera.upWC;
let right = this.lightCamera.rightWC;
right = Cesium.Cartesian3.negate(right, scratchRight);
let rotation = scratchRotation;
Cesium.Matrix3.setColumn(rotation, 0, right, rotation);
Cesium.Matrix3.setColumn(rotation, 1, up, rotation);
Cesium.Matrix3.setColumn(rotation, 2, direction, rotation);
let orientation = Cesium.Quaternion.fromRotationMatrix(rotation, scratchOrientation);
let instance = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumOutlineGeometry({
frustum: this.lightCamera.frustum,
origin: this.viewPosition,
orientation: orientation
}),
id: Math.random().toString(36).substr(2),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.YELLOWGREEN
),
show: new Cesium.ShowGeometryInstanceAttribute(true)
}
});
this.frustumOutline = this.viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: [instance],
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: false
})
})
);
}
/**
* @method 创建视网
* 在实时绘制椭球实体时,其实不是一直创建entity,而是改变实体的方向(orientation)和改变椭球的半径(radii)
*/
drawSketch() {
this.sketch = this.viewer.entities.add({
name: 'sketch',
position: this.viewPosition,
orientation: new Cesium.CallbackProperty(() => Cesium.Transforms.headingPitchRollQuaternion( //实体的方向
this.viewPosition,
Cesium.HeadingPitchRoll.fromDegrees(this.viewHeading - 90.0, this.viewPitch, 0.0))),
ellipsoid: { //椭球的半径
radii: new Cesium.CallbackProperty(() => new Cesium.Cartesian3(
this.viewDistance,
this.viewDistance,
this.viewDistance)),
innerRadii: new Cesium.Cartesian3(2.0, 2.0, 2.0), //椭球内部的半径
minimumClock: Cesium.Math.toRadians(-this.horizontalViewAngle / 2), //椭圆形的最小时钟角度
maximumClock: Cesium.Math.toRadians(this.horizontalViewAngle / 2), //椭球的最大时钟角度
minimumCone: Cesium.Math.toRadians(this.verticalViewAngle + 7.75), //椭圆形的最小圆锥角
maximumCone: Cesium.Math.toRadians(180 - this.verticalViewAngle - 7.75), //椭球的最大圆锥角
fill: false, //椭圆是否填充所提供的的材料
outline: true, //是否勾勒出椭圆形
subdivisions: 256, //每个轮廓环的样本数,确定曲率的粒度
stackPartitions: 64, //堆栈数的属性
slicePartitions: 64, //径向切片数量的属性
outlineColor: Cesium.Color.YELLOWGREEN, //轮廓的颜色
}
});
}
/**
* @method 获取偏航角
*/
getHeading(fromPosition, toPosition) {
let finalPosition = new Cesium.Cartesian3();
let matrix4 = Cesium.Transforms.eastNorthUpToFixedFrame(fromPosition);
Cesium.Matrix4.inverse(matrix4, matrix4);
Cesium.Matrix4.multiplyByPoint(matrix4, toPosition, finalPosition);
Cesium.Cartesian3.normalize(finalPosition, finalPosition);
return Cesium.Math.toDegrees(Math.atan2(finalPosition.x, finalPosition.y));
}
/**
* @method 获取俯仰角
*/
getPitch(fromPosition, toPosition) {
let finalPosition = new Cesium.Cartesian3();
let matrix4 = Cesium.Transforms.eastNorthUpToFixedFrame(fromPosition);
Cesium.Matrix4.inverse(matrix4, matrix4);
Cesium.Matrix4.multiplyByPoint(matrix4, toPosition, finalPosition);
Cesium.Cartesian3.normalize(finalPosition, finalPosition);
return Cesium.Math.toDegrees(Math.asin(finalPosition.z));
}
}
export default ViewShed;
三、保存至数据库
参考Geojson数据格式如下
{
"type": "viewshed",
"relevantId": "a4b43090-5927-4b21-b2d8-fb0db94f071f",
"name": "可视域",
"properties": {
"destination": null,
"orientation": null,
"style": {
"viewPosition": {
"x": -2583989.8924737135,
"y": 4663987.94986121,
"z": 3488423.3463035254
},
"viewPositionEnd": {
"x": -2583989.677120281,
"y": 4663953.226079538,
"z": 3488471.546363341
},
"viewDistance": 59.405666033692356,
"viewHeading": 16.268876174425284,
"horizontalViewAngle": 90,
"verticalViewAngle": 60,
"visibleAreaColor": "rgba(0, 255, 0, 1)",
"invisibleAreaColor": "rgba(255 ,0, 0, 1)",
"enabled": true,
"size": 2048,
"isSketch": true,
"viewPitch": 1.0223964959573864
},
"entitiesData": {
"type": "Point",
"coordinates": [
118.98777411497602,
33.37118801269521,
20.894417597967987
]
}
},
"classId": "2",
"layerId": "16fa9a502fbe4dc18220de4ab2d6960a"
}
三、可视域回显
这块可根据自己的业务编写
viewshedInit(ele) {
const model = ele
const style = model.properties.style
this.drawViewshedEntity = new ViewShed(this.viewer, {
viewPosition: style.viewPosition,
viewDistance: style.viewDistance,
viewPositionEnd: style.viewPositionEnd,
horizontalViewAngle: style.horizontalViewAngle,
verticalViewAngle: style.verticalViewAngle,
viewPitch: style.viewPitch,
viewHeading: style.viewHeading,
visibleAreaColor: style.visibleAreaColor,
invisibleAreaColor: style.invisibleAreaColor,
isSketch: style.isSketch
})
this.drawViewshedEntity.update()
//保存到全局数组中方便删除
global.toolsList.push({ relevantId: model.relevantId, viewShed: this.drawViewshedEntity})
}
效果:
(1)倾斜摄影
(2)地形上
注意:如果出现地形上只能出现一个颜色的,添加
this.viewer.scene.globe.shadows = Cesium.ShadowMode.ENABLED
地形接收和投射阴影
总结
这是完整的可视域实现方式,也可以根据业务场景改动