如何在 React 和 NextJS 中使用 ThreeJS

2025-06-08

如何在 React 和 NextJS 中使用 ThreeJS

首先我想说......“是的,我知道当你作为一名 React 或 NextJS 开发人员尝试让 3JS(threeJS)与 React 良好配合时,你会感到沮丧”。

假设你是一位具有 React 或 NextJS 背景且正在探索 ThreeJS 的 JavaScript 程序员,但你只想创建声明式且可重用的 3D 组件。现在你就可以了😁😁😁!!这一切都要归功于这个名为react-three/fiber 的库。

它是什么?它是一个 React 库,用 JSX 语言实现了 threeJS,允许你以声明式的方式创建场景,就像我所说的那样,“这些场景是可复用的、独立的组件,它们响应状态,易于交互,并且能够融入 React 的生态系统”。如果你想了解更多信息,可以直接跳转到他们的文档,当然是在本文之后。

让我们开始吧

启动您的 react 或 nextJS 项目,在本教程中我将使用 nextJS,但您也可以使用 React。

步骤 1-开始一个新项目:

  • npx create-next-app@latest

步骤 2 - 安装 threejs 和 react-three/fiber :

  • 导航到您的项目根文件夹
  • 跑步 :npm install three @react-three/fiber

步骤3-让我们创建我们的场景:

在创建任何场景之前,您首先必须考虑场景中存在的所有组件,在我们的场景中,我们将创建一个地板、灯泡、地板上的盒子,我们的用户应该能够与场景互动,我们的盒子应该是可移动的。

创建场景

在我们的索引页中,让我们创建一个场景包装器,它只是一个简单的 div JSX 元素,它将占用文档的视图高度和宽度,您的代码应如下所示:

// index.jsx
import css from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={css.scene}>

    </div>
  );
}

//Home.module.css
.scene{
    width:100vw;
    height:100vh;
}

.canvas{
    background: #000;
}
Enter fullscreen mode Exit fullscreen mode

要摆脱文档正文中应用的默认边距,您可以将以下 CSS 样式添加到 global.css 文件

body{
    margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

添加画布

下一步是,就像在 threejs 或任何绘图工具中一样,我们需要一个画布来绘制所有内容。react-three/fiber 提供了一个非常特殊的 Canvas 组件,您可以将其导入到您的场景中,您的代码应该如下所示:

import { Canvas } from "@react-three/fiber";
import css from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >

      </Canvas>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我们导入了 React Fiber 画布,并将默认相机从其默认位置移开。

创建地板组件

我们要做的下一步是创建一个地板组件。在我们项目的根目录中,创建一个名为 components 的文件夹,并在该文件夹中创建一个名为 Floor.jsx 的新函数组件。在我们的例子中,地板组件将由一个盒子网格对象组成,就像在 threejs 中一样,网格组件由GeometryMesh 材质组成(您可以在threejs中选择要使用哪种材质),我们的地板将由Box 缓冲区几何体组成,我们将要使用的材质是网格物理材质。盒子缓冲区几何体将通过 args JSX 属性获取其构造函数参数(注意:我们需要像在数组中一样传入构造函数参数)。
最后,您的地板组件将如下所示。

//components/Floor.jsx

import React from "react";

function Floor(props) {
  return (
    <mesh {...props} recieveShadow>
      <boxBufferGeometry args={[20,1,10]} />
      <meshPhysicalMaterial color='white' />
    </mesh>
  );
}

export default Floor;

Enter fullscreen mode Exit fullscreen mode

然后我们需要将这个地板导入到我们的画布中(就像在反应中一样)。

您的索引页应如下所示:

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Floor from "../components/Floor";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
          <Floor/>
      </Canvas>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

添加环境照明

一旦运行服务器,您就会注意到场景仍然是黑色的。那是因为我们在地板上使用了物理材料,而物理材料会受到光的影响(因此需要光才能看到),而我们的场景没有任何光可以照亮。

下一步我们要添加光照。我们的第一个光源是环境光,用来使物体可见(环境光会均匀地照亮场景中的所有物体,因此我们不需要过分提高光照强度)。

要添加光源,我们需要将
<ambientLight color='white' intensity={0.3}/>组件添加到场景中(ambientLight 是一个 Threejs 对象)。
组件本身就很好理解,我们只是添加了一个白色环境光,强度设置为 0.3。
你的首页应该如下所示:

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Floor from "../components/Floor";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
        <ambientLight color={"white"} intensity={0.3} />
        <Floor position={[0, -1, 0]} />
      </Canvas>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

创建一个盒子

接下来我们需要添加在每个 threeJS 教程中都会看到的著名框。

为此,就像地板一样,我们将添加一个名为 Box.jsx 的新组件,其代码如下:

import React from "react";

function Box(props) {
  return (
    <mesh {...props} recieveShadow={true} castShadow>
      <boxBufferGeometry />
      <meshPhysicalMaterial  color={"white"} />
    </mesh>
  );
}
export default Box;

Enter fullscreen mode Exit fullscreen mode

一旦我们完成了盒子的创建,我们就可以将其添加到场景中,您可能已经注意到我们正在将组件道具传递到网格中,我这样做的原因是为了使我的组件更具可重用性,这将允许我们在需要时将多个盒子📦放置在不同的区域或具有不同的尺寸。

更多照明 - 添加点光源

将盒子添加到画布后,我们现在要再次改进照明效果。这次我们将创建一个灯泡💡。为此,我们将在 components 文件夹中创建另一个名为 LightBulb.jsx 的新组件,该组件如下所示:

import React from "react";

function LightBulb(props) {
  return (
    <mesh {...props} >
      <pointLight castShadow />
      <sphereBufferGeometry args={[0.2, 30, 10]} />
      <meshPhongMaterial emissive={"yellow"}  />
    </mesh>
  );
}

export default LightBulb;

Enter fullscreen mode Exit fullscreen mode

您需要将其放置在场景中稍微高一点的位置,您的索引页(场景)应该如下所示:

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import LightBulb from "../components/Light";
import Floor from "../components/Floor";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
          <ambientLight color={"white"} intensity={0.2} />
          <LightBulb position={[0, 3, 0]} />
          <Box rotateX={3} rotateY={0.2} />
          <Floor position={[0, -1, 0]} />
      </Canvas>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

与场景交互 - 添加轨道控制

一切进展顺利,但问题在于我们无法与场景交互,无法绕着场景的轨道移动。为了实现这一点,我们需要在场景中添加OrbitControls 。

让我们开始吧,在我们的组件文件夹中创建一个名为 OrbitControls.jsx 的新组件

组件组件应该如下所示:

// components/OrbitControls.jsx

import React from "react";
import { extend, useThree } from "@react-three/fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

extend({ OrbitControls });

function Controls(props) {
  const { camera, gl } = useThree();
  return <orbitControls attach={"orbitControls"}  args={[camera, gl.domElement]} />;
}

export default Controls;

Enter fullscreen mode Exit fullscreen mode

就像在 threeJS 中一样,OrbitControls 需要对相机和渲染器 domElement 的引用,我们使用 react-fiber 提供的 useThree() 钩子来获取场景的相机和渲染器,在使用 react fiber 中的控件时,我们需要首先调用该extend({OrbitControls})函数,这会扩展/添加其他功能以响应光纤,在本例中为 OrbitControls。然后,我们需要添加 OrbitControls 组件并使用其中的 attachment 属性将 OrbitControls 附加到场景(这将允许我们在任何组件中访问轨道控件),当我们想要使我们的框可拖动时,这将非常方便,当我们在场景中拖动框时,我们需要禁用轨道控件。

完成后,我们需要将 OrbitControls 导入到场景中……您的索引页应如下所示

//index.jsx

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
        <ambientLight color={"white"} intensity={0.2} />
        <Light position={[0, 3, 0]} />
          <Box rotateX={3} rotateY={0.2} />
        <OrbitControls />
        <Floor position={[0, -1, 0]} />
      </Canvas>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

如果添加这样的控件似乎很困难,我有个好消息📰react three fiber 的创建者非常友好地为我们提供了其他有用的库,我们可以扩展 react three fiber 其中之一就是drei ... Drei通过抽象我们必须实现的一些细节,使您能够轻松添加控件,在本文中我不会谈论使用Drei,您可以稍后查看它。

添加拖拽控件

快完成了,我们可以使用 OrbitControls 在场景中移动主摄像机,但仍然无法移动盒子。为此,我们需要将盒子设置为可拖动。

我们将创建一个名为 Draggable.jsx 的新组件,它看起来像这样:

import React, { useEffect, useRef, useState } from "react";
import { extend, useThree } from "@react-three/fiber";
import { DragControls } from "three/examples/jsm/controls/DragControls";

extend({ DragControls });

function Draggable(props) {
  const groupRef = useRef();
  const controlsRef = useRef();
  const [objects, setObjects] = useState();
  const { camera, gl, scene } = useThree();
  useEffect(() => {
    setObjects(groupRef.current.children);
  }, [groupRef]);

  useEffect(() => {
    controlsRef.current.addEventListener("hoveron", () => {
      scene.orbitControls.enabled = false;
    });
    controlsRef.current.addEventListener("hoveroff", () => {
      scene.orbitControls.enabled = true;
    });
  }, [objects]);
  return (
    <group ref={groupRef}>
      <dragControls ref={controlsRef} args={[objects, camera, gl.domElement]} />
      {props.children}
    </group>
  );
}

export default Draggable;
Enter fullscreen mode Exit fullscreen mode

您会注意到,使用控件的步骤基本相同,就像之前我们的拖动控件需要对相机、渲染器的 dom 元素的引用,在本例中是要应用拖动功能的子项 * 3D 对象 *,因为我们传入的 react props 是 react 组件,所以我们需要用 fiber 提供的 group JSX 元素将 react prop children 包装起来,然后创建对该组的引用并从该组中提取 3D 对象子项。我们使用了 useEffect,因为我们只需要在 groupRef 设置或更改时执行此操作。最后,在第二个 use Effect 中,当您将鼠标悬停在可拖动项目上时,我们会禁用 Orbit 控件,当您将鼠标悬停离开时,我们会重新启用它,请原谅我在卸载组件时没有删除 eventListeners,您应该这样做,这是一种避免内存泄漏的良好做法。

然后我们需要用这个可拖动组件包装我们的盒子。在我们的索引页中,我们的代码应该是这样的:

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";
import Draggable from "../components/Draggable";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
        <ambientLight color={"white"} intensity={0.2} />
        <Light position={[0, 3, 0]} />
        <Draggable>
            <Box rotateX={3} rotateY={0.2} />
        </Draggable>
        <OrbitControls />
        <Floor position={[0, -1, 0]} />
      </Canvas>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

为材质添加纹理

作为额外功能,我们来为网格添加一个纹理贴图。
在我们的 Box 组件中,我们需要导入import { useLoader } from "@react-three/fiber";
调用 useLoader 的第一个参数,该参数接受一个加载器,在本例中我们使用了 TextureLoader,第二个参数接受我们要加载的纹理的路径。然后,我们创建一个纹理贴图 (textureMap) 并将其加载到材质中,代码如下所示:import { TextureLoader } from "three";


import React from "react";
import { useLoader } from "@react-three/fiber";
import { TextureLoader } from "three";

function Box(props) {
  const texture = useLoader(TextureLoader, "/texture.jpg");
  return (
    <mesh {...props} recieveShadow castShadow>
      <boxBufferGeometry />
      <meshPhysicalMaterial map={texture} color={"white"} />
    </mesh>
  );
}

export default Box;

Enter fullscreen mode Exit fullscreen mode

由于我们添加的纹理是需要加载的外部资源文件,因此我们需要用 Suspense 包装我们的盒子组件,以便仅在纹理文件加载完成时才渲染该组件。

我们的 indexPage 看起来是这样的:

import css from "../styles/Home.module.css";
import { Canvas } from "@react-three/fiber";
import Box from "../components/Box";
import OrbitControls from "../components/OrbitControls";
import Light from "../components/Light";
import Floor from "../components/Floor";
import Draggable from "../components/Draggable";
import {Suspense} from "react";

export default function Home() {
  return (
    <div className={css.scene}>
      <Canvas
        shadows
        className={css.canvas}
        camera={{
          position: [-6, 7, 7],
        }}
      >
        <ambientLight color={"white"} intensity={0.2} />
        <Light position={[0, 3, 0]} />
        <Draggable>
        <Suspense fallback={null}>
            <Box rotateX={3} rotateY={0.2} />
        </Suspense>
        </Draggable>
        <OrbitControls />
        <Floor position={[0, -1, 0]} />
      </Canvas>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

就这样,我们刚刚使用 React Fiber 在 React/NextJS 项目中创建了一个场景,还有很多内容值得探索。我喜欢 React Fiber 的地方在于它简单易用且直观,但我希望它的文档可以改进(也许在阅读并试用一段时间后,你就能胜任这项任务了)。

我希望您喜欢这篇文章,并且我希望听到您的评论。

鏂囩珷鏉ユ簮锛�https://dev.to/hnicolus/how-to-use-thirdjs-in-react-nextjs-4120
PREV
像专业人士一样使用 React Context API Context API 背后的概念考虑这个例子使用 Context 创建 Context
NEXT
fetch 和 XMLHTTPRequest 有什么区别?⚙️