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

Three.js拆分Torus显示黑块及交线高亮问题求助

问题解答

1. 拆分后模型显示为黑色点状的根源及修复

问题根源

  • 只提取孤立顶点,未保留三角面索引信息:原圆环几何体由三角面构成,仅筛选单个顶点的话,WebGL无法识别顶点的组合方式,默认渲染为点。
  • 缺失法向量(Normal)属性:MeshStandardMaterial依赖法向量计算光照效果,没有法向量时材质无法正确显色,只会呈现黑色。
  • 直接对顶点应用世界矩阵,导致新模型的局部坐标混乱,后续变换逻辑出错。

修复方案

拆分时需基于原几何体的三角面进行筛选,保留完整的面结构,同时重新计算法向量:

  1. 遍历原几何体的三角面索引,判断每个面的所有顶点是否都在平面同侧;
  2. 对跨平面的面,计算交点并切割为新的三角面(保证模型完整性);
  3. 构建新几何体时保留索引信息,并调用computeVertexNormals()生成法向量。

2. 高亮圆环与平面的交线

实现思路

  • 计算交点:遍历圆环的边,找到与平面(y=0)相交的点;
  • 创建线模型:用交点集合生成LineLoop几何体,搭配绿色线材质渲染,添加到场景中。
修改后的完整代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Three.js Plane and Torus Split Fixed</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  <script src="https://cdn.rawgit.com/mrdoob/three.js/r128/examples/js/controls/DragControls.js"></script>
  <script src="https://cdn.rawgit.com/mrdoob/three.js/r128/examples/js/controls/OrbitControls.js"></script>
  <script>
    // 场景、相机、渲染器初始化
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
    renderer.setClearColor(0x00ffff);

    // 平面创建
    const planeGeometry = new THREE.PlaneGeometry(5, 5);
    const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00, transparent: true, opacity: 0.3 });
    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    plane.rotation.x = -Math.PI / 2;
    scene.add(plane);

    // 圆环创建
    const torusGeometry = new THREE.TorusGeometry(1, 0.4, 100, 100);
    const torusMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00, roughness: 0.5, metalness: 0.5 });
    let torus = new THREE.Mesh(torusGeometry, torusMaterial);
    torus.position.set(0, 1, 0);
    scene.add(torus);

    // 光源
    const ambientLight = new THREE.AmbientLight(0x404040);
    scene.add(ambientLight);
    const pointLight = new THREE.PointLight(0xffffff, 1);
    pointLight.position.set(5, 5, 5);
    scene.add(pointLight);

    // 相机与控制器
    camera.position.set(0, 5, 5);
    camera.lookAt(0, 0, 0);
    const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

    // 拆分圆环函数(修复版)
    function splitTorus() {
      const planeY = plane.position.y;
      const posAttr = torus.geometry.attributes.position;
      const indexAttr = torus.geometry.index;
      const vertices = [];
      // 提取原模型所有局部顶点
      for (let i = 0; i < posAttr.count; i++) {
        vertices.push(new THREE.Vector3().fromBufferAttribute(posAttr, i));
      }

      const facesAbove = [];
      const facesBelow = [];
      const intersectionPoints = [];

      // 遍历每个三角面
      for (let i = 0; i < indexAttr.count; i += 3) {
        const idx1 = indexAttr.getX(i);
        const idx2 = indexAttr.getX(i+1);
        const idx3 = indexAttr.getX(i+2);
        const v1 = vertices[idx1];
        const v2 = vertices[idx2];
        const v3 = vertices[idx3];

        // 判断顶点相对于平面的位置
        const v1Above = v1.y >= planeY;
        const v2Above = v2.y >= planeY;
        const v3Above = v3.y >= planeY;

        // 面完全在上方/下方,直接保留
        if (v1Above && v2Above && v3Above) {
          facesAbove.push(idx1, idx2, idx3);
        } else if (!v1Above && !v2Above && !v3Above) {
          facesBelow.push(idx1, idx2, idx3);
        } else {
          // 跨平面的面,计算边与平面的交点
          const getIntersection = (a, b) => {
            if ((a.y >= planeY && b.y < planeY) || (a.y < planeY && b.y >= planeY)) {
              const t = (planeY - a.y) / (b.y - a.y);
              return a.clone().lerp(b, t);
            }
            return null;
          };
          const i1 = getIntersection(v1, v2);
          const i2 = getIntersection(v2, v3);
          const i3 = getIntersection(v3, v1);
          const intersections = [i1, i2, i3].filter(p => p !== null);
          intersections.forEach(p => intersectionPoints.push(p));

          // 切割跨平面的面,补充新顶点和面
          if (intersections.length === 2) {
            const newIdx1 = vertices.length;
            const newIdx2 = vertices.length + 1;
            vertices.push(intersections[0], intersections[1]);
            v1Above ? facesAbove.push(idx1, newIdx1, newIdx2) : facesBelow.push(idx1, newIdx1, newIdx2);
          }
        }
      }

      // 创建上方模型
      const geoAbove = new THREE.BufferGeometry();
      geoAbove.setAttribute('position', new THREE.Float32BufferAttribute(vertices.flatMap(v => [v.x, v.y, v.z]), 3));
      geoAbove.setIndex(facesAbove);
      geoAbove.computeVertexNormals();
      const matAbove = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.5, metalness: 0.5 });
      const torusAbove = new THREE.Mesh(geoAbove, matAbove);
      torusAbove.position.copy(torus.position);

      // 创建下方模型
      const geoBelow = new THREE.BufferGeometry();
      geoBelow.setAttribute('position', new THREE.Float32BufferAttribute(vertices.flatMap(v => [v.x, v.y, v.z]), 3));
      geoBelow.setIndex(facesBelow);
      geoBelow.computeVertexNormals();
      const matBelow = new THREE.MeshStandardMaterial({ color: 0xffff00, roughness: 0.5, metalness: 0.5 });
      const torusBelow = new THREE.Mesh(geoBelow, matBelow);
      torusBelow.position.copy(torus.position);

      // 创建交线
      const lineGeo = new THREE.BufferGeometry().setFromPoints(intersectionPoints);
      const lineMat = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 3 });
      const intersectionLine = new THREE.LineLoop(lineGeo, lineMat);
      intersectionLine.position.copy(torus.position);
      scene.add(intersectionLine);

      scene.remove(torus);
      return [torusAbove, torusBelow];
    }

    let [torusAbove, torusBelow] = splitTorus();
    scene.add(torusAbove);
    scene.add(torusBelow);

    // 拖拽控制器
    const dragControls = new THREE.DragControls([torusAbove, torusBelow], camera, renderer.domElement);
    dragControls.addEventListener('dragstart', (event) => {
      event.object.material.emissive.set(0xaaaaaa);
      orbitControls.enabled = false;
    });
    dragControls.addEventListener('dragend', (event) => {
      event.object.material.emissive.set(0x000000);
      orbitControls.enabled = true;
    });

    // 动画渲染
    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }
    animate();

    // 窗口 resize 处理
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>

关键修改点说明

  1. 模型拆分修复:

    • 基于原三角面索引筛选,保留完整的面结构;
    • 对跨平面的面计算交点并切割,保证模型完整性;
    • 生成法向量让材质正常显色;
    • 保留局部坐标并复制原模型位置,确保拖拽逻辑正常。
  2. 交线高亮:

    • 计算圆环边与平面的交点,生成闭合线模型;
    • 使用绿色线材质突出显示交线。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 18:22:08