3

I'm using Ammo.js, a direct JavaScript port of C++ Bullet Physics. The unfortunate result being that the documentation is C++, not great reading if your languages are Python and JavaScript.

I have the documentation for Ammo.btCompoundShape here but can't make sense of it.

I have a working code here where the Bone instance just falls through the floor, as you'll see. Don't worry about the naming of "Bone", at this stage in development it's just meant to test a compound shape of two blocks.

class RenderEngine {
  constructor(gameEngine) {
    this.gameEngine = gameEngine
    this.gameEngine.clock = new THREE.Clock();
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0xbfd1e5);
    this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 5000);
    this.camera.position.set(0, 30, 70);
    this.camera.lookAt(new THREE.Vector3(0, 0, 0));
    const hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.1);
    hemiLight.color.setHSL(0.6, 0.6, 0.6);
    hemiLight.groundColor.setHSL(0.1, 1, 0.4);
    hemiLight.position.set(0, 50, 0);
    this.scene.add(hemiLight);
    const dirLight = new THREE.DirectionalLight(0xffffff, 1);
    dirLight.color.setHSL(0.1, 1, 0.95);
    dirLight.position.set(-1, 1.75, 1);
    dirLight.position.multiplyScalar(100);
    this.scene.add(dirLight);
    dirLight.castShadow = true;
    dirLight.shadow.mapSize.width = 2048;
    dirLight.shadow.mapSize.height = 2048;
    const d = 50;
    dirLight.shadow.camera.left = -d;
    dirLight.shadow.camera.right = d;
    dirLight.shadow.camera.top = d;
    dirLight.shadow.camera.bottom = -d;
    dirLight.shadow.camera.far = 13500;
    this.renderer = new THREE.WebGLRenderer({
      antialias: true
    });
    this.renderer.setClearColor(0xbfd1e5);
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(this.renderer.domElement);
    this.renderer.shadowMap.enabled = true;
  }
  renderFrame() {
    this.renderer.render(this.scene, this.camera)
  }
}

class PhysicsEngine {
  constructor(gameEngine, physicsEngine) {
    this.gameEngine = gameEngine
    let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(),
      dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration),
      overlappingPairCache = new Ammo.btDbvtBroadphase(),
      solver = new Ammo.btSequentialImpulseConstraintSolver();
    this.tmpTrans = new Ammo.btTransform();
    this.physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
    this.physicsWorld.setGravity(new Ammo.btVector3(0, -10, 0));
  }
  updateFrame() {
    this.physicsWorld.stepSimulation(this.gameEngine.clock.getDelta(), 10);
    this.gameEngine.objects.forEach(object => {
      const ms = object.ammo.getMotionState()
      if (ms) {
        ms.getWorldTransform(this.tmpTrans)
        const p = this.tmpTrans.getOrigin()
        const q = this.tmpTrans.getRotation()
        object.mesh.position.set(p.x(), p.y(), p.z())
        object.mesh.quaternion.set(q.x(), q.y(), q.z(), q.w())
      }
    })
  }
}

class GameEngine {
  constructor(renderEngine, physicsEngine) {
    this.objects = []
    this.renderEngine = new RenderEngine(this, renderEngine)
    this.physicsEngine = new PhysicsEngine(this, physicsEngine)
  }
  run() {
    this.physicsEngine.updateFrame()
    this.renderEngine.renderFrame()
    requestAnimationFrame(() => {
      this.run()
    });
  }
  add(object) {
    this.objects.push(object)
    return this.objects.length - 1
  }
  remove(objectIndex) {
    this.objects[objectIndex] = false
  }
}

class Box {
  constructor(gameEngine, properties) {
    this.gameEngine = gameEngine
    this._initPhysics_(properties)
    this._initRendering_(properties)
    this.id = gameEngine.add(this)
  }
  _initPhysics_(properties) {
    const pos = properties.pos
    const quat = properties.quat
    const scale = properties.scale
    const mass = properties.mass
    const group = properties.group
    const interactionGroup = properties.interactionGroup
    const physicsWorld = this.gameEngine.physicsEngine.physicsWorld
    const transform = new Ammo.btTransform()
    transform.setIdentity()
    transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z))
    transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w))
    const motionState = new Ammo.btDefaultMotionState(transform)
    const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
    colShape.setMargin(0.05)
    const localInertia = new Ammo.btVector3(0, 0, 0)
    colShape.calculateLocalInertia(mass, localInertia)
    const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, colShape, localInertia)
    const body = new Ammo.btRigidBody(rbInfo)
    physicsWorld.addRigidBody(body, group, interactionGroup)
    this.ammo = body
  }
  _initRendering_(properties) {
    const pos = properties.pos
    const scale = properties.scale
    const color = properties.color
    this.mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshPhongMaterial({
      color
    }))
    this.mesh.position.set(pos.x, pos.y, pos.z)
    this.mesh.scale.set(scale.x, scale.y, scale.z)
    this.mesh.castShadow = true
    this.mesh.receiveShadow = true
    this.gameEngine.renderEngine.scene.add(this.mesh)
  }
}


class Bone {
  constructor(gameEngine, properties) {
    this.gameEngine = gameEngine
    this._initPhysics_(properties)
    this._initRendering_(properties)
    this.id = gameEngine.add(this)
  }
  _initPhysics_(properties) {
    const pos = properties.pos
    const quat = properties.quat
    const scale = properties.scale
    const mass = properties.mass
    const group = properties.group
    const interactionGroup = properties.interactionGroup
    const physicsWorld = this.gameEngine.physicsEngine.physicsWorld
    const compoundShape = new Ammo.btCompoundShape()
    this._addSection_(compoundShape, {
      pos,
      quat,
      scale,
      offset: {
        x: 0,
        y: 0,
        z: 0
      },
      rotation: {
        x: 0,
        y: 0,
        z: 0,
        w: 0
      }
    })
    this._addSection_(compoundShape, {
      pos,
      quat,
      scale,
      offset: {
        x: 0,
        y: 0,
        z: 0
      },
      rotation: {
        x: 0,
        y: 0,
        z: 0,
        w: 0
      }
    })
    const transform = new Ammo.btTransform()
    transform.setIdentity()
    transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z))
    transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w))
    const motionState = new Ammo.btDefaultMotionState(transform)
    compoundShape.setMargin(0.05)
    const localInertia = new Ammo.btVector3(0, 0, 0)
    compoundShape.calculateLocalInertia(mass, localInertia)
    const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, compoundShape, localInertia)
    const body = new Ammo.btRigidBody(rbInfo)
    physicsWorld.addRigidBody(body, group, interactionGroup)
    this.ammo = body
  }
  _initRendering_(properties) {
    const pos = properties.pos
    const scale = properties.scale
    const color = properties.color
    this.mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshPhongMaterial({
      color
    }))
    this.mesh.position.set(pos.x, pos.y, pos.z)
    this.mesh.scale.set(scale.x, scale.y, scale.z)
    this.mesh.castShadow = true
    this.mesh.receiveShadow = true
    this.gameEngine.renderEngine.scene.add(this.mesh)
  }
  _addSection_(compoundShape, properties) {
    const pos = properties.pos
    const quat = properties.quat
    const scale = properties.scale
    const offset = properties.offset
    const rotation = properties.rotation
    const transform = new Ammo.btTransform()
    transform.setIdentity()
    transform.setOrigin(new Ammo.btVector3(pos.x + offset.x, pos.y + offset.y, pos.z + offset.z))
    transform.setRotation(new Ammo.btQuaternion(quat.x + rotation.x, quat.y + rotation.y, quat.z + rotation.z, quat.w + rotation.w))
    const motionState = new Ammo.btDefaultMotionState(transform)
    const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
    compoundShape.addChildShape(transform, colShape)
  }
}

Ammo().then((Ammo) => {
  const gameEngine = new GameEngine(THREE, Ammo)
  const plane = new Box(gameEngine, {
    pos: {
      x: 0,
      y: 0,
      z: 0
    },
    quat: {
      x: 0,
      y: 0,
      z: 0,
      w: 1
    },
    scale: {
      x: 50,
      y: 2,
      z: 50
    },
    mass: 0,
    group: 1,
    interactionGroup: 1,
    color: 0xa0afa4
  })
  const box1 = new Box(gameEngine, {
    pos: {
      x: 0,
      y: 5,
      z: 0
    },
    quat: {
      x: 0,
      y: 0,
      z: 0,
      w: 1
    },
    scale: {
      x: 2,
      y: 2,
      z: 2
    },
    mass: 1,
    group: 1,
    interactionGroup: 1,
    color: 0xa0afa4
  })
  const box2 = new Box(gameEngine, {
    pos: {
      x: 0.75,
      y: 8,
      z: 0.75
    },
    quat: {
      x: 0,
      y: 0,
      z: 0,
      w: 1
    },
    scale: {
      x: 2,
      y: 2,
      z: 2
    },
    mass: 1,
    group: 1,
    interactionGroup: 1,
    color: 0xa0afa4
  })
  const bone1 = new Bone(gameEngine, {
    pos: {
      x: -0.75,
      y: 10,
      z: -0.75
    },
    quat: {
      x: 0,
      y: 0,
      z: 0,
      w: 1
    },
    scale: {
      x: 2,
      y: 2,
      z: 2
    },
    mass: 1,
    group: 1,
    interactionGroup: 1,
    color: 0xa0afa4
  })
  console.log("gameEngine", gameEngine)
  gameEngine.run()
})
canvas, body, html {
  margin: 0px;
  padding: 0px;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r124/three.min.js"></script>
<script src="https://cdn.babylonjs.com/ammo.js"></script>

The two Box instances land on the floor (plane), bone1 falls through. I assume I did something wrong with the Ammo.btCompoundShape. There are no errors. What's the correct way?

J.Todd
  • 707
  • 1
  • 12
  • 34
  • I don't know solution but try change scale bone.y from 2 to 15 and camera.y from 30 to 0 and look at scene after it, the plane looks like water. – Daniil Loban Jan 09 '21 at 01:30
  • Check out [this tutorial](https://medium.com/@bluemagnificent/intro-to-javascript-3d-physics-using-ammo-js-and-three-js-dd48df81f591) about Ammo.js. Seems to cover this. – benhatsor Jan 21 '21 at 08:03

1 Answers1

1

The bone actually does not fall through completely, it stops in the middle of the plane.

Reason: You are transforming two times:

  1. motionState is transformed
  2. compoundShape is transformed again inside of _addSection_

This way the compoundShape does collide and does not fall through, but the visible colShape is offset (from the reference position of compoundShape) to be inside the plane.

You can see that if you try and change this line inside _addSection_:

transform.setOrigin(new Ammo.btVector3(0, 0, 2.0))

Solution:

Do not transform two times. E.g. transform only the motionState, but not the compoundShape.
E.g. remove these two lines:

_addSection_(compoundShape, properties) {
    const transform = new Ammo.btTransform()
    transform.setIdentity()
    // -- disable second transform: --
    // transform.setOrigin(new Ammo.btVector3( ... ))
    // transform.setRotation(new Ammo.btQuaternion( ... ))

    const colShape = new Ammo.btBoxShape(new Ammo.btVector3(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5))
    compoundShape.addChildShape(transform, colShape)
}

Another remark:

localInertia is also applied to two times:

  • compoundShape.calculateLocalInertia(mass, localInertia) and
  • const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, compoundShape, localInertia).

This obviously works, but is probably not intentional. It doesn't fail because it is 0,0,0. If you indeed want two inertia, I also think you can not use the same localInertia object for both of them, but you should create a second object e.g. localInertiaCompoundShape, but I'm not sure.

kca
  • 4,856
  • 1
  • 20
  • 41