Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate skirt vertices and triangles #12

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,169 @@ class Tile {

return {vertices, triangles};
}

getMeshWithSkirts(maxError = 0) {
const {gridSize: size, indices} = this.martini;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle with reducing function bloat, and performance is very important to my use case too. I also believe it's beneficial to remove duplicate complex code and adding skits as an option to getMesh, if possible. Would you be open to combining the functions into one, together?

If you'd prefer to keep it this way, that is also fine. Are you looking for a 3rd party review? I can pull this down and test it out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem lies in getMesh being so concise and elegant :)

When I started adding if (skirt) blocks to handle the custom code I felt like I was making a mess. Once I started changing the vertices and triangle arrays as well as the return object (and more than doubling the length of the original function) I figured it might be neater to separate - though harder to maintain because of the code duplication of the core logic. I also thought a new user would have less trouble understanding getMesh by itself than getMesh with the skirt flag. @mourner any preference?

I'm guessing that the performance loss of the if (skirt) statement inside the recursive countElements function would be negligible but would have to bench at scale to be sure.

p.s. I also don't know if the way I'm generating the skirts is sound or if there's shortcuts etc that might make all of this moot

const {errors} = this;
let numVertices = 0;
let numTriangles = 0;
const max = size - 1;
let aIndex, bIndex, cIndex = 0;
// Skirt indices
const leftSkirtIndices = [];
const rightSkirtIndices = [];
const bottomSkirtIndices = [];
const topSkirtIndices = [];
// use an index grid to keep track of vertices that were already used to avoid duplication
indices.fill(0);

// retrieve mesh in two stages that both traverse the error map:
// - countElements: find used vertices (and assign each an index), and count triangles (for minimum allocation)
// - processTriangle: fill the allocated vertices & triangles typed arrays
function countElements(ax, ay, bx, by, cx, cy) {
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
countElements(cx, cy, ax, ay, mx, my);
countElements(bx, by, cx, cy, mx, my);
} else {
aIndex = ay * size + ax;
bIndex = by * size + bx;
cIndex = cy * size + cx;

if (indices[aIndex] === 0) {
if (ax === 0)
leftSkirtIndices.push(numVertices);
else if (ax === max)
rightSkirtIndices.push(numVertices);
if (ay === 0)
bottomSkirtIndices.push(numVertices);
else if (ay === max)
topSkirtIndices.push(numVertices);
indices[aIndex] = ++numVertices;
}
if (indices[bIndex] === 0) {
if (bx === 0)
leftSkirtIndices.push(numVertices);
else if (bx === max)
rightSkirtIndices.push(numVertices);
if (by === 0)
bottomSkirtIndices.push(numVertices);
else if (by === max)
topSkirtIndices.push(numVertices);
indices[bIndex] = ++numVertices;
}
if (indices[cIndex] === 0) {
if (cx === 0)
leftSkirtIndices.push(numVertices);
else if (cx === max)
rightSkirtIndices.push(numVertices);
if (cy === 0)
bottomSkirtIndices.push(numVertices);
else if (cy === max)
topSkirtIndices.push(numVertices);
indices[cIndex] = ++numVertices;
}
numTriangles++;
}
}
countElements(0, 0, max, max, max, 0);
countElements(max, max, 0, 0, 0, max);

const numTotalVertices = (
numVertices +
leftSkirtIndices.length +
rightSkirtIndices.length +
bottomSkirtIndices.length +
topSkirtIndices.length) * 2;
const numTotalTriangles = (
numTriangles +
((leftSkirtIndices.length - 1) * 2) +
((rightSkirtIndices.length - 1) * 2) +
((bottomSkirtIndices.length - 1) * 2) +
((topSkirtIndices.length - 1) * 2)) * 3;

const vertices = new Uint16Array(numTotalVertices);
const triangles = new Uint32Array(numTotalTriangles);

let triIndex = 0;
function processTriangle(ax, ay, bx, by, cx, cy) {
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
// triangle doesn't approximate the surface well enough; drill down further
processTriangle(cx, cy, ax, ay, mx, my);
processTriangle(bx, by, cx, cy, mx, my);

} else {
// add a triangle
const a = indices[ay * size + ax] - 1;
const b = indices[by * size + bx] - 1;
const c = indices[cy * size + cx] - 1;

vertices[2 * a] = ax;
vertices[2 * a + 1] = ay;

vertices[2 * b] = bx;
vertices[2 * b + 1] = by;

vertices[2 * c] = cx;
vertices[2 * c + 1] = cy;
triangles[triIndex++] = a;
triangles[triIndex++] = b;
triangles[triIndex++] = c;
}
}
processTriangle(0, 0, max, max, max, 0);
processTriangle(max, max, 0, 0, 0, max);

// Sort skirt indices to create adjacent triangles
leftSkirtIndices.sort((a, b) => vertices[2 * a + 1] - vertices[2 * b + 1]);

// Reverse (b - a) to match triangle winding
rightSkirtIndices.sort((a, b) => vertices[2 * b + 1] - vertices[2 * a + 1]);

bottomSkirtIndices.sort((a, b) => vertices[2 * b] - vertices[2 * a]);

// Reverse (b - a) to match triangle winding
topSkirtIndices.sort((a, b) => vertices[2 * a] - vertices[2 * b]);

let skirtIndex = numVertices * 2;
let currIndex, nextIndex, currentSkirt, nextSkirt, skirtLength = 0;

// Add skirt vertices from index of last mesh vertex
function constructSkirt(skirt) {
skirtLength = skirt.length;
// Loop through indices in groups of two to generate triangles
for (let i = 0; i < skirtLength - 1; i++) {
currIndex = skirt[i];
nextIndex = skirt[i + 1];
currentSkirt = skirtIndex / 2;
nextSkirt = (skirtIndex + 2) / 2;
vertices[skirtIndex++] = vertices[2 * currIndex];
vertices[skirtIndex++] = vertices[2 * currIndex + 1];

triangles[triIndex++] = currIndex;
triangles[triIndex++] = currentSkirt;
triangles[triIndex++] = nextIndex;

triangles[triIndex++] = currentSkirt;
triangles[triIndex++] = nextSkirt;
triangles[triIndex++] = nextIndex;
}
// Add vertices of last skirt not added above (i < skirtLength - 1)
vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1]];
vertices[skirtIndex++] = vertices[2 * skirt[skirtLength - 1] + 1];
}

constructSkirt(leftSkirtIndices);
constructSkirt(rightSkirtIndices);
constructSkirt(bottomSkirtIndices);
constructSkirt(topSkirtIndices);

// Return vertices and triangles and index into vertices array where skirts start
return {vertices, triangles, numVerticesWithoutSkirts: numVertices};
}
}