Skip to content

Instantly share code, notes, and snippets.

@kenjinp
Created December 11, 2021 16:46
Show Gist options
  • Save kenjinp/1b1eb135747682f9b7a2b5c37bfbd7d5 to your computer and use it in GitHub Desktop.
Save kenjinp/1b1eb135747682f9b7a2b5c37bfbd7d5 to your computer and use it in GitHub Desktop.
Create a Rapier Heightfield Collider from an Image (Heightmap)
import getPixels from "get-pixels";
import { max, min } from "lodash";
import { NdArray } from "ndarray";
import { Vector3 } from "three";
// Example, use like:
const makeHeightfieldColliderFromImage = async () => {
const heightMapTexture = "myimage.png";
const xSubdivisions = 200;
const zSubdivisions = 200;
const yScale = 10;
const data = await imageToHeightmap(
heightMapTexture,
xSubdivisions,
zSubdivisions,
yScale
);
const scale = 1;
const nrows = data.numYRows;
const ncols = data.numXCols;
const heights = data.tileHeights;
return new Rapier.Heightfield(nrows, ncols, heights, scale);
};
export const getImageValues = (
imageURL: string
): Promise<NdArray<Uint8Array>> => {
return new Promise((resolve, reject) => {
getPixels(imageURL, (error, pixels) => {
if (error) {
return reject(error);
}
return resolve(pixels);
});
});
};
// example taken from
// https://www.npmjs.com/package/image-to-heightmap
export const imageToHeightmap = async (
imageURL: string,
numXCols: number,
numYRows: number,
scale: number
) => {
const pixels = await getImageValues(imageURL);
const tileHeights = new Array();
const imageWidth = pixels.shape[0];
const imageHeight = pixels.shape[1];
const sizeX = numXCols + 1;
const sizeY = numYRows + 1;
// Loop through all of our tile corners, and generate a height for each corner
// tileHeights[x] = [];
for (let x = 0; x < sizeX; x++) {
for (let y = 0; y < sizeY; y++) {
// Get the corresponding pixel for the current tile corner
const pixelXPos = Math.ceil((x * (imageWidth - 1)) / numXCols);
const pixelYPos = Math.ceil((y * (imageHeight - 1)) / numYRows);
// Get the colors for the given pixel
const red = pixels.get(pixelXPos, pixelYPos, 0);
const green = pixels.get(pixelXPos, pixelYPos, 1);
const blue = pixels.get(pixelXPos, pixelYPos, 2);
// TODO: Should we calculate alpha for each pixel if we're expecting
// the same value every time? Should we even expect the same value every time?
const alpha = pixels.get(pixelXPos, pixelYPos, 3);
// Note that we divide by `alpha` to get the height
// If we always use fully opaque images, alpha will be the maximum possible value
// This allows us to handle 8bit, 16bit, or Nbit images
// Values are based on ratios. We don't care about the images bit depth
const heightAtThisPosition = (scale * (red + green + blue)) / 3 / alpha;
tileHeights.push(heightAtThisPosition);
}
}
return {
imageWidth,
imageHeight,
numXCols,
numYRows,
tileHeights,
max: max(tileHeights),
min: min(tileHeights),
};
};
export const genHeightfieldGeometry = (
nrows: number,
ncols: number,
heights: number[],
scale: Vector3
) => {
let vertices = new Array();
let indices = new Array();
let eltWX = 1.0 / nrows;
let eltWY = 1.0 / ncols;
let i, j;
for (j = 0; j <= ncols; ++j) {
for (i = 0; i <= nrows; ++i) {
let x = (j * eltWX - 0.5) * scale.x;
let y = heights[j * (nrows + 1) + i] * scale.y;
let z = (i * eltWY - 0.5) * scale.z;
vertices.push(x, y, z);
}
}
for (j = 0; j < ncols; ++j) {
for (i = 0; i < nrows; ++i) {
let i1 = (i + 0) * (ncols + 1) + (j + 0);
let i2 = (i + 0) * (ncols + 1) + (j + 1);
let i3 = (i + 1) * (ncols + 1) + (j + 0);
let i4 = (i + 1) * (ncols + 1) + (j + 1);
indices.push(i1, i3, i2);
indices.push(i3, i4, i2);
}
}
return {
vertices: new Float32Array(vertices),
indices: new Uint32Array(indices),
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment