如何在Three.js中为任意3D形状绘制覆盖线条?
在任意Three.js几何体/GLB模型上绘制覆盖线条
我需要在任意几何形状(包括立方体、球体等规则形状,或是地形这类不规则形状,甚至GLB模型)上绘制覆盖线条。目前我已经手动指定顶点实现了立方体上的线条效果,代码如下:
const boxGeometry = new THREE.BoxGeometry(1.5,1.5,1.5,2,2,2); const material = new THREE.MeshBasicMaterial({ color:0xffffff, wireframe:true }); const mesh = new THREE.Mesh(boxGeometry,material); scene.add(mesh); const lineGeometry = new THREE.BufferGeometry(); const lineGeometryVertices = new Float32Array([ 0,-0,.75, .75,-0,.75, .75,-0,-0 ]); const lineBufferAttribute = new THREE.BufferAttribute(lineGeometryVertices,3); lineGeometry.setAttribute("position",lineBufferAttribute); const lineMaterial = new THREE.LineBasicMaterial({ color:0xff0000, linewidth:3 }) const line = new THREE.Line(lineGeometry,lineMaterial); scene.add(line);

现在的核心问题是:如何获取任意形状上所需的线条顶点,从而实现覆盖线条的绘制?
解决方案
一、交互式拾取模型表面点(适合自定义路径)
通过Raycaster实现鼠标点击拾取模型表面的点,收集这些点作为线条的顶点,适合手动绘制自定义路径:
const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const linePoints = []; // 预先创建线条对象 let line = new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xff0000 })); scene.add(line); window.addEventListener('click', (event) => { // 转换鼠标坐标为Three.js标准化设备坐标 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); // targetMesh是你要绘制线条的目标模型 const intersects = raycaster.intersectObject(targetMesh); if (intersects.length > 0) { const point = intersects[0].point.clone(); linePoints.push(point.x, point.y, point.z); updateLineGeometry(linePoints); } }); function updateLineGeometry(pointsArray) { const newGeometry = new THREE.BufferGeometry(); newGeometry.setAttribute('position', new THREE.Float32BufferAttribute(pointsArray, 3)); // 释放旧几何体资源 line.geometry.dispose(); line.geometry = newGeometry; }
二、提取模型边缘顶点(适合沿模型边缘绘制)
如果要沿模型的现有边缘绘制线条,可以直接提取几何体的索引数据,生成去重后的边顶点数组:
function extractEdgesFromGeometry(geometry) { // 转换为非索引几何体(兼容索引/非索引格式) const nonIndexedGeo = geometry.toNonIndexed(); const vertices = nonIndexedGeo.attributes.position.array; const indices = nonIndexedGeo.index.array; const edgeSet = new Set(); const lineVertices = []; // 遍历三角面的边,避免重复添加同一条边 for (let i = 0; i < indices.length; i += 3) { const a = indices[i]; const b = indices[i + 1]; const c = indices[i + 2]; addEdge(a, b); addEdge(b, c); addEdge(c, a); } function addEdge(v1, v2) { // 用排序后的顶点ID作为唯一标识,防止重复边 const key = v1 < v2 ? `${v1},${v2}` : `${v2},${v1}`; if (!edgeSet.has(key)) { edgeSet.add(key); lineVertices.push( vertices[v1 * 3], vertices[v1 * 3 + 1], vertices[v1 * 3 + 2], vertices[v2 * 3], vertices[v2 * 3 + 1], vertices[v2 * 3 + 2] ); } } return new Float32Array(lineVertices); } // 使用示例 const targetMesh = scene.getObjectByName('your-target-mesh'); const lineVertices = extractEdgesFromGeometry(targetMesh.geometry); const lineGeometry = new THREE.BufferGeometry(); lineGeometry.setAttribute('position', new THREE.BufferAttribute(lineVertices, 3)); const line = new THREE.LineSegments(lineGeometry, new THREE.LineBasicMaterial({ color: 0xff0000 })); scene.add(line);
三、在模型表面生成自定义曲线(适合复杂路径)
如果需要生成曲线类的路径,可以先定义2D/3D曲线,再通过射线检测将曲线映射到模型表面:
// 定义一条二次贝塞尔曲线 const curve = new THREE.QuadraticBezierCurve3( new THREE.Vector3(-2, 0, 0), new THREE.Vector3(0, 2, 0), new THREE.Vector3(2, 0, 0) ); const curveSamplePoints = curve.getPoints(50); // 采样50个点 const linePoints = []; const raycaster = new THREE.Raycaster(); const targetMesh = scene.getObjectByName('your-target-mesh'); curveSamplePoints.forEach((point) => { // 从曲线点向模型方向发射射线(这里假设模型在Y轴负方向,可根据实际调整) raycaster.set(point, new THREE.Vector3(0, -1, 0)); const intersects = raycaster.intersectObject(targetMesh); if (intersects.length > 0) { const surfacePoint = intersects[0].point; linePoints.push(surfacePoint.x, surfacePoint.y, surfacePoint.z); } }); // 创建线条 const lineGeometry = new THREE.BufferGeometry(); lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePoints, 3)); const line = new THREE.Line(lineGeometry, new THREE.LineBasicMaterial({ color: 0xff0000 })); scene.add(line);
四、GLB模型的特殊处理
加载GLB模型后,需要遍历子网格处理每个几何体,注意转换压缩或索引式几何体:
const loader = new THREE.GLTFLoader(); loader.load('your-model.glb', (gltf) => { const model = gltf.scene; scene.add(model); model.traverse((child) => { if (child.isMesh) { // 提取当前子网格的边缘顶点 const lineVertices = extractEdgesFromGeometry(child.geometry); const lineGeometry = new THREE.BufferGeometry(); lineGeometry.setAttribute('position', new THREE.BufferAttribute(lineVertices, 3)); const line = new THREE.LineSegments(lineGeometry, new THREE.LineBasicMaterial({ color: 0xff0000 })); // 让线条跟随模型位置旋转缩放 line.position.copy(child.position); line.rotation.copy(child.rotation); line.scale.copy(child.scale); scene.add(line); } }); });
内容的提问来源于stack exchange,提问作者mockingbird
相关产品推荐
相关产品推荐

