你是在开发医疗网络应用程序、ThreeJS 粉丝,还是对 3D 图形的美妙世界感到好奇?你来对地方了!在这篇文章中,我们将学习如何对网格进行切片。

我们的团队已经在医疗网络领域工作了 10 多年(时间过得真快!)。尽管对网格进行切片是所有医疗应用的核心部分,但关于该主题的分享却很少。如今,新的开源工具让它变得轻而易举,我们想告诉你我们在 Promaton 是如何做到的。

1、设置场景

为了演示这个原理,我们将构建一个小型 R3F 应用程序,它允许我们用平面切割“形状”。

我们定义了一个 3D 模型(TorusKnot)和一个切片平面。我们使用“ useControls ”提供的滑块来控制平面的位置。

我们还设置了照明、控件和阴影,以使演示更有趣,但我们不会在这篇文章中介绍!

export default function App() {
  const { constant, transparent } = useControls('plane', {
    transparent: true,
    constant: { value: 0, min: -1, max: 1, step: 0.01 },
  })
  return (
<>
  <Canvas shadows onCreated={(state) => (state.gl.localClippingEnabled = true)}>
    <TorusKnot constant={constant} />
    <SlicingPlane constant={constant} transparent={transparent} />
    {/* We also setup some controls, background color and lighing */}
    <OrbitControls />
    <color attach="background" args={['lightblue']} />
    <Lights />
  </Canvas>
</>
)}

2、切片 3D模型

在 ThreeJS 中切片网格很简单。我们打开了一些选项,将剪裁平面作为材质属性提供给网格,仅此而已。

function TorusKnot({ constant }) {
  const torusKnotSettings = useMemo(() => {
    return [1, 0.2, 50, 50, 2, 3]
  }, [])
  const clippingPlane = useMemo(() => {
    const plane = new Plane()
    plane.normal.set(0, 0, -1)
    return plane
  }, [])
  // Adjust the clipping plane when the constant changes
  useEffect(() => {
    clippingPlane.constant = constant
  }, [clippingPlane, constant])
return (
<>
  {/* Outside of the TorusKnot is Hot Pink */}
  <mesh castShadow receiveShadow>
    <torusKnotBufferGeometry attach="geometry" args={torusKnotSettings} /
    <meshStandardMaterial
      attach="material"
      roughness={1}
      metalness={0.1}
      clippingPlanes={[clippingPlane]}
      clipShadows={true}
      color={'hotpink'}
    />
  </mesh>
  {/* Inside of the TorusKnot  is Dark Pink*/}
  <mesh>
    <torusKnotBufferGeometry attach="geometry" args={torusKnotSettings} /
    <meshStandardMaterial
      attach="material"
      roughness={1}
      metalness={0.1}
      clippingPlanes={[clippingPlane]}
      color={'#E75480'}
      side={BackSide}
    />
  </mesh>
</>
)}

ThreeJS 在GPU上执行网格切片。这使得单独使用切片变得困难:它只能通过GPU缓冲区访问,并且执行更改切片颜色和厚度等操作会变得非常复杂。

或者,可以在CPU上通过检查网格的每个面是否与 as 切片平面相交来实现切片。平面和面之间总是有 0 或 2 个交点。如果我们把所有这些段加起来,我们就有一条定义“切片”的“线”。

计算所有相交段的一种简单方法是迭代网格的所有三角形并测试它们与切片平面的相交。它的效率非常低,并且随着模型规模的扩大,此操作可能会变得越来越慢。

这就是three-bvh-mesh发挥作用的地方!它通过将网格几何存储在BVH 树中来加速所有空间查询。“face to plane”相交查询仅发生在靠近切片平面的面上。它允许你实时计算所有相交段,即使对于非常大的网格也是如此。它可以让你做更多的事情,值得一看!

然后我们可以将所有相交段存储在一个类型化数组中。类型化数组包含显示网格切片所需的所有信息。它可以很容易地被ThreeJS 用于可视化。

注意:出于性能原因,我们使用预分配的类型化数组,因为每次切片平面的位置更改时创建新数组的成本很高。
...
const bvhMesh = useMemo(() => {
  // setup BVH Mesh
  const geometry = new TorusKnotBufferGeometry(1, 0.2, 50, 50, 2, 3)
  return new MeshBVH(geometry, { maxLeafTris: 3 })
}, [])
...
// code re-used and adjusted from https://gkjohnson.github.io/three-mesh-bvh/example/bundle/clippedEdges.html
bvhMesh.shapecast({
  intersectsBounds: (box) => {
    return defaultPlane.intersectsBox(box)
  },
  
  intersectsTriangle: (tri) => {
  // check each triangle edge to see if it intersects with the clippingPlane. If so then add it to the list of segments.
    let count = 0
    tempLine.start.copy(tri.a)
    tempLine.end.copy(tri.b)
    if (defaultPlane.intersectLine(tempLine, tempVector)) {
      posAttr.setXYZ(index, tempVector.x, tempVector.y, tempVector.z)
      index++
      count++
    }
    tempLine.start.copy(tri.b)
    tempLine.end.copy(tri.c)
    if (defaultPlane.intersectLine(tempLine, tempVector)) {
      posAttr.setXYZ(index, tempVector.x, tempVector.y, tempVector.z)
      count++
      index++
    }
    tempLine.start.copy(tri.c)
    tempLine.end.copy(tri.a)
    if (defaultPlane.intersectLine(tempLine, tempVector)) {
      posAttr.setXYZ(index, tempVector.x, tempVector.y, tempVector.z)
      count++
      index++
    }
    // If the place intersected with one or all sides then just remove it
    if (count !== 2) {
      index -= count
    }
  }
})

现在,我们在类型化数组中拥有了描述切片形状的所有线段。让我们展示它!

3、显示切片

为了渲染切片,我们使用 ThreeJS 中的 LineSegments。它开箱即用,我们在步骤 2 中创建的类型化数组可以直接插入其中。如果你想更好地控制方面(宽度、虚线等),请考虑 LineSegment2。

请注意,你可能需要调整 lineSegments 的渲染顺序或材质的“polygonOffset*”和“depthTest”以获得更好的结果!
function TorusKnotSlice({ constant }) {
  const lineSegRef = useRef()
  const geomRef = useRef()
  const bvhMesh = ...
  useEffect(() => {
    if (bvhMesh && geomRef.current && lineSegRef.current) {
      if (geomRef.current) {
        const geo = geomRef.current
        if (!geo.hasAttribute('position')) {
          const linePosAttr = new BufferAttribute(defaultArray, 3, false)
          linePosAttr.setUsage(DynamicDrawUsage)
          geo.setAttribute('position', linePosAttr
        }
      } 
      let index = 0
      const posAttr = geomRef.current.attributes.position
      defaultPlane.constant = constant
      bvhMesh.shapecast(...)
      // set the draw range to only the new segments and offset the lines so they don't intersect with the geometry
      geomRef.current.setDrawRange(0, index)
      posAttr.needsUpdate = true
    }
  }, [constant, bvhMesh, defaultArray, defaultPlane])
return (
<>
  <lineSegments ref={lineSegRef} frustumCulled={false} matrixAutoUpdate={false} renderOrder={3}>
    <bufferGeometry ref={geomRef} attach="geometry" /
    <lineBasicMaterial
      attach="material"
      // neon yellow
      color={'#ccff15'}
      linewidth={1}
      linecap={'round'}
      linejoin={'round'}
      // battle the z-fightinh
      polygonOffset={true}
      polygonOffsetFactor={-1.0}
      polygonOffsetUnits={4.0}
      depthTest={false}
    />
  </lineSegments>
</>
)}

4、享受结果

我们将所有部分放在这个代码沙盒中,供你玩耍!


原文链接:Three steps to slice a mesh with ThreeJS

BimAnt翻译整理,转载请标明出处