计算3D模型表面积:BufferGeometry位置数组顶点重复问题排查
我正在尝试计算通过IFC Loader导入的3D模型的表面积,先从简单的金字塔(用THREE.js的ConeGeometry,边数设为3)模型入手测试。
我知道Indexed BufferGeometry包含position数组(存储所有顶点)和index数组(每3个元素构成一个三角形面的顶点索引)。但我用以下代码生成PLY文件后,发现文件里有大量重复顶点,还出现了无效面(由重复顶点连接而成)。比如这个底面为三角形的金字塔,理论上应该只有6个面(3个侧面+底面的3个三角形),但生成的文件却有9个面。
我想知道哪里操作错了,或者遗漏了什么要点?我知道这不会影响表面积计算(叉乘结果为0),但想搞清楚这些重复顶点和无效面出现的原因,以及为什么只有部分情况会这样。
测试代码
const geometry = new THREE.ConeGeometry(5, 10, 3); const material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); const cone = new THREE.Mesh(geometry, material); const positionsArray = geometry.getAttribute("position").array; const positions = [] for (var i = 0; i < positionsArray.length; i += 3) { positions.push([ positionsArray[i + 0], positionsArray[i + 1], positionsArray[i + 2] ]); } var surface = 0 const faces = geometry.getIndex().array; var data = `ply format ascii 1.0 element vertex ${positions.length} property float x property float y property float z element face ${faces.length / 3} property list uchar int vertex_index end_header ` for (const position of positions) { data += position.join(" ") + "\n" } for (var i = 0; i < faces.length; i += 3) { const p1 = positions[faces[i + 0]] const p2 = positions[faces[i + 1]] const p3 = positions[faces[i + 2]] data += `3 ${faces[i + 0]} ${faces[i + 1]} ${faces[i + 2]}\n` const p1p2 = p2.map(function (item, index) { return item - p1[index] }) const p1p3 = p3.map(function (item, index) { return item - p1[index] }) surface += Math.sqrt(Math.pow(p1p2[1] * p1p3[2] - p1p2[2] * p1p3[1], 2) + Math.pow(p1p2[2] * p1p3[0] - p1p2[0] * p1p3[2], 2) + Math.pow(p1p2[0] * p1p3[1] - p1p2[1] * p1p3[0], 2)) / 2 } const link = document.createElement('a'); var blob = new Blob([data], { type: "text/plain" }); link.href = URL.createObjectURL(blob); link.download = 'example.ply'; link.click();
生成的PLY文件内容
ply format ascii 1.0 element vertex 15 property float x property float y property float z element face 9 property list uchar int vertex_index end_header 0 5 0 0 5 0 0 5 0 0 5 0 0 -5 5 4.330127239227295 -5 -2.5 -4.330127239227295 -5 -2.5 -1.2246467996456087e-15 -5 5 0 -5 0 0 -5 0 0 -5 0 0 -5 5 4.330127239227295 -5 -2.5 -4.330127239227295 -5 -2.5 -1.2246467996456087e-15 -5 5 3 0 4 1 3 4 5 1 3 1 5 2 3 5 6 2 3 2 6 3 3 6 7 3 3 12 11 8 3 13 12 9 3 14 13 10
1. 重复顶点的来源
THREE.js生成的ConeGeometry(包括其他内置几何体)出现重复顶点,不是你的代码问题,而是因为几何体的顶点不仅包含位置信息,还附带了法线、UV坐标等其他属性。即使两个顶点的空间位置完全相同,如果它们的法线、UV等属性不同,就会被当作两个独立的顶点存储。
比如你用的3边圆锥体,顶部的顶点在位置上是同一个点,但每个侧面三角形的顶部顶点需要对应不同的法线方向(用于正确渲染光照),所以THREE.js会重复存储这个顶点,每个侧面对应一个。底部的顶点同理,底面的三角形和侧面连接的顶点,法线方向不同,也会被重复存储。
2. 无效面与多余面的原因
你看到的9个面,其中6个是有效的侧面和底面三角面,另外3个是重复的无效面。这些无效面是因为你直接读取了geometry.getIndex().array的所有索引,但实际上THREE.js的内置几何体可能会为了某些渲染需求(比如线框模式、背面剔除的兼容)生成重复或退化的面,或者是构造几何体时的冗余索引。
从你生成的PLY文件里看,最后3个面(比如3 12 11 8)用到的顶点都是底部重复的顶点,这些顶点的位置完全相同,所以构成的面是退化的(面积为0),也就是你说的无效面。
3. 为什么只有部分情况出现
这种重复顶点和无效面的情况,取决于几何体的构造逻辑。比如当你使用的边数更多(比如4边圆锥),或者几何体的拓扑结构更复杂时,重复顶点的数量会对应变化;而如果几何体构造时没有生成冗余索引,就不会出现无效面。ConeGeometry在边数较少时(比如3边),构造逻辑会产生这些冗余,是因为要适配通用的渲染管线。
解决建议
如果想得到无重复顶点的PLY文件,可以在导出前对几何体的顶点进行去重处理:
- 遍历所有顶点位置,将相同的位置合并,同时更新索引数组。
- 使用
THREE.BufferGeometryUtils.mergeVertices(geometry)方法,这个工具方法可以自动合并位置相同的顶点,并修正索引。
修改后的代码片段示例:
// 在获取索引之前先合并顶点 const mergedGeometry = THREE.BufferGeometryUtils.mergeVertices(geometry); const faces = mergedGeometry.getIndex().array; // 后续的positions读取和PLY生成逻辑改用mergedGeometry
这样处理后,生成的PLY文件就不会有重复顶点和无效面了,面数也会和你预期的一致。
内容的提问来源于stack exchange,提问作者user16283137

