You guyz rock
@MackeyK24 @Cedric @sebavan Success
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.
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.
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
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
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
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
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)
}
}
Thanks Man
Yep… that missing update was it. Thanks Again
Got native heightmap terrain collisions working smoothly… Check out other post for detail
This is all really exciting to see taking shape.
@MackeyK24 did you see my messages about the IK bone controller?
@arcman7 @MackeyK24 In @arcman7 's PG (https://playground.babylonjs.com/#9M3ILN#53), the debug drawer for the heightmap seems to be a bit off, and the spheres are able to pass through the hills. By chance, did something change on Babylon’s side that broke the PG?
Thank you for your help, and apologies for the ping
In that pg example, yes the physics height map surface is misaligned with the visual mesh surface:
You can see that by looking at the corners where dotted lines don’t match the visual surface texture. Either the visual mesh, or the physics height map object need to be rotated and probably have an axis flipped.
That said, I’m pretty sure the debug drawer is still properly drawing the surface of the physics objects.
Disable the ground visual mesh, and you’ll see the spheres do not pass through the actual physics height map:
There is something funky going on with that surface misalignment though, I never did take the time to figure it out.
I too noticed some misalignment between height map and visual ground mesh.
I also could not figure it out and eventually added an option to use mesh collider for ground
Thank you so much for your help, @arcman7 and @MackeyK24 Exactly as you both mentioned, there seems to be
Indeed, the Ammo debug drawer seems to be correctly showing where the collisions for the balls should be
Now that I’m thinking about this, I’m pretty sure I ran into this same problem before a long time ago. The problem is that the vertices are in some format or order that doesn’t match the order you’re feeding into Ammo.js.
It’s mostly fixed here:
These are the relevant changes:
// line 495
const numberOfRows = subdivisions + 1;
const numberOfCols = subdivisions + 1;
// line 507
for (let i = 0; i < numberOfRows; i++) {
// for (let j = 0; j < numberOfCols; j++) {
for (let j = numberOfCols - 1; j > -1; j--) {
height = heightData[(i * numberOfCols + j)*3 + 1];
ammoInstance.HEAPF32[ammoHeightData + p2 >> 2] = height;
p2 += 4;
}
}
//line 524
let flipQuadEdges = true; // not sure why, but phys surface appears to fit better this way
// line 565
const originY = (maxHeight - minHeight) * 0.5 - 2; // I forgot why this adjustment is necessary, and the value 2 was just a best guess on my part. I think there's also a slight x and z origin tweaking needed as well.