【问题标题】:Way to group coplanar triangles in THREEJS mesh?在三个 JS 网格中对共面三角形进行分组的方法?
【发布时间】:2018-08-20 14:17:10
【问题描述】:

我正在开发一种可以让您直接操纵网格的建模工具。例如,您可以抓住一张脸并将其拖动。用户对“脸”的感知可能不止一个共面三角形。例如,立方体的顶部“面”实际上是两个三角形,它们被拖在一起成为一个正方形。

为了实现这一点,我想收集任何特定三角形的所有共面相邻面,以便在拖动时使用。我查看了Simplifierthis post 作为示例,但我想保留底层三角形,而不是减少/删除它们。

在过去的好日子里,您会构建一个边缘模型 ala Mantlya,您可以在其中遍历每个边缘以查看相邻面并检查法线。

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

  1. 通过遍历所有面的所有顶点来构建边散列(双向)。每个条目是一个由 2 个面指针组成的数组。您只需在创建或修改网格时执行一次此步骤。
  2. 当用户选择要操作的人脸时,创建空的评估堆栈并将选择的人脸放入该堆栈。此外,创建空的共面人脸阵列。
  3. 将面从评估堆栈中弹出,并遍历该面的边缘。在边散列中查找与该边相邻的所有面。如果面是共面的,则将该面推入评估堆栈并存储在共面面数组中。
  4. 重复步骤 3-4,直到评估堆栈为空。

当这个算法完成时,你应该有一个所有面共面并与你开始的面相邻的数组。但这对我来说似乎效率相对较低。

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

【问题讨论】:

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


    【解决方案1】:

    你的想法行得通。

    我添加了一个角度阈值,以便您可以抓取稍微不共面的地形。我必须制作一个 onEvent 以允许不确定的递归时间。应该修改为将 vertexHash 放在 mesh.userData 中。

    //编辑。我已经更新了类以利用一个钳位参数,当设置为 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/

    我认为它的效率非常低,但它取决于网格。我添加了一个“pendingRecursive”变量。只要它不等于零,您就可以放置一个 gif 并在值再次为零时将其删除。

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

    【讨论】:

    • 真的很有帮助,@Radio。非常感谢!我不认为需要递归虽然它肯定是实现处理队列的一种方式......我将尝试使用你的作为起点,然后很快发布我自己的版本。我要更改的主要内容是我只需要返回一组共面面但不一定立即在其上运行一个函数,因为我不想在拖动过程中不断重新计算它(因为这些面都会一起拖动,它们应该保持共面)。
    • 如果没有递归,我看不出如何避免询问所有潜在的面孔。
    • 我认为可以通过简单的堆栈(push & pop)来完成。首先将第一个面放入堆栈。堆栈不为空时循环...找到的每个相邻面都进入堆栈。
    • 可以用 8 个面制作一个矩形边。一个起始面与 8 个面不相邻。你会得到一部分,但不是整个面。简单的情况,如立方体原语会起作用,但其他的很少。使用三叉树递归,所有符合条件的面都被耗尽,无论它们与原始面是否直接相邻,而是由面作为连续边的一部分来限定。一个平面列表方法可以工作,但在我看来,你会得到一个不完整的集合。如果您解决了这个问题并通过法线方向限定了每个面,那么您仍然需要递归来找到连续的组。
    • 对不起,我明白你现在在说什么了。像伪递归一样,跟踪自己的堆栈而不是在递归中等待javascript调用堆栈。很难说哪个会更快。值得一试。复杂的网格仍然是一个问题。
    【解决方案2】:

    按照我最初发布的问题中的要点,我编写了一个适合我的解决方案,并且不使用递归。也许这对某人有用。 (注意:为了方便哈希和数组等,我使用 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:

    1. 在上述算法的相邻面之间所有共面顶点的集合中,找到离原点最远的顶点。
    2. 查找离开该顶点的所有边。
    3. 从从原点到该顶点的向量,沿着与形成最小角度的边走到下一个顶点。
    4. 在下一个顶点,沿着与下一个顶点成最大角的边走。 (使用 dot()cross() 来确保您选择正确的角度,相对于所有共面面的法线)。
    5. 到达第一个顶点时停止。从理论上讲,您已经走遍了所有面孔(理论上!)

    想法2(光线投射):

    1. 根据上述算法找到的共面面创建唯一共面边的集合
    2. 从上述算法找到的共面顶点集中的每个顶点沿其所属的任何共面边投射一条射线。
    3. 如果顶点是内部顶点,则与其他顶点和/或边的交点数将为奇数。如果是,我们可以丢弃我们沿其投射光线的边,以及那个内部顶点。
    4. 现在选择任何剩余的顶点。走它所属的任何边缘(现在应该只有两个)。继续走与边缘匹配的顶点(当然不是跟踪前一个边缘),直到返回到起始顶点。现在我们应该得到所有共面面的周长。

    要了解 CSG 库如何真正创建过于复杂的面,请查看当减法球体实际确实与立方体相交时的立方体网格:

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

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

    【讨论】:

    • 经过进一步研究,我认为我的第二个问题(如何合并共面面)的答案在这里有多种答案:stackoverflow.com/questions/2667748/…
    • 我担心复杂网格上的“while、for-for、for、for”循环嵌套。如果其中任何一个花费大量时间,浏览器就会给用户一条消息。每次迭代都应该有一个超时(在循环关闭时终止),因此您可以在浏览器提供终止脚本之前为用户提供取消脚本的机会。你觉得那些循环会好吗?我不知道你最终的用例要说。
    • 这是一个很好的建议,一旦我可以开始处理复杂的网格,我就会实现类似的东西。仍在尝试弄清楚如何简化共面面以减小网格尺寸...
    • 这可能值得一个新的堆栈问题。您可以通过 simplifierModifier 类运行 CSG 输出吗?请参阅threejs.org/examples/#webgl_modifier_simplifier 我不确定它是否适合您的情况。我想知道这里的复杂度不是顶点的数量,而是边排列的复杂度。
    • 太棒了!你能告诉我更多你是如何挤出的信息吗?是否可以使用 BufferedGeometry 而不是 Geometry 进行挤压?如果你能分享这个 github 源代码 sn-p 那就太好了
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-06-02
    • 2015-01-06
    • 2016-09-28
    • 2012-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多