如何在Three.js中从三角化面提取带孔洞的多边形?
解决Three.js中三角化ShapeGeometry的轮廓与孔洞提取问题
我正好在Three.js开发中处理过几乎一模一样的需求,你的思路方向是对的,但EdgesGeometry确实有个天生的问题——它会把所有三角面的边都提取出来,包括内部共享边,这就是你没法区分外轮廓和孔洞的核心原因。咱们一步步拆解你的问题,给出可行的解决方案:
问题1:如何判断三角化ShapeGeometry是否包含孔洞?
分两种情况处理最靠谱:
- 如果是你自己创建的ShapeGeometry:直接保留生成它的
THREE.Shape实例就行,它的holes属性就是孔洞的Path数组,判断shape.holes.length > 0就能知道有没有孔洞,这是最直接的方式。 - 如果是外部加载/转换来的ShapeGeometry:没法直接从几何体本身拿到孔洞标记,但可以结合后续的轮廓提取,通过轮廓的环绕方向(或有向面积)来区分——外轮廓和孔洞的环绕方向是相反的,后续会详细讲这个方法。
问题2:是否存在现成函数将三角化ShapeGeometry转换为带孔洞的多边形?
Three.js原生没有专门做这件事的函数,但有两种高效的替代方案:
- 复用原始Shape数据(最优解):如果你的ShapeGeometry是从
THREE.Shape生成的,别折腾三角化的面了,直接用Shape的API提取点:
这种方式完全不需要处理三角化逻辑,性能拉满,还能直接得到有序的点集合。// 假设你有原始的Shape实例 const outerContourPoints = shape.getPoints(100); // 100是采样精度,数值越高越平滑 const holeContourPoints = shape.holes.map(hole => hole.getPoints(100)); - 自定义轮廓提取工具(针对无原始Shape的场景):如果必须从三角化几何体入手,可以通过提取「仅被单个三角面包含的边」来获取真正的轮廓边(内部边会被两个三角面共享,不会出现在结果里),再把这些边连成闭合路径,最后区分内外。
问题3:你的实现思路是否有误?
思路方向没问题,但选EdgesGeometry作为起点是个小误区——它会返回所有边,包括内部的共享边,导致你没法筛选出真正的轮廓。优化后的思路应该是:
- 提取仅属于单个三角面的边(这些才是外轮廓和孔洞的边)
- 将这些边连接成闭合路径
- 通过路径的有向面积区分外轮廓和孔洞(外轮廓和孔洞的面积符号相反)
具体实现代码示例
下面是针对无原始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
相关产品推荐
相关产品推荐

