Three.js拆分Torus显示黑块及交线高亮问题求助
问题解答
1. 拆分后模型显示为黑色点状的根源及修复
问题根源
- 只提取孤立顶点,未保留三角面索引信息:原圆环几何体由三角面构成,仅筛选单个顶点的话,WebGL无法识别顶点的组合方式,默认渲染为点。
- 缺失法向量(Normal)属性:
MeshStandardMaterial依赖法向量计算光照效果,没有法向量时材质无法正确显色,只会呈现黑色。 - 直接对顶点应用世界矩阵,导致新模型的局部坐标混乱,后续变换逻辑出错。
修复方案
拆分时需基于原几何体的三角面进行筛选,保留完整的面结构,同时重新计算法向量:
- 遍历原几何体的三角面索引,判断每个面的所有顶点是否都在平面同侧;
- 对跨平面的面,计算交点并切割为新的三角面(保证模型完整性);
- 构建新几何体时保留索引信息,并调用
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>
关键修改点说明
模型拆分修复:
- 基于原三角面索引筛选,保留完整的面结构;
- 对跨平面的面计算交点并切割,保证模型完整性;
- 生成法向量让材质正常显色;
- 保留局部坐标并复制原模型位置,确保拖拽逻辑正常。
交线高亮:
- 计算圆环边与平面的交点,生成闭合线模型;
- 使用绿色线材质突出显示交线。
内容的提问来源于stack exchange,提问作者Tranquillity
相关产品推荐
相关产品推荐

