You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Three.js中从三角化面提取带孔洞的多边形?

解决Three.js中三角化ShapeGeometry的轮廓与孔洞提取问题

我正好在Three.js开发中处理过几乎一模一样的需求,你的思路方向是对的,但EdgesGeometry确实有个天生的问题——它会把所有三角面的边都提取出来,包括内部共享边,这就是你没法区分外轮廓和孔洞的核心原因。咱们一步步拆解你的问题,给出可行的解决方案:

问题1:如何判断三角化ShapeGeometry是否包含孔洞?

分两种情况处理最靠谱:

  • 如果是你自己创建的ShapeGeometry:直接保留生成它的THREE.Shape实例就行,它的holes属性就是孔洞的Path数组,判断shape.holes.length > 0就能知道有没有孔洞,这是最直接的方式。
  • 如果是外部加载/转换来的ShapeGeometry:没法直接从几何体本身拿到孔洞标记,但可以结合后续的轮廓提取,通过轮廓的环绕方向(或有向面积)来区分——外轮廓和孔洞的环绕方向是相反的,后续会详细讲这个方法。

问题2:是否存在现成函数将三角化ShapeGeometry转换为带孔洞的多边形?

Three.js原生没有专门做这件事的函数,但有两种高效的替代方案:

  1. 复用原始Shape数据(最优解):如果你的ShapeGeometry是从THREE.Shape生成的,别折腾三角化的面了,直接用Shape的API提取点:
    // 假设你有原始的Shape实例
    const outerContourPoints = shape.getPoints(100); // 100是采样精度,数值越高越平滑
    const holeContourPoints = shape.holes.map(hole => hole.getPoints(100));
    
    这种方式完全不需要处理三角化逻辑,性能拉满,还能直接得到有序的点集合。
  2. 自定义轮廓提取工具(针对无原始Shape的场景):如果必须从三角化几何体入手,可以通过提取「仅被单个三角面包含的边」来获取真正的轮廓边(内部边会被两个三角面共享,不会出现在结果里),再把这些边连成闭合路径,最后区分内外。

问题3:你的实现思路是否有误?

思路方向没问题,但选EdgesGeometry作为起点是个小误区——它会返回所有边,包括内部的共享边,导致你没法筛选出真正的轮廓。优化后的思路应该是:

  1. 提取仅属于单个三角面的边(这些才是外轮廓和孔洞的边)
  2. 将这些边连接成闭合路径
  3. 通过路径的有向面积区分外轮廓和孔洞(外轮廓和孔洞的面积符号相反)

具体实现代码示例

下面是针对无原始Shape场景的完整实现片段:

// 第一步:提取真正的轮廓边(只被单个三角面包含的边)
function extractOutlineEdges(geometry) {
  const edgeCount = new Map();
  const vertices = geometry.attributes.position.array;
  const index = geometry.index;

  // 遍历所有三角面,统计每条边的出现次数
  for (let i = 0; i < index.count; i += 3) {
    const idxA = index.getX(i);
    const idxB = index.getX(i + 1);
    const idxC = index.getX(i + 2);

    // 生成边的唯一标识(排序端点避免重复统计)
    const edges = [
      [idxA, idxB].sort().join(','),
      [idxB, idxC].sort().join(','),
      [idxC, idxA].sort().join(',')
    ];

    edges.forEach(edgeKey => {
      edgeCount.set(edgeKey, (edgeCount.get(edgeKey) || 0) + 1);
    });
  }

  // 过滤出只出现一次的边,转换为Vector3对
  const outlineEdges = [];
  edgeCount.forEach((count, key) => {
    if (count === 1) {
      const [aIdx, bIdx] = key.split(',').map(Number);
      const a = new THREE.Vector3(
        vertices[aIdx * 3], vertices[aIdx * 3 + 1], vertices[aIdx * 3 + 2]
      );
      const b = new THREE.Vector3(
        vertices[bIdx * 3], vertices[bIdx * 3 + 1], vertices[bIdx * 3 + 2]
      );
      outlineEdges.push([a, b]);
    }
  });

  return outlineEdges;
}

// 第二步:将轮廓边连接成闭合路径
function connectEdgesToPaths(edges) {
  const paths = [];
  const usedEdgeIndices = new Set();

  edges.forEach((edge, edgeIndex) => {
    if (usedEdgeIndices.has(edgeIndex)) return;

    const path = [edge[0]];
    let currentPoint = edge[1];
    usedEdgeIndices.add(edgeIndex);

    // 循环寻找下一条相连的边,直到回到起点
    while (true) {
      const nextEdgeIdx = edges.findIndex((e, idx) => {
        if (usedEdgeIndices.has(idx)) return false;
        return e[0].equals(currentPoint) || e[1].equals(currentPoint);
      });

      if (nextEdgeIdx === -1) break;

      const nextEdge = edges[nextEdgeIdx];
      const nextPoint = nextEdge[0].equals(currentPoint) ? nextEdge[1] : nextEdge[0];
      path.push(currentPoint);
      currentPoint = nextPoint;
      usedEdgeIndices.add(nextEdgeIdx);

      // 检查是否闭合
      if (currentPoint.equals(path[0])) {
        path.push(currentPoint);
        break;
      }
    }

    paths.push(path);
  });

  return paths;
}

// 第三步:通过有向面积区分外轮廓和孔洞
function calculatePolygonArea(path) {
  let area = 0;
  const pointCount = path.length - 1; // 闭合路径最后一个点和第一个重复
  for (let i = 0; i < pointCount; i++) {
    const p1 = path[i];
    const p2 = path[i + 1];
    // 鞋带公式计算有向面积(Z轴朝上的右手坐标系)
    area += (p2.x - p1.x) * (p2.y + p1.y);
  }
  return area * 0.5;
}

// 使用示例
const yourShapeGeometry = // 你的三角化ShapeGeometry实例
const outlineEdges = extractOutlineEdges(yourShapeGeometry);
const allPaths = connectEdgesToPaths(outlineEdges);

// 区分外轮廓和孔洞(假设正面积为外轮廓,负面积为孔洞,可根据坐标系调整)
const outerPath = allPaths.find(path => calculatePolygonArea(path) > 0);
const holePaths = allPaths.filter(path => calculatePolygonArea(path) < 0);

额外提示

  • 如果你的场景是从上方视角提取轮廓,可以在提取点的时候忽略Z轴坐标,只保留X和Y,简化后续处理。
  • 采样精度(比如getPoints(100)里的100)可以根据你的需求调整,数值越高轮廓越平滑,但数据量也越大。

内容的提问来源于stack exchange,提问作者Swisx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:31:27