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

如何在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>
  );
}

当前效果:
经Simplex Noise修改的平面线框图

解决方案

你当前的代码只是抬升了部分顶点,并没有真正创建孔洞——孔洞需要移除对应区域的面,并且如果是实体的话,还要处理上下表面和侧面的衔接。下面提供两种可行方案:

方案一:手动修改几何体顶点与面(适合简单场景)

这种方法直接操作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库:

  1. 先安装依赖:
npm install @react-three/csg
  1. 实现代码:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 11:35:54