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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 04:42:03