让我们用 React 和 three.js 构建 3D 程序化景观!1. 设置项目 2. 设置画布 3. 创建场景 4. 添加灯光 5. 添加控件 6. 创建地形 7. 生成景观

2025-06-10

让我们使用 React 和 three.js 构建 3D 程序景观!

1. 设置项目

2. 设置画布

3.创建场景

4. 添加灯光

5. 添加控件

6.创建地形

7. 生成景观

如今,JavaScript 可以做很多有趣的事情,其中​​之一就是在浏览器中构建 3D 内容。在本教程中,我将向您展示如何使用 React 和 three.js 构建 3D 景观。

这是针对 three.js 初学者的教程,许多类似的教程仅教您如何在浏览器中创建旋转框,但我们将更进一步,使用 React 创建实际景观,设置正确的灯光、相机等!

我假设您具有使用 JavaScript ES6+、React 和 webpack 以及 npm 或 yarn 的基本知识(在本教程中我将使用 yarn,最近我从 npm 切换过来)。


1. 设置项目

我们将使用 three.js,这是一个 3D JavaScript 库(https://threejs.org),以及 react-three-fiber(https://github.com/react-spring/react-three-fiber),这是一个很棒的“调节器”,它为我们提供了可重复使用的组件,使我们的世界变得更加简单,同时保持与 three.js 相同的性能。

让我们首先使用 create-react-app 初始化我们的新应用程序:
$ npx create-react-app 3d-landscape

然后我们将安装三个和三个 react-fiber 包:
$ yarn add three react-three-fiber

并删除 /src 文件夹中除 index.css 和 index.js 之外的所有文件。

现在在 /src 内创建以下文件夹和文件:

src
|--components
|  |--Controls
|  |  |--index.js
|  |--Scene
|  |  |--Lights
|  |  |  |--index.js
|  |  |--Terrain
|  |  |  |--index.js
|  |  index.js
index.css
index.js

我正在使用 Visual Studio Code 的 React 代码片段扩展,强烈推荐使用它。只需在 JS 文件中输入“rafce”并按下回车键,你的 React 组件就设置好了!我还使用其他扩展,例如 eslint 和 prettier。

现在本教程不关注 CSS,因此只需将我的 CSS 复制到 /src 文件夹中的主 index.css 文件中。

@import url("https://fonts.googleapis.com/css?family=News+Cycle&display=swap");
:root {
  font-size: 20px;
}

html,
body {
  margin: 0;
  padding: 0;
  background: #070712;
  color: #606063;
  overflow: hidden;
  font-family: "News Cycle", sans-serif;
}

#root {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

canvas,
.canvas > div {
  z-index: 1;
}

.loading {
  padding: 10px;
  transform: translate3d(-50%, -50%, 0);
}

2. 设置画布

接下来我们将在 src 文件夹中的 index.js 文件中设置画布。

你总是需要定义一个画布,并将 Three.js 场景中的所有内容放入其中。我们还可以在那里声明一个相机,并定义它的缩放级别和位置。通过使用 Suspense,React 会等待场景加载完成,然后向用户显示动画或加载屏幕。

import React, { Suspense } from "react";
import ReactDOM from "react-dom";
import { Canvas, Dom } from "react-three-fiber";
import "./index.css";

function App() {
  return (
      <Canvas camera={{ zoom: 40, position: [0, 0, 500] }}>
        <Suspense
          fallback={<Dom center className="loading" children="Loading..." />}
        >
        </Suspense>
      </Canvas>
  );
}

const root = document.getElementById("root");
ReactDOM.render(<App />, root);

3.创建场景

接下来我们将创建场景组件,它作为场景内所有组件(地形和灯光)的容器。

import React from "react";
import Lights from './Lights';
import Terrain from "./Terrain";

const Scene = () => (
  <>
    <Lights />
    <Terrain />
  </>
);

export default Scene;

然后确保将场景包含到我们的主要 index.js 文件中并将其放置在我们的 Suspense 组件中。


4. 添加灯光

在 /lights 文件夹中的 index.js 文件中,我们将以下内容分组:

  • 1个假球灯
  • 1 个环境光
  • 1个定向光
  • 2个点光源

如果你想先学习 three.js 的基础知识,我建议你阅读https://threejsfundamentals.org/上的部分或全部章节

import React from "react";

export default () => {
  const FakeSphere = () => (
    <mesh>
      <sphereBufferGeometry attach="geometry" args={[0.7, 30, 30]} />
      <meshBasicMaterial attach="material" color={0xfff1ef} />
    </mesh>
  );

  return (
    <group>
      <FakeSphere />
      <ambientLight position={[0, 4, 0]} intensity={0.3} />
      <directionalLight intensity={0.5} position={[0, 0, 0]} color={0xffffff} />
      <pointLight
        intensity={1.9}
        position={[-6, 3, -6]}
        color={0xffcc77}
      />
      <pointLight
        intensity={1.9}
        position={[6, 3, 6]}
        color={0xffcc77}
        />
    </group>
  );
};

React-three-fiber 为我们提供了易于使用的组件,我们可以将它们组合在一起并赋予属性。现在你仍然会看到画布上呈现的是黑屏(请务必注释掉我们稍后要创建的地形组件)。这是因为我们的光源没有可以照射的地方。你可以想象,如果有一些引导线来指示光源的位置,那将是多么实用。Three.js实际上提供了一些光源辅助函数来实现这一点!让我们开始设置它们吧。

我们需要使用 useRef() 将我们的灯连接到我们的 light-helper,react-three-fiber 为我们提供了 useResource 钩子,它创建一个 ref 并在下一帧可用时重新渲染该组件。

import React from "react";
import { useResource } from "react-three-fiber";

export default () => {
  const FakeSphere = () => (
    <mesh>
      <sphereBufferGeometry attach="geometry" args={[0.7, 250, 250]} />
      <meshBasicMaterial attach="material" color={0xfff1ef} />
    </mesh>
  );

  const [ref, pLight1] = useResource();
  const [ref2, pLight2] = useResource();

  return (
    <group>
      <FakeSphere />
      <ambientLight ref={ref2} position={[0, 4, 0]} intensity={0.3} />

      <directionalLight intensity={0.5} position={[0, 0, 0]} color={0xffffff} />

      <pointLight
        ref={ref}
        intensity={1}
        position={[-6, 3, -6]}
        color={0xffcc77}
      >
        {pLight1 && <pointLightHelper args={[pLight1]} />}
      </pointLight>

      <pointLight
        ref={ref2}
        intensity={1}
        position={[6, 3, 6]}
        color={0xffcc77}
      >
        {pLight2 && <pointLightHelper args={[pLight2]} />}
      </pointLight>
    </group>
  );
};

黑色场景

灯光仍然没有照耀到任何东西,但我们现在可以看到它们的位置!


5. 添加控件

让我们回到 src 文件夹中的主 index.js 文件并设置相机的控制。

import Controls from "./components/Controls";
import Scene from './components/Scene';

function App() {
  return (
      <Canvas camera={{ zoom: 40, position: [0, 0, 500] }}>
        <Suspense
          fallback={<Dom center className="loading" children="Loading..." />}
        >
          <Controls />
          <Scene />
        </Suspense>
      </Canvas>
  );
}

在控件文件夹下的 index.js 中,我们将添加 OrbitControls,以便用户可以围绕我们的景观进行轨道运动。Three.js 提供了更多控件(https://threejs.org/docs/#examples/en/controls/OrbitControls)。

通过使用extend(),我们可以使用我们的代码扩展来自three.js的原生orbitcontrols。

我们需要useRef()来在useFrame()函数中定义的每一帧渲染中引用和更新我们的相机

OrbitControls 始终需要两个属性:相机和用于渲染的 DOM 元素。我们还将通过添加{...props}来使组件能够检索更多道具。

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

extend({ OrbitControls });

const Controls = props => {
  const ref = useRef();
  const {
    camera,
    gl: { domElement }
  } = useThree();
  useFrame(() => ref.current && ref.current.update());
  return <orbitControls ref={ref} args={[camera, domElement]} {...props} />;
};

export default Controls;

惊人的!


6.创建地形

现在到了最酷的部分,我们真正看到了灯光和控件在做什么!将地形组件导入场景组件,并在 Terrain 文件夹中打开 index.js。

现在我们只渲染一个旋转的基本平面。我们将使用 useRef() 引用我们的网格,并在每一帧增加它的 z 轴旋转。

每个网格组件都需要包含两个元素:材质和几何形状。three.js 中提供了许多不同的材质(https://threejsfundamentals.org/threejs/lessons/threejs-materials.html)和几何形状(https://threejs.org/docs/#api/en/core/Geometry)。

再次,我们将提供属性来设置几何体的大小和位置,以及定义我们的材质及其属性。

import React, {useRef} from "react";
import { useFrame } from "react-three-fiber";

const Terrain = () => {

  const mesh = useRef();

  // Raf loop
  useFrame(() => {
    mesh.current.rotation.z += 0.01;
  });

  return (
    <mesh ref={mesh} rotation={[-Math.PI / 2, 0, 0]}>
      <planeBufferGeometry attach="geometry" args={[25, 25, 75, 75]} />
      <meshPhongMaterial
        attach="material"
        color={"hotpink"}
        specular={"hotpink"}
        shininess={3}
        flatShading
      />
    </mesh>
  );
};  

export default Terrain;

平面旋转

现在你应该能看到一个基本的平面(稍微旋转一下视角就能看到)。很酷吧!我们可以给这个平面添加任何你想要的颜色或纹理。目前我们先让它保持粉色。

通过添加 -Math.PI / 2,飞机将水平放置而不是垂直放置。


7. 生成景观

我们希望拥有比这个基本平面更有趣的地形,所以我们将程序化渲染一个。这意味着我们通过算法而不是手动创建它。每次重新加载时,地形看起来都会不同。

首先在 Terrain 文件夹中创建一个名为 perlin.js 的新文件,其中包含一个名为 Perlin noise 的算法(https://en.wikipedia.org/wiki/Perlin_noise)。

您可以在此处找到该算法,复制我们的 perlin.js 文件中的内容:
https://github.com/josephg/noisejs/blob/master/perlin.js

然后将其导入到我们的index.js文件中。

我们将使用react-three-fiber 中的useUpdate()来强制更新几何平面。

我们的平面由许多顶点组成,我们可以赋予它们随机的宽度和高度,使平面看起来像一幅风景画。这个顶点数组实际上位于我们的几何对象内部:

顶点数组

在 useUpdate 函数中,我们将循环遍历每个顶点,并使用柏林噪声算法对每个值进行随机化。
我使用了在 Codepen 中找到的随机化方法:https://codepen.io/ptc24/pen/BpXbOW? editors=1010 。

import React from "react";
import { useFrame, useUpdate } from "react-three-fiber";

import { noise } from "./perlin";

const Terrain = () => {
  const mesh = useUpdate(({ geometry }) => {
    noise.seed(Math.random());
    let pos = geometry.getAttribute("position");
    let pa = pos.array;
    const hVerts = geometry.parameters.heightSegments + 1;
    const wVerts = geometry.parameters.widthSegments + 1;
    for (let j = 0; j < hVerts; j++) {
      for (let i = 0; i < wVerts; i++) {
        const ex = 1.1;
        pa[3 * (j * wVerts + i) + 2] =
          (noise.simplex2(i / 100, j / 100) +
            noise.simplex2((i + 200) / 50, j / 50) * Math.pow(ex, 1) +
            noise.simplex2((i + 400) / 25, j / 25) * Math.pow(ex, 2) +
            noise.simplex2((i + 600) / 12.5, j / 12.5) * Math.pow(ex, 3) +
            +(noise.simplex2((i + 800) / 6.25, j / 6.25) * Math.pow(ex, 4))) /
          2;
      }
    }

    pos.needsUpdate = true;
  });

  // Raf loop
  useFrame(() => {
    mesh.current.rotation.z += 0.001;
  });

  return (
    <mesh ref={mesh} rotation={[-Math.PI / 2, 0, 0]}>
      <planeBufferGeometry attach="geometry" args={[25, 25, 75, 75]} />
      <meshPhongMaterial
        attach="material"
        color={"hotpink"}
        specular={"hotpink"}
        shininess={3}
        flatShading
      />
    </mesh>
  );
};

export default Terrain;

令人敬畏的风景

就是这样,干得好!

现在您可以做很多其他的事情,比如以星星的形式添加粒子、改变灯光和控件,甚至在屏幕上添加 3D 动画并向其中添加控件(制作您自己的游戏)。

例如,只需添加 wireframe={true} 作为材质属性,即可将材质更改为线框:

线框

或者将 flatShading 改为 SmoothShading:

平滑着色

就是这样,尽情享受在 3D 中构建精彩事物的乐趣吧!

查看 repo:https://github.com/sanderdebr/three-dev-tutorial

鏂囩珷鏉ユ簮锛�https://dev.to/sanderdebr/let-s-build-3d-procedural-landscape-with-react-and- Three-js-47a0
PREV
Node.js 与 PHP 对比
NEXT
⭐Angular 13 功能⭐如何更新到版本 13