• 初见物理引擎库Cannon.js:与Cesium的整合


    0 前言

    本文是“初见物理引擎库Cannon.js”系列的第四篇文章,在本文中主要讲解Cannon.js和Cesium的整合方法。

    1 在线地址

    [ Live Demo ]:Cannon - Cesium (syzdev.cn)

    在这里插入图片描述

    2 整合步骤

    2.1 Cesium和Three的整合

    由于Cannon.js是要基于Three来使用的,因此第一步要完成Cesium和Three的整合,详见:最新的Cesium和Three的整合方法

    2.2 Cannon.js的基本使用

    可以参考作者之前的两篇文章:

    1. 初见物理引擎库Cannon.js:基本使用
    2. 初见物理引擎库Cannon.js:使用dat.gui修改物体属性

    这两篇文章中包含详细的说明和完整代码,本文中不再赘述。

    2.3 注意事项

    根据 最新的Cesium和Three的整合方法 ,所有的Three.js Mesh都需要转换成_3DObject对象:

    function _3DObject() {
      this.threeMesh = null // Three.js 3DObject.mesh
      this.minWGS84 = null // location bounding box
      this.maxWGS84 = null
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再将_3DObject对象添加到_3Dobjects数组中,并在每次renderThreeObj()方法中更新Mesh的位置,但问题在于这种方法是针对于静态物体的,对于物理引擎来说,需要实时更新物体的位置和旋转,见 初见物理引擎库Cannon.js:基本使用

    // 更新MeshBodyToUpdate中的Mesh和Body,使其位置和旋转同步
    for (const object of MeshBodyToUpdate) {
      object.mesh.position.copy(object.body.position)
      object.mesh.quaternion.copy(object.body.quaternion)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    物理引擎Cannon.js计算的位置和旋转是基于Three.js的坐标的,因此若将MeshBodyToUpdate中的动态物体也添加到_3Dobjects中,那么每次在renderThreeObj()方法中更新Mesh的位置就不是在Cesium场景下的正确位置。

    解决该问题,可以在每次“更新MeshBodyToUpdate中的Mesh和Body,使其位置和旋转同步”操作之前,添加一个位置转换方法,类似于renderThreeObj()方法,先将物理引擎计算的位置转换成Cesium坐标下的位置,再进行位置和旋转的同步。

    由于作者没有深入研究该问题,这里提供的思路仅为抛砖引玉。因此在示例程序中,并没有将Mesh添加到_3Dobjects数组中,相当于在renderThreeObj()方法中下面这段代码是没有被执行的:

    // Configure Three.js meshes to stand against globe center position up direction
    for (let id in _3Dobjects) {
      minWGS84 = _3Dobjects[id].minWGS84
      maxWGS84 = _3Dobjects[id].maxWGS84
      // convert lat/long center position to Cartesian3
      let center = Cesium.Cartesian3.fromDegrees(
        (minWGS84[0] + maxWGS84[0]) / 2,
        (minWGS84[1] + maxWGS84[1]) / 2
      )
      // get forward direction for orienting model
      let centerHigh = Cesium.Cartesian3.fromDegrees(
        (minWGS84[0] + maxWGS84[0]) / 2,
        (minWGS84[1] + maxWGS84[1]) / 2,
        1
      )
      // use direction from bottom left to top left as up-vector
      let bottomLeft = cartToVec(
        Cesium.Cartesian3.fromDegrees(minWGS84[0], minWGS84[1])
      )
      let topLeft = cartToVec(
        Cesium.Cartesian3.fromDegrees(minWGS84[0], maxWGS84[1])
      )
      let latDir = new THREE.Vector3().subVectors(bottomLeft, topLeft).normalize()
      // configure entity position and orientation
      _3Dobjects[id].threeMesh.position.copy(center)
      _3Dobjects[id].threeMesh.lookAt(centerHigh.x, centerHigh.y, centerHigh.z)
      _3Dobjects[id].threeMesh.up.copy(latDir)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    示例程序中是通过地球半径来确定Three.js物体在Cesium坐标中的位置的,虽然现实中地球是一个不规则的椭球:
    在这里插入图片描述
    但是在Cesium中将地球模拟为一个规则的球体,球体的球心就是坐标原点,在 最新的Cesium和Three的整合方法 中,Three的坐标原点就是Cesium地球的球心,可以添加AxesHelper查看坐标轴:

    // 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
    const axesHelper = new THREE.AxesHelper(64410000)
    three.scene.add(axesHelper)
    
    • 1
    • 2
    • 3

    [ Live Demo ]:Cannon - Cesium-axes (syzdev.cn)

    在这里插入图片描述
    因此把物体的坐标按照坐标轴进行调整,以地球半径作为长度,即可将物体置于地表,如:

    let cannonGroundBody = new CANNON.Body({
      mass: 0,
      position: new CANNON.Vec3(0, 6371000, 0), // 置于地表Y轴
      shape: cannonGroundShape,
      material: cannonDefaultMaterial,
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    3 完整代码

    cesium-docs-demo - syzdev

    let three = {
      renderer: null,
      camera: null,
      scene: null,
    }
    
    let cesium = {
      viewer: null,
    }
    
    let cannon = {
      world: null,
    }
    const cannonDefaultMaterial = new CANNON.Material()
    let cannonDefaultCantactMaterial = null // 关联材质
    let cannonSphereBody = null // 小球
    let threeSphereMaterial = null // 小球材质
    let friction = 0.5
    let restitution = 0.7
    const MeshBodyToUpdate = []
    
    let minWGS84 = [115.23, 39.55];
    let maxWGS84 = [116.23, 41.55];
    
    let cesiumContainer = document.getElementById('cesiumContainer')
    let ThreeContainer = document.getElementById('ThreeContainer')
    let _3Dobjects = [] // Could be any Three.js object mesh
    
    function _3DObject() {
      this.threeMesh = null // Three.js 3DObject.mesh
      this.minWGS84 = null // location bounding box
      this.maxWGS84 = null
    }
    
    function initCannon() {
      cannon.world = new CANNON.World()
      cannon.world.gravity.set(0, -90000.8, 0)
      cannon.world.broadphase = new CANNON.NaiveBroadphase()
      cannonDefaultCantactMaterial = new CANNON.ContactMaterial(
        cannonDefaultMaterial,
        cannonDefaultMaterial,
        {
          friction,
          restitution,
        }
      )
      cannon.world.addContactMaterial(cannonDefaultCantactMaterial)
    }
    
    function addPlane() {
      // 添加CannonPlane地板
      let cannonGroundShape = new CANNON.Plane()
      let cannonGroundBody = new CANNON.Body({
        mass: 0,
        position: new CANNON.Vec3(0, 6371000, 0),
        shape: cannonGroundShape,
        material: cannonDefaultMaterial,
      })
      cannonGroundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
      cannon.world.add(cannonGroundBody)
      // 添加ThreePlaneGeometry地板
      let threeGroundGeometry = new THREE.PlaneGeometry(20, 20, 32)
      let threeGroundMaterial = new THREE.MeshLambertMaterial({
        color: 0xf5f5f5,
        side: THREE.DoubleSide,
      })
      let threeGroundMesh = new THREE.Mesh(threeGroundGeometry, threeGroundMaterial)
      threeGroundMesh.rotation.x = -Math.PI / 2
      threeGroundMesh.position.set(0, 6371000, 0)
      threeGroundMesh.receiveShadow = true
      threeGroundMesh.scale.set(2500, 2500, 2500)
      three.scene.add(threeGroundMesh)
    }
    
    function addShpere() {
      // 添加CannonSphere小球
      let cannonSphereShape = new CANNON.Sphere(1000)
      cannonSphereBody = new CANNON.Body({
        mass: 5,
        shape: cannonSphereShape,
        position: new CANNON.Vec3(0, 6420000, 0),
        material: cannonDefaultMaterial,
      })
      cannon.world.add(cannonSphereBody)
      // 添加ThreeSphereGeometry小球
      let threeSphereGeometry = new THREE.SphereGeometry(1, 32, 32)
      threeSphereMaterial = new THREE.MeshStandardMaterial({ color: 0xFFB6C1 })
      let threeSphereMesh = new THREE.Mesh(threeSphereGeometry, threeSphereMaterial)
      threeSphereMesh.castShadow = true
      threeSphereMesh.scale.set(1000, 1000, 1000)
      three.scene.add(threeSphereMesh)
      // 添加到MeshBodyToUpdate
      MeshBodyToUpdate.push({
        body: cannonSphereBody,
        mesh: threeSphereMesh,
      })
    }
    
    function initCesium() {
      cesium.viewer = new Cesium.Viewer(cesiumContainer, {
        useDefaultRenderLoop: false,
        selectionIndicator: false,
        homeButton: false,
        sceneModePicker: false,
        infoBox: false,
        navigationHelpButton: false,
        navigationInstructionsInitiallyVisible: false,
        animation: false,
        timeline: false,
        fullscreenButton: false,
        allowTextureFilterAnisotropic: false,
        baseLayerPicker: false,
    
        // contextOptions: {
        //   webgl: {
        //     alpha: false,
        //     antialias: true,
        //     preserveDrawingBuffer: true,
        //     failIfMajorPerformanceCaveat: false,
        //     depth: true,
        //     stencil: false,
        //     anialias: false,
        //   },
        // },
    
        targetFrameRate: 60,
        // resolutionScale: 0.1,
        orderIndependentTranslucency: true,
        geocoder: false,
        imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
          url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer',
        }),
        automaticallyTrackDataSourceClocks: false,
        // creditContainer : "hidecredit", // Cannot read properties of null (reading 'appendChild')
        dataSources: null,
        clock: null,
        terrainShadows: Cesium.ShadowMode.DISABLED,
      })
      cesium.viewer.camera.flyTo({
        destination: Cesium.Cartesian3.fromDegrees(
          90.015299, 0.000161, 150000
        ),
      })
    }
    
    function initDatGui() {
      let gui = new dat.GUI()
    
      let controls = {
        resetBall: () => {
          cannonSphereBody.position.set(0, 6420000, 0) // 重置小球位置
          cannonSphereBody.velocity.set(0, 0, 0) // 重置小球速度
          cannonDefaultCantactMaterial.friction = friction // 修改摩擦力
          cannonDefaultCantactMaterial.restitution = restitution // 修改弹性系数
        },
        friction,
        restitution,
        color: threeSphereMaterial.color.getStyle()
      }
    
      gui.add(controls, 'resetBall').name('重置小球下落')
      gui.add(controls, 'friction', 0, 2).name('摩擦力').onChange((e) => {
        friction = e
      })
      gui.add(controls, 'restitution', 0, 2).name('弹性系数').onChange((e) => {
        restitution = e
      })
      gui.addColor(controls, 'color').name('小球颜色').onChange((e) => {
        threeSphereMaterial.color.setStyle(e)
      })
    }
    
    function initThree() {
      let fov = 45
      let width = window.innerWidth
      let height = window.innerHeight
      let aspect = width / height
      let near = 50
      let far = 10 * 1000 * 10000
      three.scene = new THREE.Scene()
      three.camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
      three.renderer = new THREE.WebGLRenderer({ alpha: true })
      three.renderer.shadowMap.enabled = true
      ThreeContainer.appendChild(three.renderer.domElement)
      // 创建环境光
      let Amlight = new THREE.AmbientLight(0x444444, 1)
      three.scene.add(Amlight)
      // 创建点光源
      let spotLight = new THREE.SpotLight(0x999999, 1)
      spotLight.position.set(0, 6441000, -20000)
      spotLight.castShadow = true
      spotLight.shadow.camera.near = 1
      spotLight.shadow.camera.far = 30000000
      spotLight.shadow.camera.fov = 90
      three.scene.add(spotLight)
      let spotLightHelper = new THREE.SpotLightHelper(spotLight)
      three.scene.add(spotLightHelper)
    }
    
    function renderCesium() {
      cesium.viewer.render()
    }
    
    function renderThreeObj() {
      // register Three.js scene with Cesium
      three.camera.fov = Cesium.Math.toDegrees(cesium.viewer.camera.frustum.fovy) // ThreeJS FOV is vertical
      // three.camera.updateProjectionMatrix();
      let cartToVec = function (cart) {
        return new THREE.Vector3(cart.x, cart.y, cart.z)
      }
    
      // Configure Three.js meshes to stand against globe center position up direction
      for (let id in _3Dobjects) {
        minWGS84 = _3Dobjects[id].minWGS84
        maxWGS84 = _3Dobjects[id].maxWGS84
        // convert lat/long center position to Cartesian3
        let center = Cesium.Cartesian3.fromDegrees(
          (minWGS84[0] + maxWGS84[0]) / 2,
          (minWGS84[1] + maxWGS84[1]) / 2
        )
        // get forward direction for orienting model
        let centerHigh = Cesium.Cartesian3.fromDegrees(
          (minWGS84[0] + maxWGS84[0]) / 2,
          (minWGS84[1] + maxWGS84[1]) / 2,
          1
        )
        // use direction from bottom left to top left as up-vector
        let bottomLeft = cartToVec(
          Cesium.Cartesian3.fromDegrees(minWGS84[0], minWGS84[1])
        )
        let topLeft = cartToVec(
          Cesium.Cartesian3.fromDegrees(minWGS84[0], maxWGS84[1])
        )
        let latDir = new THREE.Vector3().subVectors(bottomLeft, topLeft).normalize()
        // configure entity position and orientation
        _3Dobjects[id].threeMesh.position.copy(center)
        _3Dobjects[id].threeMesh.lookAt(centerHigh.x, centerHigh.y, centerHigh.z)
        _3Dobjects[id].threeMesh.up.copy(latDir)
      }
      // Clone Cesium Camera projection position so the
      // Three.js Object will appear to be at the same place as above the Cesium Globe
      three.camera.matrixAutoUpdate = false
      let cvm = cesium.viewer.camera.viewMatrix
      let civm = cesium.viewer.camera.inverseViewMatrix
    
      three.camera.lookAt(0, 0, 0)
    
      three.camera.matrixWorld.set(
        civm[0],
        civm[4],
        civm[8],
        civm[12],
        civm[1],
        civm[5],
        civm[9],
        civm[13],
        civm[2],
        civm[6],
        civm[10],
        civm[14],
        civm[3],
        civm[7],
        civm[11],
        civm[15]
      )
    
      three.camera.matrixWorldInverse.set(
        cvm[0],
        cvm[4],
        cvm[8],
        cvm[12],
        cvm[1],
        cvm[5],
        cvm[9],
        cvm[13],
        cvm[2],
        cvm[6],
        cvm[10],
        cvm[14],
        cvm[3],
        cvm[7],
        cvm[11],
        cvm[15]
      )
    
      let width = cesiumContainer.clientWidth
      let height = cesiumContainer.clientHeight
    
      let aspect = width / height
      three.camera.aspect = aspect
      three.camera.updateProjectionMatrix()
      three.renderer.setSize(width, height)
      three.renderer.clear()
      three.renderer.render(three.scene, three.camera)
    }
    
    function loop() {
      cannon.world.step(1.0 / 60.0)
    
      // 更新MeshBodyToUpdate中的Mesh和Body,使其位置和旋转同步
      for (const object of MeshBodyToUpdate) {
        object.mesh.position.copy(object.body.position)
        object.mesh.quaternion.copy(object.body.quaternion)
      }
    
      requestAnimationFrame(loop)
      renderCesium()
      renderThreeObj()
    }
    
    initCesium() // Initialize Cesium renderer
    initThree() // Initialize Three.js renderer
    initCannon()
    addShpere()
    addPlane()
    initDatGui()
    loop() // Looping renderer
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
  • 相关阅读:
    基于JAVA学生公寓管理系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
    python socketserver模块开启ssl双向认证
    【MAC】MacOS M2 芯片的Mysql 数据库安装与使用
    如何在springboot项目中删除引用jar包中的无用bean
    《数字图像处理-OpenCV/Python》连载(2)目录
    基于SSM+MySQL+Bootstrap的停车场管理系统
    近期的ABAP FI开发总结
    leetcode53 -- 最大数组和
    AMD发布4.06.10.651版芯片组驱动 修复安装器无响应问题
    贪心算法 Problem N 1013 求最大盈利
  • 原文地址:https://blog.csdn.net/syzdev/article/details/126332158