按照我最初发布的问题中的要点,我编写了一个适合我的解决方案,并且不使用递归。也许这对某人有用。 (注意:为了方便哈希和数组等,我使用 underscorejs)。
此算法首先在网格顶点上添加映射,列出每个顶点所属的所有面。从那里,我可以从一个特定的面开始,然后寻找所有与起始面共享至少一个顶点的共面面(并从那里开始)。如果两个顶点是共享的,那没关系。
var COPLANAR_ANGLE_TOLERANCE = .1; // degrees, not radians
var RAD_TO_DEG = 180 / Math.PI;
var FACELEN = 3; // meshes have triangles by default
function checkCoplanarity(f1, f2) {
return ((f1.normal.angleTo(f2.normal) * RAD_TO_DEG) <= COPLANAR_ANGLE_TOLERANCE);
}
function assignVertexFaceHashes(geometry) {
var vertices = geometry.vertices;
var faces = geometry.faces, face;
var theVertex;
for (var faceIndex in faces) {
face = geometry.faces[faceIndex];
for (var vertIndex of [face.a, face.b, face.c]) {
theVertex = vertices[vertIndex];
if (!theVertex.hasOwnProperty('inFaces')) {
theVertex.inFaces = {};
}
theVertex.inFaces[faceIndex] = true;
}
}
}
function findCoplanarAdjacentFaces(startFaceIndex, geometry) {
var adjoiningFaceIndexes;
var coplanarAdjacentFaces = {};
var coplanarAdjacentVertices = {};
var examQueue = [];
var examined = {};
var examFace, examFaceIndex;
var adjoiningFace, adjoiningFaceIndex;
var faces = geometry.faces;
var vertices = geometry.vertices;
var startFace = faces[startFaceIndex];
examQueue.push(startFaceIndex);
// include the start face as a coplanar face
coplanarAdjacentVertices[startFace.a] = true;
coplanarAdjacentVertices[startFace.b] = true;
coplanarAdjacentVertices[startFace.c] = true;
coplanarAdjacentFaces[startFaceIndex] = true;
// Map vertices back to all faces they belong to
assignVertexFaceHashes(geometry);
while (examQueue.length > 0) {
examFaceIndex = examQueue.pop();
examFace = faces[examFaceIndex];
// console.log('examQueue:', examQueue.length);
adjoiningFaceIndexes = [];
for (var vertIndex of [examFace.a, examFace.b, examFace.c]) {
adjoiningFaceIndexes = _.union(adjoiningFaceIndexes, _.map(_.keys(vertices[vertIndex].inFaces), function(c) { return parseInt(c); }));
}
//console.log('adjoiningFaceIndexes:', adjoiningFaceIndexes);
for (adjoiningFaceIndex of adjoiningFaceIndexes) {
//console.log('Examining adjoining face index:', adjoiningFaceIndex);
if (!examined.hasOwnProperty(adjoiningFaceIndex)) {
if ((adjoiningFaceIndex != examFaceIndex) && (!coplanarAdjacentFaces.hasOwnProperty(adjoiningFaceIndex))) {
//console.log('adjoiningFaceIndex:', adjoiningFaceIndex);
adjoiningFace = faces[adjoiningFaceIndex];
if (checkCoplanarity(examFace, adjoiningFace)) {
var overlap1 = [adjoiningFace.a, adjoiningFace.b, adjoiningFace.c];
var overlap2 = [examFace.a, examFace.b, examFace.c];
var vertsInCommon = _.intersection(overlap1, overlap2);
// Check for vertices in common. If any vertices are in comment, these coplanar faces touch at least one vertex.
if (vertsInCommon.length > 0) {
//console.log('Pushing adjoining face due to vertices in common:', adjoiningFaceIndex);
coplanarAdjacentFaces[adjoiningFaceIndex] = true;
examQueue.push(adjoiningFaceIndex);
coplanarAdjacentVertices[adjoiningFace.a] = true;
coplanarAdjacentVertices[adjoiningFace.b] = true;
coplanarAdjacentVertices[adjoiningFace.c] = true;
} else {
// it's possible the adjoining face only touches vertices to the middle of edges, so check for that.
edgeIntersectExam:
for (var i = 0; i < FACELEN; ++i) {
adjoinP1 = overlap1[i];
adjoinP2 = overlap1[(i + 1) % FACELEN];
for (var j = 0; j < FACELEN; ++j) {
splitPoint = distToSegmentSquared3d(vertices[overlap2[j]], vertices[adjoinP1], vertices[adjoinP2]);
if (splitPoint.distance < POINT_ON_LINE_TOLERANCE) {
console.log('adding adjoining face due to edge intersection:', adjoiningFaceIndex);
console.log('j=', j, 'Source face:', examFaceIndex, examFace, 'We found split point on adjoining face index:', adjoiningFaceIndex, adjoiningFace);
coplanarAdjacentFaces[adjoiningFaceIndex] = true;
examQueue.push(adjoiningFaceIndex);
coplanarAdjacentVertices[adjoiningFace.a] = true;
coplanarAdjacentVertices[adjoiningFace.b] = true;
coplanarAdjacentVertices[adjoiningFace.c] = true;
break edgeIntersectExam;
}
}
}
}
}
}
}
}
examined[examFaceIndex] = true;
}
return ({ faces: coplanarAdjacentFaces, vertices: coplanarAdjacentVertices });
}
function assignFacesToCoplanarGroups(csgPrimitive) {
var geometry = csgPrimitive.geometry;
var faceIndexList = _.mapObject(_.keys(geometry.faces), function() { return true; });
var processedFaces = {};
var coplanarFaces;
var faces = geometry.faces;
var intIndex;
var coplanarGroupMax;
var coplanarGroups = [];
for (var processFaceIndex in faceIndexList) {
intIndex = parseInt(processFaceIndex);
if (!processedFaces.hasOwnProperty(intIndex)) {
coplanars = findCoplanarAdjacentFaces(processFaceIndex, geometry);
coplanarGroups.push({ faces: coplanars.faces, vertices: coplanars.vertices });
coplanarGroupMax = coplanarGroups.length - 1;
for (var groupedFaceIndex in coplanars.faces) {
faces[groupedFaceIndex].coplanarGroupIndex = coplanarGroupMax;
faces[groupedFaceIndex].color.setHex(0x0000ff); // just to help see the results
processedFaces[groupedFaceIndex] = true;
}
}
}
geometry.coplanarGroups = coplanarGroups;
geometry.colorsNeedUpdate = true;
}
function assignFacesToAllCoplanarGroups() {
var now = new Date();
var startTime = now.getTime();
for (var csgPrimitive of csgPrimitives.children) {
assignFacesToCoplanarGroups(csgPrimitive);
}
var later = new Date();
var duration = later.getTime() - startTime;
console.log('Done assigning faces to coplanar groups in:', duration, 'ms');
}
这是我的使用方法。我有一组网格(称为 csgPrimitives,因为它们来自 ThreeCSG.js。我计算每个基元的共面面组并将它们放在每个基元的几何体上。
function assignFacesToAllCoplanarGroups() {
var now = new Date();
var startTime = now.getTime();
for (var csgPrimitive of csgPrimitives.children) {
assignFacesToCoplanarGroups(csgPrimitive);
}
var later = new Date();
var duration = later.getTime() - startTime;
console.log('Done assigning faces to coplanar groups in:', duration, 'ms');
}
每个生成的共面组都包含一组共面面和一组由这些面使用的唯一顶点。使用顶点数组,我现在可以通过简单地应用 Vector3.add() 函数一次抓取和拖动网格中的所有共面面。
下面的截图可以说明这项工作的原因。为了创建所示的网格,生成了一个立方体,然后使用上面提到的 CSG 库从中减去一个球体。
var box = new THREE.Mesh( new THREE.BoxGeometry( width, height, length ) );
// CSG GEOMETRY
cube_bsp = new ThreeBSP( box );
var cutgeo = new THREE.SphereGeometry( 0.5,32,32 );
// move geometry to where the cut should be
var matrix = new THREE.Matrix4();
matrix.setPosition( new THREE.Vector3(0.25, 0, 1.88) ); // NB: sphere does not intersect with cube
cutgeo.applyMatrix( matrix );
var sub = new THREE.Mesh( cutgeo );
var substract_bsp = new ThreeBSP( sub );
var subtract_bsp = cube_bsp.subtract( substract_bsp );
csgPrimitiveMesh = subtract_bsp.toMesh();
球体距离足够远,实际上并没有与立方体相交,但是,经过操作后,立方体有许多额外的共面三角形,但不是一致的边界表示。例如,如图所示,一些三角形接触到其他三角形边缘的中间(几个例子用红色箭头表示)。
我写了另一种算法,当三角形像这样接触时,它试图进一步分割三角形。该算法在一定程度上改善了这种情况:
但仍然不完美,因为 CSG 库有时会生成接近直线的三角形(两个顶点非常靠近),导致舍入错误导致我的算法无法使用。如果三角形边与网格中的多个其他三角形相交,它也不能很好地工作。
考虑到这一切,在一个完美的世界里,我实际上想将所有共面的面重新组合成一个面,然后让 THREEjs 正确地对生成的(更大的)面进行三角测量(或使用像 libtess 这样的其他库来做)。
我正在寻找一种方法来做到这一点,现在我已经将所有共面三角形组合在一起了。在我看来,应该有一个算法,给定所有这些共面三角形的所有边,可以找到所有这些三角形的周长。有了它,我可以生成一个新的三角面来替换它们,使用ShapeGeometry 之类的东西来创建一组新的三角形来插入到我的原始网格中。最终结果将是,在应用所有这些之后,我将返回到由 CSG 库转换为 BSP 后首先创建的精确几何图形,然后再转换为网格。
如果有人对实现第二个目标的最佳方法有任何想法,请发表评论。如果我想出一些好方法,我最终可能会在这里发布。目前我的最佳想法是:
想法 1:
- 在上述算法的相邻面之间所有共面顶点的集合中,找到离原点最远的顶点。
- 查找离开该顶点的所有边。
- 从从原点到该顶点的向量,沿着与形成最小角度的边走到下一个顶点。
- 在下一个顶点,沿着与下一个顶点成最大角的边走。 (使用 dot() 和 cross() 来确保您选择正确的角度,相对于所有共面面的法线)。
- 到达第一个顶点时停止。从理论上讲,您已经走遍了所有面孔(理论上!)
想法2(光线投射):
- 根据上述算法找到的共面面创建唯一共面边的集合
- 从上述算法找到的共面顶点集中的每个顶点沿其所属的任何共面边投射一条射线。
- 如果顶点是内部顶点,则与其他顶点和/或边的交点数将为奇数。如果是,我们可以丢弃我们沿其投射光线的边,以及那个内部顶点。
- 现在选择任何剩余的顶点。走它所属的任何边缘(现在应该只有两个)。继续走与边缘匹配的顶点(当然不是跟踪前一个边缘),直到返回到起始顶点。现在我们应该得到所有共面面的周长。
要了解 CSG 库如何真正创建过于复杂的面,请查看当减法球体实际确实与立方体相交时的立方体网格:
如您所见,应该不受布尔运算影响的立方体边有大量内部三角形。
最后,拖动这些杂乱的共面但不正确的边界表示网格曲面的最终结果显示在下面的动画 gif 中。您可以看到我为什么要简化网格,因为拖动会完全弄乱网格。