初见物理引擎库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
}

再将_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)
}

物理引擎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)
}

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

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

[ 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,
})

在这里插入图片描述

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值