Sharing this because I was looking for it and couldn’t find it so I figured it out based on other code snippets online. Here’s a class to make rounded boxes.
import {
Application, calculateNormals, createMesh, Mesh, Vec3,
} from 'playcanvas';
// reference: https://github.com/nepluno/RoundCornerBox/blob/master/RoundCornerBox/RoundCornerBox.hpp
export default class RoundedBox {
app: Application;
vertices: Vec3[];
indices: Vec3[];
m_nEdge: number;
m_index_to_verts: number[];
m_radius: number;
private tmp: Vec3 = new Vec3();
constructor(N: number, dimension: Vec3, radius: number, app: Application) {
this.vertices = [];
this.indices = [];
this.app = app;
const b = dimension
.clone()
.divScalar(2)
.subScalar(radius);
const nEdge = 2 * (N + 1);
this.m_nEdge = nEdge;
this.m_index_to_verts = Array(nEdge * nEdge * nEdge).fill(-1);
this.m_radius = radius;
const dx = radius / N;
const sign = [-1.0, 1.0];
const ks = [0, N * 2 + 1];
// xy-planes
for (let kidx = 0; kidx < 2; ++kidx) {
const k = ks[kidx];
const origin = this.tmp.clone().set(
-b.x - radius,
-b.y - radius,
(b.z + radius) * sign[kidx],
);
for (let j = 0; j <= N; ++j) {
for (let i = 0; i <= N; ++i) {
let pos = origin.clone().add(this.tmp.clone().set(dx * i, dx * j, 0.0));
this.addVertex(i, j, k, pos, this.tmp.clone().set(-b.x, -b.y, b.z * sign[kidx]));
pos = origin.clone().add(this.tmp.clone().set(dx * i + 2.0 * b.x + radius, dx * j, 0.0));
this.addVertex(i + N + 1, j, k, pos, this.tmp.clone().set(b.x, -b.y, b.z * sign[kidx]));
pos = origin.clone().add(this.tmp.clone().set(
dx * i + 2.0 * b.x + radius,
dx * j + 2.0 * b.y + radius,
0.0,
));
this.addVertex(
i + N + 1,
j + N + 1,
k,
pos,
this.tmp.clone().set(b.x, b.y, b.z * sign[kidx]),
);
pos = origin.clone().add(this.tmp.clone().set(dx * i, dx * j + 2.0 * b.y + radius, 0.0));
this.addVertex(i, j + N + 1, k, pos, this.tmp.clone().set(-b.x, b.y, b.z * sign[kidx]));
}
}
// corners
for (let j = 0; j < N; ++j) {
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(i, j, k),
this.translateIndices(i + 1, j + 1, k),
this.translateIndices(i, j + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i, j, k),
this.translateIndices(i + 1, j, k),
this.translateIndices(i + 1, j + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i, j + N + 1, k),
this.translateIndices(i + 1, j + N + 2, k),
this.translateIndices(i, j + N + 2, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i, j + N + 1, k),
this.translateIndices(i + 1, j + N + 1, k),
this.translateIndices(i + 1, j + N + 2, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, j + N + 1, k),
this.translateIndices(i + N + 2, j + N + 2, k),
this.translateIndices(i + N + 1, j + N + 2, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, j + N + 1, k),
this.translateIndices(i + N + 2, j + N + 1, k),
this.translateIndices(i + N + 2, j + N + 2, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, j, k),
this.translateIndices(i + N + 2, j + 1, k),
this.translateIndices(i + N + 1, j + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, j, k),
this.translateIndices(i + N + 2, j, k),
this.translateIndices(i + N + 2, j + 1, k),
kidx === 0,
);
}
}
// sides
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(i, N, k),
this.translateIndices(i + 1, N + 1, k),
this.translateIndices(i, N + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i, N, k),
this.translateIndices(i + 1, N, k),
this.translateIndices(i + 1, N + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(N, i, k),
this.translateIndices(N + 1, i + 1, k),
this.translateIndices(N, i + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(N, i, k),
this.translateIndices(N + 1, i, k),
this.translateIndices(N + 1, i + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, N, k),
this.translateIndices(i + N + 2, N + 1, k),
this.translateIndices(i + N + 1, N + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(i + N + 1, N, k),
this.translateIndices(i + N + 2, N, k),
this.translateIndices(i + N + 2, N + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(N, i + N + 1, k),
this.translateIndices(N + 1, i + N + 2, k),
this.translateIndices(N, i + N + 2, k),
kidx === 0,
);
this.addFace(
this.translateIndices(N, i + N + 1, k),
this.translateIndices(N + 1, i + N + 1, k),
this.translateIndices(N + 1, i + N + 2, k),
kidx === 0,
);
}
// central
this.addFace(
this.translateIndices(N, N, k),
this.translateIndices(N + 1, N + 1, k),
this.translateIndices(N, N + 1, k),
kidx === 0,
);
this.addFace(
this.translateIndices(N, N, k),
this.translateIndices(N + 1, N, k),
this.translateIndices(N + 1, N + 1, k),
kidx === 0,
);
}
// xz-planes
for (let kidx = 0; kidx < 2; ++kidx) {
const k = ks[kidx];
const origin = this.tmp.clone().set(
-b.x - radius,
(b.y + radius) * sign[kidx],
-b.z - radius,
);
for (let j = 0; j <= N; ++j) {
for (let i = 0; i <= N; ++i) {
let pos = origin.clone().add(this.tmp.clone().set(dx * i, 0.0, dx * j));
this.addVertex(i, k, j, pos, this.tmp.clone().set(-b.x, b.y * sign[kidx], -b.z));
pos = origin.clone().add(this.tmp.clone().set(dx * i + 2.0 * b.x + radius, 0.0, dx * j));
this.addVertex(i + N + 1, k, j, pos, this.tmp.clone().set(b.x, b.y * sign[kidx], -b.z));
pos = origin.clone().add(this.tmp.clone().set(
dx * i + 2.0 * b.x + radius,
0.0,
dx * j + 2.0 * b.z + radius,
));
this.addVertex(
i + N + 1,
k,
j + N + 1,
pos,
this.tmp.clone().set(b.x, b.y * sign[kidx], b.z),
);
pos = origin.clone().add(this.tmp.clone().set(dx * i, 0.0, dx * j + 2.0 * b.z + radius));
this.addVertex(i, k, j + N + 1, pos, this.tmp.clone().set(-b.x, b.y * sign[kidx], b.z));
}
}
// corners
for (let j = 0; j < N; ++j) {
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(i, k, j),
this.translateIndices(i + 1, k, j + 1),
this.translateIndices(i, k, j + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i, k, j),
this.translateIndices(i + 1, k, j),
this.translateIndices(i + 1, k, j + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i, k, j + N + 1),
this.translateIndices(i + 1, k, j + N + 2),
this.translateIndices(i, k, j + N + 2),
kidx === 1,
);
this.addFace(
this.translateIndices(i, k, j + N + 1),
this.translateIndices(i + 1, k, j + N + 1),
this.translateIndices(i + 1, k, j + N + 2),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, j + N + 1),
this.translateIndices(i + N + 2, k, j + N + 2),
this.translateIndices(i + N + 1, k, j + N + 2),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, j + N + 1),
this.translateIndices(i + N + 2, k, j + N + 1),
this.translateIndices(i + N + 2, k, j + N + 2),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, j),
this.translateIndices(i + N + 2, k, j + 1),
this.translateIndices(i + N + 1, k, j + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, j),
this.translateIndices(i + N + 2, k, j),
this.translateIndices(i + N + 2, k, j + 1),
kidx === 1,
);
}
}
// sides
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(i, k, N),
this.translateIndices(i + 1, k, N + 1),
this.translateIndices(i, k, N + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i, k, N),
this.translateIndices(i + 1, k, N),
this.translateIndices(i + 1, k, N + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(N, k, i),
this.translateIndices(N + 1, k, i + 1),
this.translateIndices(N, k, i + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(N, k, i),
this.translateIndices(N + 1, k, i),
this.translateIndices(N + 1, k, i + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, N),
this.translateIndices(i + N + 2, k, N + 1),
this.translateIndices(i + N + 1, k, N + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(i + N + 1, k, N),
this.translateIndices(i + N + 2, k, N),
this.translateIndices(i + N + 2, k, N + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(N, k, i + N + 1),
this.translateIndices(N + 1, k, i + N + 2),
this.translateIndices(N, k, i + N + 2),
kidx === 1,
);
this.addFace(
this.translateIndices(N, k, i + N + 1),
this.translateIndices(N + 1, k, i + N + 1),
this.translateIndices(N + 1, k, i + N + 2),
kidx === 1,
);
}
// central
this.addFace(
this.translateIndices(N, k, N),
this.translateIndices(N + 1, k, N + 1),
this.translateIndices(N, k, N + 1),
kidx === 1,
);
this.addFace(
this.translateIndices(N, k, N),
this.translateIndices(N + 1, k, N),
this.translateIndices(N + 1, k, N + 1),
kidx === 1,
);
}
// yz-planes
for (let kidx = 0; kidx < 2; ++kidx) {
const k = ks[kidx];
const origin = this.tmp.clone().set(
(b.x + radius) * sign[kidx],
-b.y - radius,
-b.z - radius,
);
for (let j = 0; j <= N; ++j) {
for (let i = 0; i <= N; ++i) {
let pos = origin.clone().add(this.tmp.clone().set(0.0, dx * i, dx * j));
this.addVertex(k, i, j, pos, this.tmp.clone().set(b.x * sign[kidx], -b.y, -b.z));
pos = origin.clone().add(this.tmp.clone().set(0.0, dx * i + 2.0 * b.y + radius, dx * j));
this.addVertex(k, i + N + 1, j, pos, this.tmp.clone().set(b.x * sign[kidx], b.y, -b.z));
pos = origin.clone().add(this.tmp.clone().set(
0.0,
dx * i + 2.0 * b.y + radius,
dx * j + 2.0 * b.z + radius,
));
this.addVertex(
k,
i + N + 1,
j + N + 1,
pos,
this.tmp.clone().set(b.x * sign[kidx], b.y, b.z),
);
pos = origin.clone().add(this.tmp.clone().set(0.0, dx * i, dx * j + 2.0 * b.z + radius));
this.addVertex(k, i, j + N + 1, pos, this.tmp.clone().set(b.x * sign[kidx], -b.y, b.z));
}
}
// corners
for (let j = 0; j < N; ++j) {
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(k, i, j),
this.translateIndices(k, i + 1, j + 1),
this.translateIndices(k, i, j + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i, j),
this.translateIndices(k, i + 1, j),
this.translateIndices(k, i + 1, j + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i, j + N + 1),
this.translateIndices(k, i + 1, j + N + 2),
this.translateIndices(k, i, j + N + 2),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i, j + N + 1),
this.translateIndices(k, i + 1, j + N + 1),
this.translateIndices(k, i + 1, j + N + 2),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, j + N + 1),
this.translateIndices(k, i + N + 2, j + N + 2),
this.translateIndices(k, i + N + 1, j + N + 2),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, j + N + 1),
this.translateIndices(k, i + N + 2, j + N + 1),
this.translateIndices(k, i + N + 2, j + N + 2),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, j),
this.translateIndices(k, i + N + 2, j + 1),
this.translateIndices(k, i + N + 1, j + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, j),
this.translateIndices(k, i + N + 2, j),
this.translateIndices(k, i + N + 2, j + 1),
kidx === 0,
);
}
}
// sides
for (let i = 0; i < N; ++i) {
this.addFace(
this.translateIndices(k, i, N),
this.translateIndices(k, i + 1, N + 1),
this.translateIndices(k, i, N + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i, N),
this.translateIndices(k, i + 1, N),
this.translateIndices(k, i + 1, N + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, N, i),
this.translateIndices(k, N + 1, i + 1),
this.translateIndices(k, N, i + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, N, i),
this.translateIndices(k, N + 1, i),
this.translateIndices(k, N + 1, i + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, N),
this.translateIndices(k, i + N + 2, N + 1),
this.translateIndices(k, i + N + 1, N + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, i + N + 1, N),
this.translateIndices(k, i + N + 2, N),
this.translateIndices(k, i + N + 2, N + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, N, i + N + 1),
this.translateIndices(k, N + 1, i + N + 2),
this.translateIndices(k, N, i + N + 2),
kidx === 0,
);
this.addFace(
this.translateIndices(k, N, i + N + 1),
this.translateIndices(k, N + 1, i + N + 1),
this.translateIndices(k, N + 1, i + N + 2),
kidx === 0,
);
}
// central
this.addFace(
this.translateIndices(k, N, N),
this.translateIndices(k, N + 1, N + 1),
this.translateIndices(k, N, N + 1),
kidx === 0,
);
this.addFace(
this.translateIndices(k, N, N),
this.translateIndices(k, N + 1, N),
this.translateIndices(k, N + 1, N + 1),
kidx === 0,
);
}
}
addVertex(i: number, j: number, k: number, pos: Vec3, basePos: Vec3) {
const pidx = k * this.m_nEdge * this.m_nEdge + j * this.m_nEdge + i;
if (this.m_index_to_verts[pidx] < 0) {
this.m_index_to_verts[pidx] = this.vertices.length;
const dir = pos.sub(basePos);
if (dir.length() > 0.0) {
dir.normalize();
this.vertices.push(basePos.add(dir.mulScalar(this.m_radius)));
} else { this.vertices.push(pos); }
}
}
translateIndices(i: number, j: number, k: number) {
const pidx = k * this.m_nEdge * this.m_nEdge + j * this.m_nEdge + i;
return this.m_index_to_verts[pidx];
}
addFace(i: number, j: number, k: number, inversed: boolean) {
if (inversed) {
this.indices.push(this.tmp.clone().set(i, k, j));
} else {
this.indices.push(this.tmp.clone().set(i, j, k));
}
}
toMesh() {
const vertexPositions = [];
const vertexIndices = [];
this.vertices.forEach((vertex) => {
vertexPositions.push(vertex.x, vertex.y, vertex.z);
});
this.indices.forEach((idx) => {
vertexIndices.push(idx.x, idx.y, idx.z);
});
const mesh = createMesh(
this.app.graphicsDevice,
vertexPositions,
{
indices: vertexIndices,
normals: calculateNormals(vertexPositions, vertexIndices),
},
);
return mesh;
}
static create(N: number, dimension: Vec3, radius: number, app: Application): Mesh {
const box = new RoundedBox(N, dimension, radius, app);
return box.toMesh();
}
}