如何在THREEJS网格中对共面三角形进行分组?

时间:2017-01-04 19:20:30

标签: algorithm three.js computational-geometry geometry-surface

我正在使用一种可以直接操作网格的建模工具。例如,你可以抓住一张脸然后拖动它。用户对" face"的感知可能是一个以上的共面三角形。例如,顶部"面部"一个立方体实际上是两个三角形,它们被拖在一起作为一个正方形。

为了实现这一点,我想收集任何特定三角形的共面相邻面,以便在拖动时使用。我已经查看了Simplifier以及this post作为示例,但我希望保留基础三角形,而不是减少/删除它们。

在好的'几天,你建立了一个边缘模型ala Mantlya,在那里你可以走每条边来看相邻的面并检查法线。

我希望可能已经为THREEJS编写了一些代码,这些代码将共面三角形组合在一起。如果我从头开始写这个,我能想到的最好的算法是O(n ^ 2),类似于:

  1. 通过遍历所有面的所有顶点来构建边缘哈希(两个方向)。每个条目都是一个包含2个面部指针的数组。创建或修改网格时,您只需执行此步骤一次。
  2. 当用户选择要操作的面时,创建空的评估堆栈并将拾取的面放入该堆栈中。另外,创建空的coplanar face array。
  3. 弹出面对评估堆栈,然后走到那张脸的边缘。在边哈希中查找与该边相邻的所有面。如果face是coplanar,将该面推到评估堆栈并存储在coplanar face array中。
  4. 重复步骤3-4,直到评估堆栈为空。
  5. 当此算法完成时,您应该有一个所有面的数组,这些面共面并且与您开始的面相邻。但对我来说似乎效率相对较低。

    欢迎任何和所有建议/指针!

2 个答案:

答案 0 :(得分:2)

你的想法有效。

我添加了一个角度阈值,因此您可以获得轻微的非共面地形。我必须创建一个onEvent以允许不确定的递归时间。应该修改它以将vertexHash放在mesh.userData中。

//编辑。我已经更新了类,以便使用一个clamp参数,当你设置为true时,可以将maxAngle钳制到原始面。设置为false时,它会将每个面部与下一个面部进行比较。

faceUtils = function(){};
faceUtils.vertexHash = function(geometry){
  geometry.vertexHash = [];
  var faces = geometry.faces;
  var vLen = geometry.vertices.length;
  for(var i=0;i<vLen;i++){
    geometry.vertexHash[i] = [];
    for(var f in faces){
         if(faces[f].a == i || faces[f].b == i || faces[f].c == i){
            geometry.vertexHash[i].push(faces[f]);
       }
     }
   }
}

faceUtils.prototype.getCoplanar = function(maxAngle, geometry, face, clamp, out, originFace){
  if(clamp == undefined){
      clamp = true;
  }
  if(this.originFace == undefined){
    this.originFace = face;
  }
  if(this.pendingRecursive == undefined){
    this.pendingRecursive = 0;
  }
    this.result = out;
  if(out == undefined){
       this.result = {count:0};
  }
  if(geometry.vertexHash == undefined){
    faceUtils.vertexHash(geometry);
  }
  this.pendingRecursive++;
  var vertexes = ["a","b","c"];
  for (var i in vertexes){
    var vertexIndex = face[vertexes[i]];
    var adjacentFaces = geometry.vertexHash[vertexIndex];
    for(var a in adjacentFaces){
        var newface = adjacentFaces[a];
        var testF = this.originFace;
        if(clamp == false){
          testF = face
        }
        if(testF.normal.angleTo(newface.normal) * (180/ Math.PI) <= maxAngle){
          if(this.result["f"+newface.a+newface.b+newface.c] == undefined){
            this.result["f"+newface.a+newface.b+newface.c] = newface;
            this.result.count++;
            this.getCoplanar(maxAngle, geometry, newface, clamp, this.result, this.originFace);
          }
        }
    }
  }
  this.pendingRecursive--;

  if(this.pendingRecursive == 0 && this.onCoplanar != undefined){
    delete this.result.count;
    this.onCoplanar(this.result);
  }
}

用法很简单:

         var faceTools = new faceUtils();
         faceTools.onCoplanar = function(rfaces){
           for(var i in rfaces){
              rfaces[i].color.setHex(0xff0000);
              intersects[0].object.geometry.colorsNeedUpdate = true;
           }
         }
         //params: maxangle, geometry, picked face
         faceTools.getCoplanar(13, geometry, face);

我将课程添加到其他人的小提琴中并且工作正常。 http://jsfiddle.net/fnuaw44r/

我更新了小提琴以使用限幅选项:http://jsfiddle.net/ta0g3mLc/

我认为你的建议非常低效,但这取决于网格。我添加了一个&#34; pendingRecursive&#34;变量。只要它不等于零,你可以提出一个gif并在值再次为零时将其删除。

无论如何,这是一个起点。我相信有一个聪明的人可以在没有嵌套for循环的情况下折腾面孔。

答案 1 :(得分:1)

我编写了一个适合我的解决方案,沿着我最初发布的问题中的项目符号,并且不使用递归。也许这对某人有用。 (注意:为了方便起见,我使用下划线和哈希和数组等)。

此算法首先在网格顶点上添加映射,列出每个顶点所属的所有面。从那里,我可以从一个特定的面开始,然后寻找与起始面共享至少一个顶点的所有共面面(并从那里开始)。如果共享两个顶点,那就没问题。

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');
}

每个得到的共面组包含一个共面面数组和这些面使用的唯一顶点数组。使用顶点数组,我现在可以抓住&amp;只需应用 Vector3.add()函数,即可立即拖动网格中的所有共面面。

下面的截图可以澄清这项工作的原因。为了创建显示的网格,生成了一个立方体,然后使用上面提到的CSG库从中减去了布尔值。

Poor triangulation

  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(); 

球体足够远,它实际上不与立方体相交,但是,在操作之后,立方体有许多额外的共面三角形,但不是一致的边界表示。例如,正如您在图中看到的,一些三角形触摸其他三角形的边缘的中间(一些示例用红色箭头表示)。

我写了另一种算法,试图在三角形像这样触摸时进一步分割三角形。该算法在某种程度上改善了这种情况:

enter image description here

但仍然不完美,因为CSG库有时会产生几乎是直线的三角形(两个顶点非常靠近),导致舍入错误,导致我的算法失效。如果三角形边缘与网格中的多个其他三角形相交,它也不能很好地工作。

鉴于这一切,在一个完美的世界中,我实际上想将所有共面面重新组合成一个面,然后让THREEjs正确地对所得(较大)的面进行三角测量(或使用像{{3}这样的其他库。 } 去做吧)。

我正在寻找一种方法来做到这一点,现在我将所有共面三角形组合在一起。在我看来,应该有一个算法,给定所有这些共面三角形的所有边缘,可以找到所有这些的周长。有了这个,我可以生成一个新的三角形面,用libtess之类的东西替换它们来创建一组新的三角形插入到我的原始网格中。在应用了所有这些之后,最终的结果是,在CSG库转换为BSP之后,我将首先返回到BoxGeometry创建的精确几何体,然后重新转换为网格。

如果有人对完成第二个目标的最佳方法有任何想法,请发表评论。如果我找到一些好的方法,我最终可能会在这里发布。现在我得到的最好的想法是:

创意1:

  1. 在上述算法中相邻面之间的所有共面顶点的集合中,找到距离原点最远的顶点。
  2. 找到离开该顶点的所有边。
  3. 沿着从原点通过该顶点到下一个顶点的矢量形成最小角度的边缘。
  4. 在下一个顶点处,将使最大角度的边缘移动到下一个顶点。 (使用 dot() cross()以确保相对于所有共面面的法线正确选取角度。)
  5. 到达第一个顶点时停止。从理论上讲,你已经走过所有面孔的周边(理论上!)
  6. 创意2(光线投射):

    1. 从上述算法
    2. 找到的共面面创建一组独特的共面边
    3. 从上面算法找到的共面顶点集中的每个顶点投射一条光线,沿着它所属的任何共面边缘。
    4. 如果顶点是内部顶点,则与其他顶点和/或边的交点的数量将是奇数。如果是,我们可以丢弃我们投射光线的边缘,以及内部顶点。
    5. 现在选择任何剩余的顶点。走它所属的任何边缘(现在应该只有两个)。继续使用边缘(不跟踪当前边缘)匹配顶点,直到返回到起始顶点。现在我们应该拥有所有共面面的边界。
    6. 要了解CSG库如何创建过于复杂的面,请在实际 与多维数据集相交的减法球体时查看立方体网格:

      ShapeGeometry enter image description here

      如您所见,应该不受布尔运算影响的立方体边有大量内部三角形。

      最后,拖动这些凌乱的共面但不正确的边界代表网格曲面的最终结果显示在下面的动画gif中。你可以看到为什么我想简化网格,因为拖动会完全弄乱网格。

      enter image description here