mesh-simplifier
Version:
Collection of mesh simplification methods written in Typescript
454 lines (365 loc) • 12.1 kB
text/typescript
/*
* Copyright (c) 2020 NAVER Corp.
* egjs projects are licensed under the MIT license
* Original code: https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification
* License: MIT
*/
import MeshSimplifier from "../MeshSimplifier";
import Triangle from "./structs/Triangle";
import Vertex from "./structs/Vertex";
import Ref from "./structs/Ref";
import Vector3 from "~/math/Vector3";
import Face3 from "~/math/Face3";
import SymmetricMatrix from "~/math/SymmetricMatrix";
import Adapter from "~/Adapters/Adapter";
import Geometry from "~/Geometries/Geometry";
import Timer from "~/Timer";
class FastQuadric implements MeshSimplifier {
/* Options */
public targetPercentage: number;
public aggressiveness: number;
private _triangles: Triangle[];
private _vertices: Vertex[];
private _refs: Ref[];
private _timer: Timer;
public get timeConsumed() { return this._timer.diff; }
constructor({
targetPercentage = 0.5,
aggressiveness = 7
} = {}) {
this._triangles = [];
this._vertices = [];
this._refs = [];
this.targetPercentage = targetPercentage;
this.aggressiveness = aggressiveness;
this._timer = new Timer();
}
public simplify(target: Adapter | Geometry): this {
const timer = this._timer;
timer.start();
if ((target as Adapter).geometries) {
(target as Adapter).geometries.forEach(geometry => {
this._process(geometry);
});
} else {
this._process(target as Geometry);
}
timer.end();
return this;
}
private _process(geometry: Geometry) {
this._getData(geometry);
const triangles = this._triangles;
const vertices = this._vertices;
const refs = this._refs;
const targetPercentage = this.targetPercentage;
const aggressiveness = this.aggressiveness;
const targetCount = this._triangles.length * targetPercentage;
triangles.forEach(triangle => triangle.deleted = false);
let deletedTriangles: number = 0;
const deleted0: boolean[] = [];
const deleted1: boolean[] = [];
const triangleCount = triangles.length;
for (let iteration = 0; iteration < 100; iteration++) {
// Break when target number of triangles reached
if (triangleCount - deletedTriangles <= targetCount) break;
// Update mesh once in a while
if (iteration % 5 === 0) {
this._updateMesh(iteration);
}
// Clear dirty flag
triangles.forEach(triangle => triangle.dirty = false);
//
// All triangles with edges below the threshold will be removed
//
// The following numbers works well for most models.
// If it does not, try to adjust the 3 parameters
//
const threshold = 0.000000001 * Math.pow(iteration + 3, aggressiveness);
for (let i = triangles.length - 1; i >= 0; i--) {
const t = triangles[i];
if (t.err[3] > threshold || t.deleted || t.dirty) continue;
for (let j = 0; j < 3; j++) {
if (t.err[j] < threshold) {
const i0 = t.v[j];
const i1 = t.v[(j + 1) % 3];
const v0 = vertices[i0];
const v1 = vertices[i1];
// Border check
if (v0.border || v1.border) continue;
// Compute vertex to collapse to
const p = new Vector3();
this._calculateError(i0, i1, p);
deleted0.splice(0); // normals temporarily
deleted1.splice(0); // normals temporarily
// Don't remove if flipped
if (this._flipped(p, i1, v0, deleted0)) continue;
if (this._flipped(p, i0, v1, deleted1)) continue;
// Not flipped, so remove edge
v0.p = p;
v0.q.add(v1.q);
const tstart = refs.length;
deletedTriangles += this._updateTriangles(i0, v0, deleted0);
deletedTriangles += this._updateTriangles(i0, v1, deleted1);
const tcount = refs.length - tstart;
v0.tstart = tstart;
v0.tcount = tcount;
break;
}
}
// Done?
if (triangleCount - deletedTriangles <= targetCount) break;
}
}
this._compactMesh();
this._setData(geometry);
}
private _getData(geometry: Geometry) {
const data = geometry.prepare();
this._vertices = data.vertices.map((v, idx) => {
const vertex = new Vertex(idx);
vertex.p.copy(v);
return vertex;
});
this._triangles = data.faces.map((f, idx) => {
const triangle = new Triangle(idx);
triangle.v = [f.a, f.b, f.c];
return triangle;
});
this._refs = [];
}
private _setData(geometry: Geometry) {
const triangles = this._triangles;
const vertices = this._vertices.map(vertex => vertex.p);
const faces = triangles.map(triangle => {
const v = triangle.v;
return new Face3(v[0], v[1], v[2]);
});
const unculledVertices = this._vertices.map(v => v.originalIndex);
const unculledFaces = this._triangles.map(f => f.originalIndex);
geometry.update({
vertices,
faces,
unculledVertices,
unculledFaces,
});
}
private _flipped(p: Vector3, i: number, v: Vertex, deleted: boolean[]): boolean {
const triangles = this._triangles;
const vertices = this._vertices;
const refs = this._refs;
for (let k = 0; k < v.tcount; k++) {
const ref = refs[v.tstart + k];
const t = triangles[ref.tid];
if (t.deleted) continue;
const s = ref.tvertex;
const id1 = t.v[(s + 1) % 3];
const id2 = t.v[(s + 2) % 3];
if (id1 === i || id2 === i) {
deleted[k] = true;
continue;
}
const d1 = Vector3.subVectors(vertices[id1].p, p);
const d2 = Vector3.subVectors(vertices[id2].p, p);
d1.normalize();
d2.normalize();
if (Math.abs(d1.dot(d2)) > 0.999) return true;
const n = new Vector3().copy(d1).cross(d2);
n.normalize();
deleted[k] = false;
if (n.dot(t.n) < 0.2) return true;
}
return false;
}
private _updateTriangles(i: number, v: Vertex, deleted: boolean[]): number {
const triangles = this._triangles;
const refs = this._refs;
const p = new Vector3();
let deletedCount = 0;
for (let k = 0; k < v.tcount; k++) {
const r = refs[v.tstart + k];
const t = triangles[r.tid];
if (t.deleted) continue;
if (deleted[k]) {
t.deleted = true;
deletedCount++;
continue;
}
t.v[r.tvertex] = i;
t.dirty = true;
t.err[0] = this._calculateError(t.v[0], t.v[1], p);
t.err[1] = this._calculateError(t.v[1], t.v[2], p);
t.err[2] = this._calculateError(t.v[2], t.v[0], p);
t.err[3] = Math.min(t.err[0], t.err[1], t.err[2]);
refs.push(r);
}
return deletedCount;
}
private _updateMesh(iteration: number) {
const vertices = this._vertices;
const refs = this._refs;
if (iteration > 0) {
// compact triangles
this._triangles = this._triangles.filter(triangle => !triangle.deleted);
} else {
//
// Init Quadrics by Plane & Edge Errors
//
// required at the beginning ( iteration == 0 )
// recomputing during the simplification is not required,
// but mostly improves the result for closed meshes
//
vertices.forEach(vertex => vertex.q = new SymmetricMatrix());
this._triangles.forEach(t => {
const p = t.v.map(v => vertices[v].p);
const n = Vector3.subVectors(p[1], p[0])
.cross(Vector3.subVectors(p[2], p[0]))
.normalize();
t.n = n;
const tmp = SymmetricMatrix.makePlane(n.x, n.y, n.z, -n.dot(p[0]));
t.v.forEach(v => vertices[v].q.add(tmp));
});
this._triangles.forEach(t => {
const p = new Vector3();
t.v.forEach((v, i) => {
t.err[i] = this._calculateError(v, t.v[(i + 1) % 3], p);
});
});
}
// Init Reference ID list
vertices.forEach(vertex => {
vertex.tstart = 0;
vertex.tcount = 0;
});
const triangles = this._triangles;
triangles.forEach(triangle => {
triangle.v.forEach(v => vertices[v].tcount++);
});
let tstart = 0;
vertices.forEach(v => {
v.tstart = tstart;
tstart += v.tcount;
v.tcount = 0;
});
// Write References
for (let i = refs.length; i < triangles.length * 3; i++) {
refs[i] = new Ref();
}
triangles.forEach((t, i) => {
for (let j = 0; j < 3; j++) {
const v = vertices[t.v[j]];
refs[v.tstart + v.tcount].tid = i;
refs[v.tstart + v.tcount].tvertex = j;
v.tcount++;
};
});
// Identify boundary : vertices[].border=0,1
if (iteration === 0) {
vertices.forEach(vertex => vertex.border = false);
vertices.forEach(v => {
// clear
const vcount: number[] = [];
const vids: number[] = [];
for (let i = 0; i < v.tcount; i++) {
const k = refs[v.tstart + i].tid;
const t = triangles[k];
for (let j = 0; j < 3; j++) {
const id = t.v[j];
let ofs = 0;
while (ofs < vcount.length) {
if (vids[ofs] === id) break;
ofs++;
}
if (ofs === vcount.length) {
vcount.push(1);
vids.push(id);
} else {
vcount[ofs]++;
}
};
}
for (let j = 0; j < vcount.length; j++) {
if (vcount[j] === 1) {
vertices[vids[j]].border = true;
}
};
});
}
}
// Error for one edge
private _calculateError(idV1: number, idV2: number, result: Vector3) {
// Compute interpolated vertex
const vertices = this._vertices;
const v1 = vertices[idV1];
const v2 = vertices[idV2];
const q = new SymmetricMatrix().copy(v1.q).add(v2.q);
const border = v1.border && v2.border;
const det = q.det(0, 1, 2, 1, 4, 5, 2, 5, 7);
let error: number = 0;
if (det !== 0 && !border) {
// q_delta is invertible
result.x = -1 / det * (q.det(1, 2, 3, 4, 5, 6, 5, 7, 8)); // vx = A41/det(q_delta)
result.y = 1 / det * (q.det(0, 2, 3, 1, 5, 6, 2, 7, 8)); // vy = A42/det(q_delta)
result.z = -1 / det * (q.det(0, 1, 3, 1, 4, 6, 2, 5, 8)); // vz = A43/det(q_delta)
error = this._vertexError(q, result);
} else {
const p1 = v1.p;
const p2 = v2.p;
const p3 = new Vector3(
(p1.x + p2.x) * 0.5,
(p1.y + p2.y) * 0.5,
(p1.z + p2.z) * 0.5,
);
const error1 = this._vertexError(q, p1);
const error2 = this._vertexError(q, p2);
const error3 = this._vertexError(q, p3);
error = Math.min(error1, error2, error3);
if (error1 === error) result.copy(p1);
if (error2 === error) result.copy(p2);
if (error3 === error) result.copy(p3);
}
return error;
}
private _vertexError(q: SymmetricMatrix, v: Vector3): number {
const {x, y, z} = v;
const m = q.m;
const err = m[0] * x * x
+ 2 * m[1] * x * y
+ 2 * m[2] * x * z
+ 2 * m[3] * x
+ m[4] * y * y
+ 2 * m[5] * y * z
+ 2 * m[6] * y
+ m[7] * z * z
+ 2 * m[8] * z
+ m[9];
return err;
}
private _compactMesh() {
this._triangles = this._triangles.filter(t => !t.deleted);
const triangles = this._triangles;
const vertices = this._vertices;
vertices.forEach(vertex => vertex.tcount = 0);
triangles.forEach(triangle => {
triangle.v.forEach(v => {
vertices[v].tcount = 1;
});
});
let dst = 0;
vertices.forEach(vertex => {
if (vertex.tcount > 0) {
vertex.tstart = dst;
vertices[dst].originalIndex = vertex.originalIndex;
vertices[dst].p = vertex.p;
dst++;
}
});
triangles.forEach(t => {
t.v.forEach((v, i) => {
t.v[i] = vertices[v].tstart;
});
});
vertices.splice(dst); // resize
}
}
export default FastQuadric;