Ammo Heightfield Terrain Shape

You guyz rock :slight_smile:

1 Like

@MackeyK24 @Cedric @sebavan Success :smiley:

The Debug.PhysicsViewer is more appropriate for a small scene like this or anytime the number of physics meshes is less than 150k (a number I’ve found empirically on my own machine), but if you’ve got large open terrain as seen in both Mackey’s and my own demos then you’d probably want to use this AmmoDebugDrawer interface instead.

2 Likes

Yo @arcman7

So the success was the SHOW HMAP where we see the point clould of the actual physics objects.

Not the get height from terrain and the alignment of the original mesh with the btHeightfieldTerrainShape ?

Yeah, I probably should have clarified; I was demonstrating the newly exposed Ammo Debug Drawer Interface being used in a Babylon.js playground. Up until now this was something I had to do locally. The pg actually shows that the height map data does not match the shape of the flattened globe terrain, and it’s really easy to tell now that we have direct visual references coming from Ammo itself.

1 Like

Hey that new Ammo Debug Drawer looks different that the two scripts i got from you… Mainly in the version i got from you before… the moving object still show the point from the last position… so when they are moving you still still all the previous positions of the points… the one in the demo above the points are removed so only see current points for moving objects

So @arcman7 and @Pryme8

I got btHeightfieldTerrainShape and working in the toolkit. Using AmmoDebugDrawer (older version i think) the heightmap fits perfect to the terrain mesh.

This is using TerrainData.GetHeights … I simply denormalize the raw heights from the terrain height and … Voilà

Looks Beautiful :slight_smile:

Next i wanna try using the heights from the mesh vertices instead of serializing the raw heights from unity as json… Because i already have the mesh anyways… No need to store the extra data if i can get the heights from the mesh

UPDATE
Holy Crap… That works as well… I just gotta work alignment when the terrain is not a zero. but the terrain collision using mesh heights is working and the ammo debug drawer is working

FYI… This is how i am creating the height field terrain shape from Babylon.Mesh data

private static CreateHeightfieldTerrainShapeFromMesh(mesh:BABYLON.Mesh, terrainWidth:number, terrainDepth:number, terrainMinHeight:number, terrainMaxHeight:number, terrainWidthExtents:number, terrainDepthExtents:number, ammoHeightResult:any, flipQuadEdges:boolean = false):any {
    // This parameter is not really used, since we are using PHY_FLOAT height data type and hence it is ignored
    const heightScale = 1;

    // Up axis = 0 for X, 1 for Y, 2 for Z. Normally 1 = Y is used.
    const upAxis = 1;

    // hdt, height data type. "PHY_FLOAT" is used. Possible values are "PHY_FLOAT", "PHY_UCHAR", "PHY_SHORT"
    const hdt = "PHY_FLOAT";

    // Create heightfield physics shape
    mesh.bakeCurrentTransformIntoVertices();
    const heightData = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);

    // Creates height data buffer in Ammo heap
    ammoHeightResult.ammoHeightData = Ammo._malloc(4 * terrainWidth * terrainDepth);

    let height = 0, p2 = 0;
    for (let i = 0; i < heightData.length / 3; i++) {
        height = heightData[i * 3 + 1]; // Note: Get The Y Component Only
        // ..
        // FIXME: Maybe Minus The Mesh Position Y To Offset Not A Zero Origin - ???
        // ..
        Ammo.HEAPF32[ammoHeightResult.ammoHeightData + p2 >> 2] = height;
        // 4 bytes/float
        p2 += 4;
    }

    // Creates the heightfield physics shape
    const heightFieldShape:any = new Ammo.btHeightfieldTerrainShape(
        terrainWidth,
        terrainDepth,
        ammoHeightResult.ammoHeightData,
        heightScale,
        terrainMinHeight,
        terrainMaxHeight,
        upAxis,
        hdt,
        flipQuadEdges
    );

    // Set horizontal scale
    const scaleX:number = terrainWidthExtents / (terrainWidth - 1);
    const scaleZ:number = terrainDepthExtents / (terrainDepth - 1);
    heightFieldShape.setLocalScaling( new Ammo.btVector3(scaleX, 1, scaleZ));
    heightFieldShape.setMargin(BABYLON.SceneManager.DefaultHeightFieldMargin);
    return heightFieldShape;
}

So I thought about this for a bit, and one thing that stopped me from taking that approach is that mesh vertices are never guaranteed to be equally spaced out from each other in a perfect grid structure. I’ve been downloading models from sketchfab to test with, and I’ve straight up seen models that have certain regions that are more dense than rest in terms of vertices per square unit of length. I think serializing the height map data is the way to go. IF you want really efficient storage and transmission, then you can store each normalized height vertex as an rgba value in an image file instead of the serialized json. I just never went that route because it hasn’t been an issue for me yet.

That is not an issue for me and the toolkit.

First off, the terrain meshes i make are from the RAW HEIGHTS of the terrain data. Terrains are made uniformly in unity, wont just be any ol model you download.

Besides the actual mesh is made from the the same raw heights i get from unity. I just do them at design time. It also sets up all the terrain splatmap shader stuff on that mesh so you get all the detail terrain painting from the Unity Terrain Tools… So the mesh I export has everything I need already… And since I am already exporting a mesh that already has the EXACT height I need (From Unity TerrainData.GetHeights) I dont need the extra json that has those same raw heights… Working great on all the terrains i exported so far from Unity

1 Like

Oh that’s great! I can’t wait to learn how to use this Unity Toolkit a bit more, and not have to deal with these issues in the wild that I’ve seen lol.

But what you have there looks good! Try alternating the colors to get cool color combos via:

debugDrawer.setAlternate(1)
/* set primary color to blue */
debugDrawer.setColor( new BABYLON.Color4(0, 0, 1, 0.5))
/* set secondary color to yellow */
debugDrawer.setSecondaryColor(new BABYLON.Color4(1, 1, 0, 0.5))

Also if your machine has enough ram, you can set the interpolationDensity value to something a bit higher; this is just the number of points displayed between any two vertices. So if you wanted to see almost a line being drawn between any two vertices, you could use:

debugDrawer.setInterpolationDensity(10)

Yo @arcman7

I wanna make sure I got your latest AmmoDebugDrawer.ts and BabylonAmmoDebugDrawer.ts

Can you please send me those again :slight_smile:

Yeah… those classes in the gist are identical to what I have in my own project though; is something not working?

EDIT: Okay I realized the update method was missing from the BabylonAmmoDebugDrawer class. It’s included in the file below. I’ll make the change to the gist as well.

Here it is though (I just keep the two classes in the same file):

AmmoDebugDrawer.ts

/* ADOPTED FROM
https://github.com/InfiniteLee/ammo-debug-drawer
*/

import { Color4, Mesh, Particle, PointsCloudSystem, Scene, Vector3 } from "@babylonjs/core";
import Ammo from "ammo.js"

export const DefaultBufferSize = 3 * 1000000

export const AmmoDebugConstants = {
  NoDebug: 0,
  DrawWireframe: 1,
  DrawAabb: 2,
  DrawFeaturesText: 4,
  DrawContactPoints: 8,
  NoDeactivation: 16,
  NoHelpText: 32,
  DrawText: 64,
  ProfileTimings: 128,
  EnableSatComparison: 256,
  DisableBulletLCP: 512,
  EnableCCD: 1024,
  DrawConstraints: 1 << 11, //2048
  DrawConstraintLimits: 1 << 12, //4096
  FastWireframe: 1 << 13, //8192
  DrawNormals: 1 << 14, //16384
  MAX_DEBUG_DRAW_MODE: 0xffffffff
};

const setXYZ = function(array: number[] | Float32Array, index: number, x: number, y: number, z: number) {
  index *= 3;
  array[index + 0] = x;
  array[index + 1] = y;
  array[index + 2] = z;
}

/**
 * An implementation of the btIDebugDraw interface in Ammo.js, for debug rendering of Ammo shapes
*/
export class AmmoDebugDrawer implements Ammo.btIDebugDraw {
  world: Ammo.btDiscreteDynamicsWorld
  debugDrawer: Ammo.DebugDrawer
  indexArray: Uint32Array
  verticesArray: Float32Array
  colorsArray: Float32Array
  debugDrawMode: number
  index: number
  enabled: boolean
  warnedOnce: boolean

  constructor(
    indexArray: Uint32Array,
    verticesArray: Float32Array,
    colorsArray: Float32Array,
    world: Ammo.btDiscreteDynamicsWorld,
    options: {
      debugDrawMode: number
    } = {
      debugDrawMode: AmmoDebugConstants.DrawWireframe
    }
  ) {

    this.world = world
    //@ts-ignore
    this.debugDrawer = new window.Ammo.DebugDrawer()
    this.verticesArray = verticesArray
    this.colorsArray = colorsArray
    this.indexArray = indexArray

    this.debugDrawMode = options.debugDrawMode || AmmoDebugConstants.DrawWireframe

    this.index = 0
    if (this.indexArray) {
      Atomics.store(this.indexArray, 0, this.index)
    }

    this.enabled = false

    this.drawLine = this.drawLine.bind(this)
    this.drawContactPoint = this.drawContactPoint.bind(this)
    this.reportErrorWarning = this.reportErrorWarning.bind(this)
    this.draw3dText = this.draw3dText.bind(this)
    this.setDebugMode = this.setDebugMode.bind(this)
    this.getDebugMode = this.getDebugMode.bind(this)
    this.enable = this.enable.bind(this)
    this.disable = this.disable.bind(this)
    //@ts-ignore
    this.update = this.update.bind(this)

    //@ts-ignore
    this.debugDrawer.drawLine = this.drawLine
    //@ts-ignore
    this.debugDrawer.drawContactPoint = this.drawContactPoint.bind(this)
    this.debugDrawer.reportErrorWarning = this.reportErrorWarning.bind(this)
    this.debugDrawer.draw3dText = this.draw3dText.bind(this)
    this.debugDrawer.setDebugMode = this.setDebugMode.bind(this)
    this.debugDrawer.getDebugMode = this.getDebugMode.bind(this)
    //@ts-ignore
    this.debugDrawer.enable = this.enable.bind(this)
    //@ts-ignore
    this.debugDrawer.disable = this.disable.bind(this)
    //@ts-ignore
    this.debugDrawer.update = this.update.bind(this)
    //@ts-ignore
    this.world.setDebugDrawer(this.debugDrawer)
  }

  enable() {
    this.enabled = true
  }

  disable() {
    this.enabled = false
  }

  update()  {
    if (!this.enabled) {
      return
    }

    if (this.indexArray) {
      if (Atomics.load(this.indexArray, 0) === 0) {
        this.index = 0
        this.world.debugDrawWorld()
        Atomics.store(this.indexArray, 0, this.index)
      }
    } else {
      this.index = 0
      this.world.debugDrawWorld()
    }
  }

  //@ts-ignore
  drawLine(from: number, to: number, color: number) {
    //@ts-ignore
    const heap: Float32Array = window.Ammo.HEAPF32
    const r = heap[(color + 0) / 4]
    const g = heap[(color + 4) / 4]
    const b = heap[(color + 8) / 4]
  
    const fromX = heap[(from + 0) / 4]
    const fromY = heap[(from + 4) / 4]
    const fromZ = heap[(from + 8) / 4]
    setXYZ(this.verticesArray, this.index, fromX, fromY, fromZ)
    setXYZ(this.colorsArray, this.index++, r, g, b)
  
    const toX = heap[(to + 0) / 4]
    const toY = heap[(to + 4) / 4]
    const toZ = heap[(to + 8) / 4]
    setXYZ(this.verticesArray, this.index, toX, toY, toZ)
    setXYZ(this.colorsArray, this.index++, r, g, b)
  }

  //TODO: figure out how to make lifeTime work
  //@ts-ignore
  drawContactPoint(
    pointOnB: number, normalOnB: number, distance: number, _lifeTime: number, color: number
  ) {
    //@ts-ignore
    const heap: Float32Array = window.Ammo.HEAPF32
    const r = heap[(color + 0) / 4]
    const g = heap[(color + 4) / 4]
    const b = heap[(color + 8) / 4]
  
    const x = heap[(pointOnB + 0) / 4]
    const y = heap[(pointOnB + 4) / 4]
    const z = heap[(pointOnB + 8) / 4]
    setXYZ(this.verticesArray, this.index, x, y, z)
    setXYZ(this.colorsArray, this.index++, r, g, b)
  
    const dx = heap[(normalOnB + 0) / 4] * distance
    const dy = heap[(normalOnB + 4) / 4] * distance
    const dz = heap[(normalOnB + 8) / 4] * distance
    setXYZ(this.verticesArray, this.index, x + dx, y + dy, z + dz)
    setXYZ(this.colorsArray, this.index++, r, g, b)
  }

  reportErrorWarning(warningString: string) {
    if (Ammo.hasOwnProperty("UTF8ToString")) {
      //@ts-ignore
      console.warn(window.Ammo.UTF8ToString(warningString))
    } else if (!this.warnedOnce) {
      this.warnedOnce = true
      console.warn("Cannot print warningString, please export UTF8ToString from Ammo.js in make.py")
    }
  }

  draw3dText = function(_location: Ammo.btVector3, textString: string) {
    //TODO
    console.warn("TODO: draw3dText");
  }

  setDebugMode(debugMode: number) {
    this.debugDrawMode = debugMode
  }

  getDebugMode() {
    return this.debugDrawMode
  }
}


export class BabylonAmmoDebugDrawer extends AmmoDebugDrawer {
  pcs: PointsCloudSystem
  _pcsMesh: Mesh
  points: Vector3[]
  scene: Scene
  seenPoints: { [key: string]: number }
  pointPairs: number[][]
  _drawCount: number // useful info 
  _pcsCount: number  // for development
  _pointSize: number
  _color1: Color4
  _color2: Color4
  _pointsPerLine: number
  _alternate: number
  _pointsDrawn: number
  _updateAfter: number
  _updateTimer: number | null

  constructor(
    indexArray: Uint32Array,
    verticesArray: Float32Array,
    colorsArray: Float32Array,
    world: Ammo.btDiscreteDynamicsWorld,
    scene: Scene,
    options: {
      debugDrawMode: number
    } = {
      debugDrawMode: AmmoDebugConstants.DrawWireframe
    }
  ) {
    super(
      indexArray,
      verticesArray,
      colorsArray,
      world,
      options,
    )

    this.scene = scene
    this.seenPoints = {}
    this.pointPairs = []
    this._drawCount = 0
    this._pointSize = 2
    this._color1 = new Color4(1, 0, 0, 0.5)
    this._color2 = new Color4(0, 0, 1, 0.5)
    this._alternate = 0
    this._pointsDrawn = 0
    this._pointsPerLine = 2
    this._pcsCount = 0
    this._updateAfter = 0
  }

  update() {
    // reset everything
    this.seenPoints = {}
    this.pointPairs = []
    super.update()
  }

  drawLine(from: number, to: number, color: number) {
    super.drawLine(from, to, color)
    this._drawCount += 1
    const seenPoints = this.seenPoints
    //@ts-ignore
    const heap: Float32Array = window.Ammo.HEAPF32

    const fromX = heap[(from + 0) / 4]
    const fromY = heap[(from + 4) / 4]
    const fromZ = heap[(from + 8) / 4]

    const toX = heap[(to + 0) / 4]
    const toY = heap[(to + 4) / 4]
    const toZ = heap[(to + 8) / 4]


    if (seenPoints[`${fromX}-${fromY}-${fromZ}`] === undefined) {
      this.pointPairs.push([fromX, fromY, fromZ, toX, toY, toZ])
      seenPoints[`${fromX}-${fromY}-${fromZ}`] = this.pointPairs.length - 1
    } else if (seenPoints[`${toX}-${toY}-${toZ}`] === undefined) {
      const ind = seenPoints[`${fromX}-${fromY}-${fromZ}`]
      this.pointPairs[ind] = [fromX, fromY, fromZ, toX, toY, toZ]
    }
  }

  drawPhysWorld() {
    if (this._pcsMesh) {
      this._pcsMesh.dispose()
    }

    this.pcs = new PointsCloudSystem(
      `pcs-world-view-${this._pcsCount}`, this._pointSize, this.scene
    )
    this._pcsCount += 1

    const numPoints = this._pointsPerLine

    for (let i = 0; i < this.pointPairs.length; i++) {
      const fromX = this.pointPairs[i][0]
      const fromY = this.pointPairs[i][1]
      const fromZ = this.pointPairs[i][2]

      const toX = this.pointPairs[i][3]
      const toY = this.pointPairs[i][4]
      const toZ = this.pointPairs[i][5]
  
      let dX = (toX - fromX) / numPoints
      let dY = (toY - fromY) / numPoints
      let dZ = (toZ - fromZ) / numPoints

      if (numPoints === 1) {
        /* take average instead */
        dX *= 0.5
        dY *= 0.5
        dZ *= 0.5
      }

      const myFunc = (particle: Particle, _i: number, s: number) => {
        this._pointsDrawn += 1
        let usedColor = this._color1

        if (this._alternate && (this._pointsDrawn % (this._alternate + 1)) === 0) {
          usedColor = this._color2 //@ts-ignore
        }
        const pos = new Vector3(
          dX * s + fromX,
          dY * s + fromY,
          dZ * s + fromZ,
        )
        particle.position = pos
        particle.color = usedColor
      }

      this.pcs.addPoints(numPoints, myFunc)
    }
    
    return this.pcs.buildMeshAsync().then((mesh) => {
      this._pcsMesh = mesh
      this._pcsMesh.hasVertexAlpha = true
      this._pcsMesh.isPickable = false
      return mesh
    })
  }

  setRenderPointSize(size: number) {
    this._pointSize = size
  }

  setColor(color: Color4) {
    this._color1 = color.clone()
  }

  setSecondaryColor(color: Color4) {
    this._color2 = color.clone()
  }


  setAlternate(switchEveryNPoints: number) {
    this._alternate = switchEveryNPoints
  }

  setInterpolationDensity(pointsPerLine: number) {
    this._pointsPerLine = pointsPerLine
  }

  /* period in miliseconds */
  setUpdatePeriod(updateAfter: number) {
    this._updateAfter = updateAfter

    if (this._updateTimer) {
      window.clearInterval(this._updateTimer)
    }

    if (updateAfter === 0) {
      return
    }

    this._updateTimer = window.setInterval(() => {
      this.update()
      this.drawPhysWorld()
    }, this._updateAfter)
  }
}
1 Like

Thanks Man :slight_smile:

1 Like

Yep… that missing update was it. Thanks Again :slight_smile:

1 Like

Got native heightmap terrain collisions working smoothly… Check out other post for detail

3 Likes

This is all really exciting to see taking shape.

@MackeyK24 did you see my messages about the IK bone controller?