Skip to content

Instantly share code, notes, and snippets.

@guiseek
Created July 12, 2025 22:56
Show Gist options
  • Save guiseek/70d303595f77a70e899659e886f2ce90 to your computer and use it in GitHub Desktop.
Save guiseek/70d303595f77a70e899659e886f2ce90 to your computer and use it in GitHub Desktop.
Shape Mapper
import {Mesh, Vector3, type BufferGeometry, type Vector3Tuple} from 'three'
import {ConvexHull} from 'three/examples/jsm/Addons.js'
import {
Box,
Vec3,
Plane,
Sphere,
Trimesh,
Cylinder,
Heightfield,
ConvexPolyhedron,
} from 'cannon-es'
export class ShapeMapper {
static plane() {
return new Plane()
}
static box(geometry: BufferGeometry) {
geometry.computeBoundingBox()
if (!geometry.boundingBox) {
throw `Bounding box error`
}
const halfExtents = new Vector3()
.subVectors(geometry.boundingBox.max, geometry.boundingBox.min)
.multiplyScalar(0.5)
.toArray()
return new Box(new Vec3(...halfExtents))
}
static sphere(geometry: BufferGeometry) {
geometry.computeBoundingSphere()
if (!geometry.boundingSphere) {
throw `Bounding sphere error`
}
const radius = geometry.boundingSphere.radius
return new Sphere(radius)
}
static cylinder(geometry: BufferGeometry) {
geometry.computeBoundingBox()
if (!geometry.boundingBox) {
throw `Bounding box error`
}
const size = new Vector3().subVectors(
geometry.boundingBox.max,
geometry.boundingBox.min
)
const radiusTop = size.x * 0.5
const radiusBottom = radiusTop
const height = size.y
const numSegments = 8
return new Cylinder(radiusTop, radiusBottom, height, numSegments)
}
static trimesh(geometry: BufferGeometry) {
geometry.computeVertexNormals()
geometry.computeBoundingBox()
const position = geometry.attributes['position']
const index = geometry.index
const vertices: number[] = []
for (let i = 0; i < position.count; i++) {
vertices.push(position.getX(i), position.getY(i), position.getZ(i))
}
const indices: number[] = []
if (index) {
for (let i = 0; i < index.count; i++) {
indices.push(index.getX(i))
}
} else {
for (let i = 0; i < position.count; i++) {
indices.push(i)
}
}
return new Trimesh(vertices, indices)
}
static heightfield(geometry: BufferGeometry) {
geometry.computeBoundingBox()
const position = geometry.attributes['position']
const matrix: number[][] = []
const width = Math.round(Math.sqrt(position.count))
const height = width
for (let i = 0; i < width; i++) {
const row: number[] = []
for (let j = 0; j < height; j++) {
const index = i * width + j
row.push(position.getY(index))
}
matrix.push(row)
}
if (!geometry.boundingBox) {
throw `Bounding box error`
}
const elementSize =
(geometry.boundingBox.max.x - geometry.boundingBox.min.x) / (width - 1)
return new Heightfield(matrix, {elementSize})
}
static convexPolyhedron(geometry: BufferGeometry) {
// Perturb.
const eps = 1e-4
for (let i = 0; i < geometry.attributes['position'].count; i++) {
geometry.attributes['position'].setXYZ(
i,
geometry.attributes['position'].getX(i) + (Math.random() - 0.5) * eps,
geometry.attributes['position'].getY(i) + (Math.random() - 0.5) * eps,
geometry.attributes['position'].getZ(i) + (Math.random() - 0.5) * eps
)
}
// Compute the 3D convex hull and collect convex hull vertices and faces.
const convexHull = new ConvexHull().setFromObject(new Mesh(geometry))
const vertexToIndex = new Map()
const verticeTuples = convexHull.vertices.map(({point}) => {
return [point.x, point.y, point.z]
})
convexHull.vertices.forEach((vertex, index) =>
vertexToIndex.set(vertex, index)
)
const faces: Vector3Tuple[] = convexHull.faces.map((f) => {
const indices = []
let edge = f.edge
do {
const vertexIndex = vertexToIndex.get(edge.vertex)
indices.push(vertexIndex)
edge = edge.next
} while (edge !== f.edge)
return indices as Vector3Tuple
})
const normalTuples: Vector3Tuple[] = convexHull.faces.map(({normal}) => {
return [normal.x, normal.y, normal.z]
})
const vertices = verticeTuples.map((v) => new Vec3(...v))
const normals = normalTuples.map((v) => new Vec3(...v))
return new ConvexPolyhedron({vertices, normals, faces})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment