Open ghost opened 4 years ago
Instead of random numbers, i did the following:
I created a height field in blender then extracted the height values as an array of arrays (with the help of a python script) in a way that i was able to paste it into my js function. https://github.com/tomo0613/offroadJS
@baronwatts If still interested here's a function that basically does that: I did some experiments (same repo)
note:
function meshToHeightField(mesh) {
const geometry = findGeometry(mesh);
// positions coordinates are stored in a THREE.Float32BufferAttribute (array buffer [c0.x,c0.y,c0.z,c1.x, ...])
const vertices = mapPositionBufferToVertices(geometry.getAttribute('position'));
// if the the plane width equals to its length
const rowCount = Math.sqrt(vertices.length);
const columnCount = rowCount;
geometry.computeBoundingBox();
const minX = geometry.boundingBox.min.x;
const maxX = geometry.boundingBox.max.x;
const minZ = geometry.boundingBox.min.z;
const maxZ = geometry.boundingBox.max.z;
const gridWidth = maxX - minX;
const gridLength = maxZ - minZ;
// the scale is bit off, so it needs some adjustment (+ 0.1585)
const gridElementSize = gridWidth / columnCount + 0.1585;
// create grid
const grid = [];
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = [];
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const vertexIndex = rowIndex * rowCount + columnIndex;
const vertex = vertices[vertexIndex];
row.push(vertex.y);
}
grid.push(row);
}
// create heightField from grid
const heightFieldShape = new CANNON.Heightfield(grid, {elementSize: gridElementSize});
const heightField = new CANNON.Body({mass: 0, shape: heightFieldShape});
const q1 = new THREE.Quaternion();
q1.setFromAxisAngle(new CANNON.Vec3(0, 0, 1), -Math.PI / 2);
const q = new THREE.Quaternion();
q.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
q.multiply(q1);
heightField.quaternion.copy(q);
heightField.position.set(
-gridWidth / 2,
0,
gridLength / 2 - gridLength,
);
return heightField;
function findGeometry(mesh) {
let geometry;
mesh.traverse((child) => {
if (!geometry && child.type === 'Mesh' && child.geometry) {
geometry = child.geometry;
}
});
return geometry;
}
function mapPositionBufferToVertices(positionBuffer) {
const vertexArray = [];
const vertexCount = positionBuffer.count;
for (let i = 0; i < vertexCount; i++) {
vertexArray.push(new THREE.Vector3(
positionBuffer.getX(i),
positionBuffer.getY(i),
positionBuffer.getZ(i),
));
}
// vertices in a mesh are not in order, sort them by x & z position
vertexArray.sort((a, b) => {
if (a.z === b.z) {
return (a.x < b.x) ? -1 : (a.x > b.x) ? 1 : 0;
} else {
return (a.z < b.z) ? -1 : 1;
}
});
// filter duplicated vertices
return vertexArray.filter((vertex, index) => {
const nextVertex = vertexArray[index + 1];
const duplicated = nextVertex
&& vertex.x === nextVertex.x
&& vertex.y === nextVertex.y
&& vertex.z === nextVertex.z;
return !duplicated;
});
}
}
this is the way:
how to use?: (as you want) in general you only want to calculate the height values if the mesh was edited, then print the value of {heightMap} and copy to a variable or store it as JSON, than use that like a static value to create your Heightfield().
async function generateHeightfieldFromMesh(mesh/*: Mesh*/, pointDistance/*: number*/) {
// https://threejs.org/docs/index.html#api/en/core/Raycaster
const rayCaster = new THREE.Raycaster();
const rayCasterPosition = new THREE.Vector3();
const upAxis = new THREE.Vector3(0, 1, 0);
const heightMap = [];
const geometry = findGeometry(mesh);
geometry.computeBoundingBox();
const {
min: {x: minX, y: minY, z: minZ},
max: {x: maxX, z: maxZ},
} = geometry.boundingBox;
const width = maxX - minX;
const length = maxZ - minZ;
const totalX = width / pointDistance + 1;
const totalZ = length / pointDistance + 1;
const totalSteps = totalX * totalZ;
let currentStep = 0;
for (let x = minX; x <= maxX; x += pointDistance) {
const heightDataRow = [];
heightMap.push(heightDataRow);
for (let z = maxZ; z >= minZ; z -= pointDistance) {
rayCasterPosition.set(x, minY, z);
rayCaster.set(rayCasterPosition, upAxis);
const y = await calculateMeshSurfaceDistanceByRayCast();
heightDataRow.push(y);
}
}
const terrainShape = new CANNON.Heightfield(heightMap, {elementSize: pointDistance});
const heightfield = new CANNON.Body({ mass: 0, shape: terrainShape });
heightfield.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
heightfield.position.set(minX, 0, maxZ);
return heightfield;
function calculateMeshSurfaceDistanceByRayCast() {
return new Promise((resolve) => {
window.setTimeout(() => {
currentStep++;
console.log(`generating height field... ${Math.floor(100 / totalSteps * currentStep)}%`);
const [result] = rayCaster.intersectObject(mesh, true);
resolve(result.distance);
});
});
}
}
https://github.com/Loryhoof/Mesh2Heightfield I hope this can help someone, its based on @tomo0613 's OffroadJS
Is there a way to have the vehicle raycast on an imported 3d terrain rather than creating a matrix of random numbers?