@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
708 lines • 29.2 kB
JavaScript
import { Vector3 } from "../Maths/math.vector.js";
import { VertexBuffer } from "../Buffers/buffer.js";
import { SubMesh } from "../Meshes/subMesh.js";
import { Mesh } from "../Meshes/mesh.js";
import { AsyncLoop } from "../Misc/tools.js";
import { Epsilon } from "../Maths/math.constants.js";
/**
* Class used to specify simplification options
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/simplifyingMeshes
*/
export class SimplificationSettings {
/**
* Creates a SimplificationSettings
* @param quality expected quality
* @param distance distance when this optimized version should be used
* @param optimizeMesh already optimized mesh
*/
constructor(
/** expected quality */
quality,
/** distance when this optimized version should be used */
distance,
/** already optimized mesh */
optimizeMesh) {
this.quality = quality;
this.distance = distance;
this.optimizeMesh = optimizeMesh;
}
}
/**
* Queue used to order the simplification tasks
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/simplifyingMeshes
*/
export class SimplificationQueue {
/**
* Creates a new queue
*/
constructor() {
this.running = false;
this._simplificationArray = [];
}
/**
* Adds a new simplification task
* @param task defines a task to add
*/
addTask(task) {
this._simplificationArray.push(task);
}
/**
* Execute next task
*/
executeNext() {
const task = this._simplificationArray.pop();
if (task) {
this.running = true;
this.runSimplification(task);
}
else {
this.running = false;
}
}
/**
* Execute a simplification task
* @param task defines the task to run
*/
runSimplification(task) {
if (task.parallelProcessing) {
//parallel simplifier
for (const setting of task.settings) {
const simplifier = this._getSimplifier(task);
simplifier.simplify(setting, (newMesh) => {
if (setting.distance !== undefined) {
task.mesh.addLODLevel(setting.distance, newMesh);
}
newMesh.isVisible = true;
//check if it is the last
if (setting.quality === task.settings[task.settings.length - 1].quality && task.successCallback) {
//all done, run the success callback.
task.successCallback();
}
this.executeNext();
});
}
}
else {
//single simplifier.
const simplifier = this._getSimplifier(task);
const runDecimation = (setting, callback) => {
simplifier.simplify(setting, (newMesh) => {
if (setting.distance !== undefined) {
task.mesh.addLODLevel(setting.distance, newMesh);
}
newMesh.isVisible = true;
//run the next quality level
callback();
});
};
AsyncLoop.Run(task.settings.length, (loop) => {
runDecimation(task.settings[loop.index], () => {
loop.executeNext();
});
}, () => {
//execution ended, run the success callback.
if (task.successCallback) {
task.successCallback();
}
this.executeNext();
});
}
}
_getSimplifier(task) {
switch (task.simplificationType) {
case 0 /* SimplificationType.QUADRATIC */:
default:
return new QuadraticErrorSimplification(task.mesh);
}
}
}
/**
* The implemented types of simplification
* At the moment only Quadratic Error Decimation is implemented
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/simplifyingMeshes
*/
export var SimplificationType;
(function (SimplificationType) {
/** Quadratic error decimation */
SimplificationType[SimplificationType["QUADRATIC"] = 0] = "QUADRATIC";
})(SimplificationType || (SimplificationType = {}));
class DecimationTriangle {
constructor(_vertices) {
this._vertices = _vertices;
this.error = new Array(4);
this.deleted = false;
this.isDirty = false;
this.deletePending = false;
this.borderFactor = 0;
}
}
class DecimationVertex {
constructor(position, id) {
this.position = position;
this.id = id;
this.isBorder = true;
this.q = new QuadraticMatrix();
this.triangleCount = 0;
this.triangleStart = 0;
this.originalOffsets = [];
}
updatePosition(newPosition) {
this.position.copyFrom(newPosition);
}
}
class QuadraticMatrix {
constructor(data) {
this.data = new Array(10);
for (let i = 0; i < 10; ++i) {
if (data && data[i]) {
this.data[i] = data[i];
}
else {
this.data[i] = 0;
}
}
}
det(a11, a12, a13, a21, a22, a23, a31, a32, a33) {
const det = this.data[a11] * this.data[a22] * this.data[a33] +
this.data[a13] * this.data[a21] * this.data[a32] +
this.data[a12] * this.data[a23] * this.data[a31] -
this.data[a13] * this.data[a22] * this.data[a31] -
this.data[a11] * this.data[a23] * this.data[a32] -
this.data[a12] * this.data[a21] * this.data[a33];
return det;
}
addInPlace(matrix) {
for (let i = 0; i < 10; ++i) {
this.data[i] += matrix.data[i];
}
}
addArrayInPlace(data) {
for (let i = 0; i < 10; ++i) {
this.data[i] += data[i];
}
}
add(matrix) {
const m = new QuadraticMatrix();
for (let i = 0; i < 10; ++i) {
m.data[i] = this.data[i] + matrix.data[i];
}
return m;
}
static FromData(a, b, c, d) {
return new QuadraticMatrix(QuadraticMatrix.DataFromNumbers(a, b, c, d));
}
//returning an array to avoid garbage collection
static DataFromNumbers(a, b, c, d) {
return [a * a, a * b, a * c, a * d, b * b, b * c, b * d, c * c, c * d, d * d];
}
}
class Reference {
constructor(vertexId, triangleId) {
this.vertexId = vertexId;
this.triangleId = triangleId;
}
}
/**
* An implementation of the Quadratic Error simplification algorithm.
* Original paper : http://www1.cs.columbia.edu/~cs4162/html05s/garland97.pdf
* Ported mostly from QSlim and http://voxels.blogspot.de/2014/05/quadric-mesh-simplification-with-source.html to babylon JS
* @author RaananW
* @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/simplifyingMeshes
*/
export class QuadraticErrorSimplification {
/**
* Creates a new QuadraticErrorSimplification
* @param _mesh defines the target mesh
*/
constructor(_mesh) {
this._mesh = _mesh;
/** Gets or sets the number pf sync iterations */
this.syncIterations = 5000;
this.aggressiveness = 7;
this.decimationIterations = 100;
this.boundingBoxEpsilon = Epsilon;
}
/**
* Simplification of a given mesh according to the given settings.
* Since this requires computation, it is assumed that the function runs async.
* @param settings The settings of the simplification, including quality and distance
* @param successCallback A callback that will be called after the mesh was simplified.
*/
simplify(settings, successCallback) {
this._initDecimatedMesh();
//iterating through the submeshes array, one after the other.
AsyncLoop.Run(this._mesh.subMeshes.length, (loop) => {
this._initWithMesh(loop.index, () => {
this._runDecimation(settings, loop.index, () => {
loop.executeNext();
});
}, settings.optimizeMesh);
}, () => {
setTimeout(() => {
successCallback(this._reconstructedMesh);
}, 0);
});
}
_runDecimation(settings, submeshIndex, successCallback) {
const targetCount = ~~(this._triangles.length * settings.quality);
let deletedTriangles = 0;
const triangleCount = this._triangles.length;
const iterationFunction = (iteration, callback) => {
setTimeout(() => {
if (iteration % 5 === 0) {
this._updateMesh(iteration === 0);
}
for (let i = 0; i < this._triangles.length; ++i) {
this._triangles[i].isDirty = false;
}
const threshold = 0.000000001 * Math.pow(iteration + 3, this.aggressiveness);
const trianglesIterator = (i) => {
const tIdx = ~~((this._triangles.length / 2 + i) % this._triangles.length);
const t = this._triangles[tIdx];
if (!t) {
return;
}
if (t.error[3] > threshold || t.deleted || t.isDirty) {
return;
}
for (let j = 0; j < 3; ++j) {
if (t.error[j] < threshold) {
const deleted0 = [];
const deleted1 = [];
const v0 = t._vertices[j];
const v1 = t._vertices[(j + 1) % 3];
if (v0.isBorder || v1.isBorder) {
continue;
}
const p = Vector3.Zero();
// var n = Vector3.Zero();
// var uv = Vector2.Zero();
// var color = new Color4(0, 0, 0, 1);
this._calculateError(v0, v1, p);
const delTr = [];
if (this._isFlipped(v0, v1, p, deleted0, delTr)) {
continue;
}
if (this._isFlipped(v1, v0, p, deleted1, delTr)) {
continue;
}
if (deleted0.indexOf(true) < 0 || deleted1.indexOf(true) < 0) {
continue;
}
const uniqueArray = [];
for (const deletedT of delTr) {
if (uniqueArray.indexOf(deletedT) === -1) {
deletedT.deletePending = true;
uniqueArray.push(deletedT);
}
}
if (uniqueArray.length % 2 !== 0) {
continue;
}
v0.q = v1.q.add(v0.q);
v0.updatePosition(p);
const tStart = this._references.length;
deletedTriangles = this._updateTriangles(v0, v0, deleted0, deletedTriangles);
deletedTriangles = this._updateTriangles(v0, v1, deleted1, deletedTriangles);
const tCount = this._references.length - tStart;
if (tCount <= v0.triangleCount) {
if (tCount) {
for (let c = 0; c < tCount; c++) {
this._references[v0.triangleStart + c] = this._references[tStart + c];
}
}
}
else {
v0.triangleStart = tStart;
}
v0.triangleCount = tCount;
break;
}
}
};
AsyncLoop.SyncAsyncForLoop(this._triangles.length, this.syncIterations, trianglesIterator, callback, () => {
return triangleCount - deletedTriangles <= targetCount;
});
}, 0);
};
AsyncLoop.Run(this.decimationIterations, (loop) => {
if (triangleCount - deletedTriangles <= targetCount) {
loop.breakLoop();
}
else {
iterationFunction(loop.index, () => {
loop.executeNext();
});
}
}, () => {
setTimeout(() => {
//reconstruct this part of the mesh
this._reconstructMesh(submeshIndex);
successCallback();
}, 0);
});
}
_initWithMesh(submeshIndex, callback, optimizeMesh) {
this._vertices = [];
this._triangles = [];
const positionData = this._mesh.getVerticesData(VertexBuffer.PositionKind);
const indices = this._mesh.getIndices();
const submesh = this._mesh.subMeshes[submeshIndex];
const findInVertices = (positionToSearch) => {
if (optimizeMesh) {
for (let ii = 0; ii < this._vertices.length; ++ii) {
if (this._vertices[ii].position.equalsWithEpsilon(positionToSearch, 0.0001)) {
return this._vertices[ii];
}
}
}
return null;
};
const vertexReferences = [];
const vertexInit = (i) => {
if (!positionData) {
return;
}
const offset = i + submesh.verticesStart;
const position = Vector3.FromArray(positionData, offset * 3);
const vertex = findInVertices(position) || new DecimationVertex(position, this._vertices.length);
vertex.originalOffsets.push(offset);
if (vertex.id === this._vertices.length) {
this._vertices.push(vertex);
}
vertexReferences.push(vertex.id);
};
//var totalVertices = mesh.getTotalVertices();
const totalVertices = submesh.verticesCount;
AsyncLoop.SyncAsyncForLoop(totalVertices, (this.syncIterations / 4) >> 0, vertexInit, () => {
const indicesInit = (i) => {
if (!indices) {
return;
}
const offset = submesh.indexStart / 3 + i;
const pos = offset * 3;
const i0 = indices[pos + 0];
const i1 = indices[pos + 1];
const i2 = indices[pos + 2];
const v0 = this._vertices[vertexReferences[i0 - submesh.verticesStart]];
const v1 = this._vertices[vertexReferences[i1 - submesh.verticesStart]];
const v2 = this._vertices[vertexReferences[i2 - submesh.verticesStart]];
const triangle = new DecimationTriangle([v0, v1, v2]);
triangle.originalOffset = pos;
this._triangles.push(triangle);
};
AsyncLoop.SyncAsyncForLoop(submesh.indexCount / 3, this.syncIterations, indicesInit, () => {
this._init(callback);
});
});
}
_init(callback) {
const triangleInit1 = (i) => {
const t = this._triangles[i];
t.normal = Vector3.Cross(t._vertices[1].position.subtract(t._vertices[0].position), t._vertices[2].position.subtract(t._vertices[0].position)).normalize();
for (let j = 0; j < 3; j++) {
t._vertices[j].q.addArrayInPlace(QuadraticMatrix.DataFromNumbers(t.normal.x, t.normal.y, t.normal.z, -Vector3.Dot(t.normal, t._vertices[0].position)));
}
};
AsyncLoop.SyncAsyncForLoop(this._triangles.length, this.syncIterations, triangleInit1, () => {
const triangleInit2 = (i) => {
const t = this._triangles[i];
for (let j = 0; j < 3; ++j) {
t.error[j] = this._calculateError(t._vertices[j], t._vertices[(j + 1) % 3]);
}
t.error[3] = Math.min(t.error[0], t.error[1], t.error[2]);
};
AsyncLoop.SyncAsyncForLoop(this._triangles.length, this.syncIterations, triangleInit2, () => {
callback();
});
});
}
_reconstructMesh(submeshIndex) {
const newTriangles = [];
let i;
for (i = 0; i < this._vertices.length; ++i) {
this._vertices[i].triangleCount = 0;
}
let t;
let j;
for (i = 0; i < this._triangles.length; ++i) {
if (!this._triangles[i].deleted) {
t = this._triangles[i];
for (j = 0; j < 3; ++j) {
t._vertices[j].triangleCount = 1;
}
newTriangles.push(t);
}
}
const newPositionData = (this._reconstructedMesh.getVerticesData(VertexBuffer.PositionKind) || []);
const newNormalData = (this._reconstructedMesh.getVerticesData(VertexBuffer.NormalKind) || []);
const newUVsData = (this._reconstructedMesh.getVerticesData(VertexBuffer.UVKind) || []);
const newColorsData = (this._reconstructedMesh.getVerticesData(VertexBuffer.ColorKind) || []);
const normalData = this._mesh.getVerticesData(VertexBuffer.NormalKind);
const uvs = this._mesh.getVerticesData(VertexBuffer.UVKind);
const colorsData = this._mesh.getVerticesData(VertexBuffer.ColorKind);
let vertexCount = 0;
for (i = 0; i < this._vertices.length; ++i) {
const vertex = this._vertices[i];
vertex.id = vertexCount;
if (vertex.triangleCount) {
for (const originalOffset of vertex.originalOffsets) {
newPositionData.push(vertex.position.x);
newPositionData.push(vertex.position.y);
newPositionData.push(vertex.position.z);
if (normalData && normalData.length) {
newNormalData.push(normalData[originalOffset * 3]);
newNormalData.push(normalData[originalOffset * 3 + 1]);
newNormalData.push(normalData[originalOffset * 3 + 2]);
}
if (uvs && uvs.length) {
newUVsData.push(uvs[originalOffset * 2]);
newUVsData.push(uvs[originalOffset * 2 + 1]);
}
if (colorsData && colorsData.length) {
newColorsData.push(colorsData[originalOffset * 4]);
newColorsData.push(colorsData[originalOffset * 4 + 1]);
newColorsData.push(colorsData[originalOffset * 4 + 2]);
newColorsData.push(colorsData[originalOffset * 4 + 3]);
}
++vertexCount;
}
}
}
const startingIndex = this._reconstructedMesh.getTotalIndices();
const startingVertex = this._reconstructedMesh.getTotalVertices();
const submeshesArray = this._reconstructedMesh.subMeshes;
this._reconstructedMesh.subMeshes = [];
const newIndicesArray = this._reconstructedMesh.getIndices(); //[];
const originalIndices = this._mesh.getIndices();
for (i = 0; i < newTriangles.length; ++i) {
t = newTriangles[i]; //now get the new referencing point for each vertex
for (let idx = 0; idx < 3; ++idx) {
const id = originalIndices[t.originalOffset + idx];
let offset = t._vertices[idx].originalOffsets.indexOf(id);
if (offset < 0) {
offset = 0;
}
newIndicesArray.push(t._vertices[idx].id + offset + startingVertex);
}
}
//overwriting the old vertex buffers and indices.
this._reconstructedMesh.setIndices(newIndicesArray);
this._reconstructedMesh.setVerticesData(VertexBuffer.PositionKind, newPositionData);
if (newNormalData.length > 0) {
this._reconstructedMesh.setVerticesData(VertexBuffer.NormalKind, newNormalData);
}
if (newUVsData.length > 0) {
this._reconstructedMesh.setVerticesData(VertexBuffer.UVKind, newUVsData);
}
if (newColorsData.length > 0) {
this._reconstructedMesh.setVerticesData(VertexBuffer.ColorKind, newColorsData);
}
//create submesh
const originalSubmesh = this._mesh.subMeshes[submeshIndex];
if (submeshIndex > 0) {
this._reconstructedMesh.subMeshes = [];
for (const submesh of submeshesArray) {
SubMesh.AddToMesh(submesh.materialIndex, submesh.verticesStart, submesh.verticesCount,
/* 0, newPositionData.length/3, */ submesh.indexStart, submesh.indexCount, submesh.getMesh());
}
SubMesh.AddToMesh(originalSubmesh.materialIndex, startingVertex, vertexCount,
/* 0, newPositionData.length / 3, */ startingIndex, newTriangles.length * 3, this._reconstructedMesh);
}
}
_initDecimatedMesh() {
this._reconstructedMesh = new Mesh(this._mesh.name + "Decimated", this._mesh.getScene());
this._reconstructedMesh.material = this._mesh.material;
this._reconstructedMesh.parent = this._mesh.parent;
this._reconstructedMesh.isVisible = false;
this._reconstructedMesh.renderingGroupId = this._mesh.renderingGroupId;
}
_isFlipped(vertex1, vertex2, point, deletedArray, delTr) {
for (let i = 0; i < vertex1.triangleCount; ++i) {
const t = this._triangles[this._references[vertex1.triangleStart + i].triangleId];
if (t.deleted) {
continue;
}
const s = this._references[vertex1.triangleStart + i].vertexId;
const v1 = t._vertices[(s + 1) % 3];
const v2 = t._vertices[(s + 2) % 3];
if (v1 === vertex2 || v2 === vertex2) {
deletedArray[i] = true;
delTr.push(t);
continue;
}
let d1 = v1.position.subtract(point);
d1 = d1.normalize();
let d2 = v2.position.subtract(point);
d2 = d2.normalize();
if (Math.abs(Vector3.Dot(d1, d2)) > 0.999) {
return true;
}
const normal = Vector3.Cross(d1, d2).normalize();
deletedArray[i] = false;
if (Vector3.Dot(normal, t.normal) < 0.2) {
return true;
}
}
return false;
}
_updateTriangles(origVertex, vertex, deletedArray, deletedTriangles) {
let newDeleted = deletedTriangles;
for (let i = 0; i < vertex.triangleCount; ++i) {
const ref = this._references[vertex.triangleStart + i];
const t = this._triangles[ref.triangleId];
if (t.deleted) {
continue;
}
if (deletedArray[i] && t.deletePending) {
t.deleted = true;
newDeleted++;
continue;
}
t._vertices[ref.vertexId] = origVertex;
t.isDirty = true;
t.error[0] = this._calculateError(t._vertices[0], t._vertices[1]) + t.borderFactor / 2;
t.error[1] = this._calculateError(t._vertices[1], t._vertices[2]) + t.borderFactor / 2;
t.error[2] = this._calculateError(t._vertices[2], t._vertices[0]) + t.borderFactor / 2;
t.error[3] = Math.min(t.error[0], t.error[1], t.error[2]);
this._references.push(ref);
}
return newDeleted;
}
_identifyBorder() {
for (let i = 0; i < this._vertices.length; ++i) {
const vCount = [];
const vId = [];
const v = this._vertices[i];
let j;
for (j = 0; j < v.triangleCount; ++j) {
const triangle = this._triangles[this._references[v.triangleStart + j].triangleId];
for (let ii = 0; ii < 3; ii++) {
let ofs = 0;
const vv = triangle._vertices[ii];
while (ofs < vCount.length) {
if (vId[ofs] === vv.id) {
break;
}
++ofs;
}
if (ofs === vCount.length) {
vCount.push(1);
vId.push(vv.id);
}
else {
vCount[ofs]++;
}
}
}
for (j = 0; j < vCount.length; ++j) {
if (vCount[j] === 1) {
this._vertices[vId[j]].isBorder = true;
}
else {
this._vertices[vId[j]].isBorder = false;
}
}
}
}
_updateMesh(identifyBorders = false) {
let i;
if (!identifyBorders) {
const newTrianglesVector = [];
for (i = 0; i < this._triangles.length; ++i) {
if (!this._triangles[i].deleted) {
newTrianglesVector.push(this._triangles[i]);
}
}
this._triangles = newTrianglesVector;
}
for (i = 0; i < this._vertices.length; ++i) {
this._vertices[i].triangleCount = 0;
this._vertices[i].triangleStart = 0;
}
let t;
let j;
let v;
for (i = 0; i < this._triangles.length; ++i) {
t = this._triangles[i];
for (j = 0; j < 3; ++j) {
v = t._vertices[j];
v.triangleCount++;
}
}
let tStart = 0;
for (i = 0; i < this._vertices.length; ++i) {
this._vertices[i].triangleStart = tStart;
tStart += this._vertices[i].triangleCount;
this._vertices[i].triangleCount = 0;
}
const newReferences = new Array(this._triangles.length * 3);
for (i = 0; i < this._triangles.length; ++i) {
t = this._triangles[i];
for (j = 0; j < 3; ++j) {
v = t._vertices[j];
newReferences[v.triangleStart + v.triangleCount] = new Reference(j, i);
v.triangleCount++;
}
}
this._references = newReferences;
if (identifyBorders) {
this._identifyBorder();
}
}
_vertexError(q, point) {
const x = point.x;
const y = point.y;
const z = point.z;
return (q.data[0] * x * x +
2 * q.data[1] * x * y +
2 * q.data[2] * x * z +
2 * q.data[3] * x +
q.data[4] * y * y +
2 * q.data[5] * y * z +
2 * q.data[6] * y +
q.data[7] * z * z +
2 * q.data[8] * z +
q.data[9]);
}
_calculateError(vertex1, vertex2, pointResult) {
const q = vertex1.q.add(vertex2.q);
const border = vertex1.isBorder && vertex2.isBorder;
let error = 0;
const qDet = q.det(0, 1, 2, 1, 4, 5, 2, 5, 7);
if (qDet !== 0 && !border) {
if (!pointResult) {
pointResult = Vector3.Zero();
}
pointResult.x = (-1 / qDet) * q.det(1, 2, 3, 4, 5, 6, 5, 7, 8);
pointResult.y = (1 / qDet) * q.det(0, 2, 3, 1, 5, 6, 2, 7, 8);
pointResult.z = (-1 / qDet) * q.det(0, 1, 3, 1, 4, 6, 2, 5, 8);
error = this._vertexError(q, pointResult);
}
else {
const p3 = vertex1.position.add(vertex2.position).divide(new Vector3(2, 2, 2));
//var norm3 = (vertex1.normal.add(vertex2.normal)).divide(new Vector3(2, 2, 2)).normalize();
const error1 = this._vertexError(q, vertex1.position);
const error2 = this._vertexError(q, vertex2.position);
const error3 = this._vertexError(q, p3);
error = Math.min(error1, error2, error3);
if (error === error1) {
if (pointResult) {
pointResult.copyFrom(vertex1.position);
}
}
else if (error === error2) {
if (pointResult) {
pointResult.copyFrom(vertex2.position);
}
}
else {
if (pointResult) {
pointResult.copyFrom(p3);
}
}
}
return error;
}
}
//# sourceMappingURL=meshSimplification.js.map