让我们使用 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