如何在Three.js的Simplex Noise平面中切割实体孔洞?
如何用Simplex Noise创建带孔洞的实体平面几何体(非着色器实现)
问题描述
我创建了一个使用Simplex Noise抬升部分顶点的平面,抬升的顶点区域是我需要从平面上切除的部分(类似瑞士奶酪结构)。我需要的是带有孔洞的实体几何体,不使用着色器,请问该如何实现?
当前实现代码:
function NoisePlane() { const meshRef = useRef(); const [widthSegments, heightSegments] = [100, 100]; const vertices = useMemo(() => { const geometry = new PlaneGeometry(10, 10, widthSegments, heightSegments); const simplex = createNoise2D(); const threshold = 0.5; const positions = geometry.attributes.position.array; for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; const z = positions[i + 2]; const noiseValue = simplex(x, y); console.log(noiseValue); positions[i + 2] = noiseValue > threshold ? 0.5 : 0; } geometry.computeVertexNormals(); return geometry; }, [widthSegments, heightSegments]); return ( <mesh ref={meshRef} geometry={vertices}> <meshBasicMaterial color={0x00ff00} wireframe /> </mesh> ); }
当前效果:
解决方案
你当前的代码只是抬升了部分顶点,并没有真正创建孔洞——孔洞需要移除对应区域的面,并且如果是实体的话,还要处理上下表面和侧面的衔接。下面提供两种可行方案:
方案一:手动修改几何体顶点与面(适合简单场景)
这种方法直接操作PlaneGeometry的索引数组,移除需要切除区域的面,再手动挤压成实体:
function NoiseHolePlane() { const meshRef = useRef(); const [widthSegments, heightSegments] = [100, 100]; const threshold = 0.5; const geometry = useMemo(() => { const planeGeo = new PlaneGeometry(10, 10, widthSegments, heightSegments); const simplex = createNoise2D(); const positions = planeGeo.attributes.position.array; const isCut = []; // 标记每个顶点是否属于要切除的区域 for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; const noiseValue = simplex(x, y); isCut.push(noiseValue > threshold); positions[i + 2] = 0; // 恢复平面的z坐标 } // 过滤索引:移除完全在切除区域的面 const indices = planeGeo.index.array; const newIndices = []; for (let i = 0; i < indices.length; i += 3) { const idx1 = indices[i]; const idx2 = indices[i + 1]; const idx3 = indices[i + 2]; // 三个顶点都在切除区域才移除这个面 if (!(isCut[idx1] && isCut[idx2] && isCut[idx3])) { newIndices.push(idx1, idx2, idx3); } } // 更新几何体索引 planeGeo.index = new BufferAttribute(new Uint32Array(newIndices), 1); planeGeo.computeVertexNormals(); planeGeo.computeBoundingBox(); // 挤压成实体(手动构建上下表面和侧面,侧面处理较复杂) const depth = 0.5; const topPositions = [...positions]; // 生成顶部顶点(z坐标设为厚度值) for (let i = 2; i < topPositions.length; i += 3) { topPositions[i] = depth; } // 合并上下顶点 const allPositions = [...positions, ...topPositions]; // 生成底部面索引(反转顺序保证法线正确) const bottomIndices = []; for (let i = 0; i < newIndices.length; i += 3) { const offset = positions.length / 3; bottomIndices.push( newIndices[i] + offset, newIndices[i + 2] + offset, newIndices[i + 1] + offset ); } // 合并索引(顶部+底部,侧面需额外处理,这里省略) const finalIndices = [...newIndices, ...bottomIndices]; const solidGeo = new BufferGeometry(); solidGeo.setAttribute('position', new BufferAttribute(new Float32Array(allPositions), 3)); solidGeo.setIndex(new BufferAttribute(new Uint32Array(finalIndices), 1)); solidGeo.computeVertexNormals(); return solidGeo; }, [widthSegments, heightSegments]); return ( <mesh ref={meshRef} geometry={geometry}> <meshStandardMaterial color={0x00ff00} /> </mesh> ); }
注意:手动处理侧面需要遍历所有边缘顶点,逻辑较繁琐,适合简单平面孔洞场景。
方案二:使用CSG布尔运算(推荐,适合复杂实体)
CSG(构造实体几何)可以通过布尔运算(减、交、并)快速生成带孔洞的实体,无需手动处理顶点。在react-three-fiber中可以使用@react-three/csg库:
- 先安装依赖:
npm install @react-three/csg
- 实现代码:
import { Subtract, Mesh as CSGMesh } from '@react-three/csg'; import { createNoise2D } from 'simplex-noise'; function NoiseHoleSolid() { const meshRef = useRef(); const [widthSegments, heightSegments] = [100, 100]; const threshold = 0.5; const simplex = createNoise2D(); // 生成需要切除的区域对应的几何体位置 const cutters = useMemo(() => { const cutters = []; const stepX = 10 / widthSegments; const stepY = 10 / heightSegments; // 遍历平面网格点,标记Noise超过阈值的位置 for (let x = -5; x < 5; x += stepX) { for (let y = -5; y < 5; y += stepY) { if (simplex(x, y) > threshold) { cutters.push({ x, y }); } } } return cutters; }, [widthSegments, heightSegments]); return ( <CSGMesh ref={meshRef}> <Subtract> {/* 基础实体:带厚度的立方体,对应平面大小 */} <boxGeometry args={[10, 10, 0.5]} /> <meshStandardMaterial color={0x00ff00} /> {/* 切割用的球体:从基础实体中减去这些球体,形成孔洞 */} {cutters.map((pos, idx) => ( <mesh key={idx}> <sphereGeometry args={[0.3, 8, 8]} /> <meshStandardMaterial /> <position x={pos.x} y={pos.y} z={0} /> </mesh> ))} </Subtract> </CSGMesh> ); }
这种方法的优势是自动处理实体的孔洞和表面衔接,适合复杂的Noise区域。如果切割几何体数量过多,可以适当合并相邻区域或降低切割体精度来优化性能。
内容的提问来源于stack exchange,提问作者Ashbury
相关产品推荐
相关产品推荐

