如何在Three.js中挤出网格中选中的红色三角形
Three.js 选中红色三角形的挤出实现步骤
前提说明
你的场景中,选中的三角形通过顶点颜色标记为红色(RGB值为(1, 0, 0)),我们将基于这个标记识别并处理目标三角形。
步骤1:识别选中的三角形
先从几何体属性里筛选出所有红色标记的三角形:
- 判断几何体是否使用索引:
- 索引几何体(
mesh.geometry.index !== null):从mesh.geometry.index.array按每3个一组读取三角形顶点索引; - 非索引几何体:直接按每3个顶点为一组,对应一个三角形。
- 索引几何体(
- 遍历每个三角形的3个顶点,从
mesh.geometry.attributes.color.array读取每个顶点的RGB值(每个顶点占3个数组元素:r, g, b)。 - 若三角形的3个顶点颜色均为
(1, 0, 0),标记该三角形为选中状态,记录对应顶点索引。
示例代码片段:
const geometry = mesh.geometry; const colorAttr = geometry.attributes.color; const positionAttr = geometry.attributes.position; const isIndexed = geometry.index !== null; const indices = isIndexed ? geometry.index.array : []; const selectedTriangles = []; const triangleCount = isIndexed ? indices.length / 3 : positionAttr.count / 3; for (let i = 0; i < triangleCount; i++) { let v1Idx, v2Idx, v3Idx; if (isIndexed) { v1Idx = indices[i * 3]; v2Idx = indices[i * 3 + 1]; v3Idx = indices[i * 3 + 2]; } else { v1Idx = i * 3; v2Idx = i * 3 + 1; v3Idx = i * 3 + 2; } // 读取三个顶点的颜色值 const [v1R, v1G, v1B] = [colorAttr.array[v1Idx*3], colorAttr.array[v1Idx*3+1], colorAttr.array[v1Idx*3+2]]; const [v2R, v2G, v2B] = [colorAttr.array[v2Idx*3], colorAttr.array[v2Idx*3+1], colorAttr.array[v2Idx*3+2]]; const [v3R, v3G, v3B] = [colorAttr.array[v3Idx*3], colorAttr.array[v3Idx*3+1], colorAttr.array[v3Idx*3+2]]; const isRedTriangle = (v1R === 1 && v1G === 0 && v1B === 0) && (v2R === 1 && v2G === 0 && v2B === 0) && (v3R === 1 && v3G === 0 && v3B === 0); if (isRedTriangle) { selectedTriangles.push([v1Idx, v2Idx, v3Idx]); } }
步骤2:提取选中区域的独立几何体
把选中三角形对应的顶点数据(位置、法线、颜色等)单独提取,创建新的BufferGeometry,避免干扰原几何体:
- 遍历
selectedTriangles中的每个三角形索引,收集对应的位置、法线、颜色属性值; - 为新几何体创建对应的
BufferAttribute。
示例代码片段:
const selectedPositions = []; const selectedNormals = []; const selectedColors = []; selectedTriangles.forEach(tri => { tri.forEach(idx => { // 收集位置数据 selectedPositions.push(positionAttr.array[idx*3], positionAttr.array[idx*3+1], positionAttr.array[idx*3+2]); // 收集法线数据 selectedNormals.push(geometry.attributes.normal.array[idx*3], geometry.attributes.normal.array[idx*3+1], geometry.attributes.normal.array[idx*3+2]); // 收集颜色数据 selectedColors.push(colorAttr.array[idx*3], colorAttr.array[idx*3+1], colorAttr.array[idx*3+2]); }); }); const selectedGeo = new THREE.BufferGeometry(); selectedGeo.setAttribute('position', new THREE.Float32BufferAttribute(selectedPositions, 3)); selectedGeo.setAttribute('normal', new THREE.Float32BufferAttribute(selectedNormals, 3)); selectedGeo.setAttribute('color', new THREE.Float32BufferAttribute(selectedColors, 3)); selectedGeo.computeVertexNormals();
步骤3:执行挤出操作(核心)
挤出逻辑分为三步:生成偏移顶面、生成侧面、合并所有几何体:
3.1 生成偏移后的顶面几何体
复制选中区域的位置属性,每个顶点沿法线方向移动指定距离(比如extrudeDistance = 0.5):
const extrudeDistance = 0.5; const extrudedPositions = []; const positionArr = selectedGeo.attributes.position.array; const normalArr = selectedGeo.attributes.normal.array; for (let i = 0; i < positionArr.length; i += 3) { const [x, y, z] = [positionArr[i], positionArr[i+1], positionArr[i+2]]; const [nx, ny, nz] = [normalArr[i], normalArr[i+1], normalArr[i+2]]; // 计算偏移后的顶点位置 extrudedPositions.push(x + nx*extrudeDistance, y + ny*extrudeDistance, z + nz*extrudeDistance); } const extrudedTopGeo = selectedGeo.clone(); extrudedTopGeo.setAttribute('position', new THREE.Float32BufferAttribute(extrudedPositions, 3)); extrudedTopGeo.computeVertexNormals();
3.2 生成侧面几何体
连接原选中面的每个边与偏移后的对应边,每个四边形边拆分为两个三角形:
const sidePositions = []; const sideNormals = []; selectedTriangles.forEach((tri, triIdx) => { const baseIdx = triIdx * 3; const edges = [[baseIdx, baseIdx+1], [baseIdx+1, baseIdx+2], [baseIdx+2, baseIdx]]; edges.forEach(edge => { const [a, b] = edge; // 原顶点位置 const [ax, ay, az] = [positionArr[a*3], positionArr[a*3+1], positionArr[a*3+2]]; const [bx, by, bz] = [positionArr[b*3], positionArr[b*3+1], positionArr[b*3+2]]; // 偏移后顶点位置 const [aExX, aExY, aExZ] = [extrudedPositions[a*3], extrudedPositions[a*3+1], extrudedPositions[a*3+2]]; const [bExX, bExY, bExZ] = [extrudedPositions[b*3], extrudedPositions[b*3+1], extrudedPositions[b*3+2]]; // 生成两个三角形,保证顶点顺序符合Three.js面朝向规则 sidePositions.push(ax, ay, az, bx, by, bz, bExX, bExY, bExZ); sidePositions.push(ax, ay, az, bExX, bExY, bExZ, aExX, aExY, aExZ); // 计算侧面法线 const edgeVec = new THREE.Vector3(bx-ax, by-ay, bz-az); const normalVec = new THREE.Vector3(normalArr[a*3], normalArr[a*3+1], normalArr[a*3+2]); const sideNormal = edgeVec.cross(normalVec).normalize(); // 给6个顶点赋值法线 for (let i=0; i<6; i++) { sideNormals.push(sideNormal.x, sideNormal.y, sideNormal.z); } }); }); const sideGeo = new THREE.BufferGeometry(); sideGeo.setAttribute('position', new THREE.Float32BufferAttribute(sidePositions, 3)); sideGeo.setAttribute('normal', new THREE.Float32BufferAttribute(sideNormals, 3)); sideGeo.computeVertexNormals();
3.3 合并所有几何体
将原选中面、偏移顶面、侧面合并为完整的挤出几何体:
const mergedGeo = THREE.BufferGeometryUtils.mergeBufferGeometries([ selectedGeo, extrudedTopGeo, sideGeo ]); mergedGeo.computeVertexNormals();
步骤4:应用到场景
将合并后的几何体添加到场景中:
- 可以创建新网格单独显示挤出部分;
- 若要保留原网格的未选中区域,需重新生成原几何体并移除选中的三角形。
示例代码:
const extrudedMesh = new THREE.Mesh(mergedGeo, mesh.material); scene.add(extrudedMesh); // 可选:隐藏原网格或处理原网格保留未选中部分
常见问题修复
- 网格散乱:检查侧面三角形的顶点顺序,确保符合Three.js的面朝向规则(顺时针/逆时针),避免背面剔除导致显示异常;
- 法线异常:挤出后务必调用
computeVertexNormals()重新计算法线,保证光照显示正常; - 非索引几何体适配:原几何体无索引时,直接按每3个顶点一组遍历三角形即可,无需读取
index属性。
内容的提问来源于stack exchange,提问作者AshhadDevLab
相关产品推荐
相关产品推荐

