Cesium 實現可視域分析

路遥_13發表於2024-07-03
	*前言:嘗試了網上好多個版本的可視域分析,感覺都有一些問題,我這個也可能不是最完美的,但是我覺得對我來說夠用了,實現效果如下*

image

此示例基於vue3上實現,cesium版本1.101.0vite-plugin-cesium版本1.2.22

新建一個名為ViewshedAnalysis.js的JS檔案

import glsl from './glsl2'

/**
 * @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 ViewshedAnalysis {
    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
    }

    add() {
        this.createLightCamera()
        this.createShadowMap()
        this.drawFrustumOutline(); //視錐線
        this.drawSketch()
        this.createPostStage()
    }

    update() {
        this.clear()
        this.add()
    }

    /**
     * @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
        }
    }

    /**
     * @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(() => {
                return Cesium.Transforms.headingPitchRollQuaternion(
                    this.viewPosition,
                    Cesium.HeadingPitchRoll.fromDegrees(this.viewHeading - this.horizontalViewAngle, this.viewPitch, 0.5)
                )
            }, false),
            ellipsoid: {
                //橢球的半徑
                radii: new Cesium.CallbackProperty(() => {
                    return new Cesium.Cartesian3(this.viewDistance,
                             this.viewDistance,
                            this.viewDistance)
                }, false),
                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 ViewshedAnalysis

引入js:

import ViewShed from '@/utils/analysis/visibility/ViewshedAnalysis.js'

呼叫方法:

const shootAreaAnalysis = (type) => {
    store.setSelected('shootArea')
    let i = 0
    var horizontalViewAngle = 90  //視角水平張角
    var verticalViewAngle = 60    //視角垂直張角
    var endPosition = null
    var viewShed = null
    var handler = new Cesium.ScreenSpaceEventHandler(window.Viewer.scene.canvas)
    handler.setInputAction(movement => {
        i++
        if (i === 1) {
            var startPosition = window.Viewer.scene.pickPosition(movement.position) //滑鼠點選一次獲取開始座標
            if (!startPosition) return
            viewShed = new ViewShed(window.Viewer, {
                viewPosition: startPosition,
                viewPositionEnd: startPosition,
                horizontalViewAngle: horizontalViewAngle,
                verticalViewAngle: verticalViewAngle
            })
            // 滑鼠移動的事件
            handler.setInputAction(movement => {
                endPosition = window.Viewer.scene.pickPosition(movement.endPosition)
                if (!endPosition) return
                viewShed.updatePosition(endPosition)
                if (!viewShed.sketch) {
                    viewShed.drawSketch()
                }
            }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
        }
        // 滑鼠點選兩次獲取結束座標
        if (i === 2) {
            i = 0
            endPosition = window.Viewer.scene.pickPosition(movement.position)
            viewShed.updatePosition(endPosition)
            viewShed.update()
            handler && handler.destroy()  //銷燬滑鼠事件
            store.setSelected(null)
        }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

glsl2.js 檔案我找了兩個, 我測試都是可以實現的,只是效果問題,可以切換了嘗試一下
下載地址

相關文章