├── public
├── grass.jpg
├── rock.jpg
├── snow.jpg
├── favicon.ico
├── robots.txt
└── index.html
├── src
├── components
│ ├── WireframeMaterial.jsx
│ ├── TerrainManager.jsx
│ ├── MountainMaterial.jsx
│ └── Terrain.jsx
├── index.jsx
├── index.css
└── App.jsx
├── .gitignore
├── README.md
└── package.json
/public/grass.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozzius/r3f-terrain/HEAD/public/grass.jpg
--------------------------------------------------------------------------------
/public/rock.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozzius/r3f-terrain/HEAD/public/rock.jpg
--------------------------------------------------------------------------------
/public/snow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozzius/r3f-terrain/HEAD/public/snow.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozzius/r3f-terrain/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/components/WireframeMaterial.jsx:
--------------------------------------------------------------------------------
1 | const WireframeMaterial = () => ;
2 |
3 | export default WireframeMaterial;
4 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { Leva } from "leva";
4 | import "./index.css";
5 | import App from "./App";
6 |
7 | createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | #root {
6 | position: fixed;
7 | width: 100%;
8 | height: 100%;
9 | top: 0;
10 | left: 0;
11 | }
12 |
13 | .loading {
14 | position: fixed;
15 | top: 0;
16 | left: 0;
17 | width: 100%;
18 | height: 100%;
19 | z-index: 9999;
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | font-size: 50px;
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { Canvas } from "@react-three/fiber";
3 | import { OrbitControls, Sky } from "@react-three/drei";
4 |
5 | import TerrainManager from "./components/TerrainManager";
6 |
7 | const App = () => {
8 | return (
9 | ⛰}>
10 |
20 |
21 | );
22 | };
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 | Terrain Generator
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/TerrainManager.jsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from "react";
2 | import { button, useControls } from "leva";
3 |
4 | import Terrain from "./Terrain";
5 |
6 | const TerrainManager = () => {
7 | const [seed, setSeed] = useState(Date.now());
8 |
9 | const { resolution, height, levels, scale, offsetX, offsetZ } = useControls({
10 | generate: button(() => setSeed(Date.now())),
11 | resolution: { value: 50, min: 10, max: 500, step: 1 },
12 | height: { value: 0.2, min: 0, max: 1 },
13 | levels: { value: 8, min: 1, max: 16, step: 1 },
14 | scale: { value: 1, min: 1, max: 16, step: 1 },
15 | offsetX: 0,
16 | offsetZ: 0,
17 | });
18 |
19 | const offset = useMemo(
20 | () => ({ x: offsetX, z: offsetZ }),
21 | [offsetX, offsetZ]
22 | );
23 |
24 | return (
25 |
33 | );
34 | };
35 |
36 | export default TerrainManager;
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Terrain Generator
2 |
3 | A simple Simplex noise terrain generator, built with THREE and @react-three/fiber.
4 |
5 | [See it here!](https://terrain.mozzius.now.sh) Hosted on Vercel.
6 |
7 | ## How it works
8 |
9 | Terrain is generated by adding layers of noise on top of each other at increasingly fine resolutions, which creates an approximation of mountainous terrain. This is then used as a heightmap to modify the height of the vertices of a plane. You can fiddle with the options panel to see what happens when you change the levels of noise or the scale.
10 |
11 | For more information on the topic, I recommend [this excellent YouTube video](https://www.youtube.com/watch?v=eaXk97ujbPQ).
12 |
13 | This is all implemented using @react-three/fiber, which is basically React for THREE elements instead of HTML elements. It allows you to write THREE code imperatively, using React. This in turn removes a lot of the boilerplate that normally comes with THREE, along with all the other benefits that React comes with.
14 |
15 | ## Running it yourself
16 |
17 | It uses the Create React App template, so simply run:
18 |
19 | ```
20 | > yarn
21 | > yarn start
22 | ```
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "terrain-fiber",
3 | "version": "0.1.2",
4 | "description": "Terrain generator, using @react-three/fiber",
5 | "author": "Samuel Newman",
6 | "license": "MIT",
7 | "dependencies": {
8 | "@react-three/drei": "^9.40.4",
9 | "@react-three/fiber": "^8.9.1",
10 | "leva": "^0.9.34",
11 | "react": "^18.2.0",
12 | "react-dom": "^18.2.0",
13 | "react-scripts": "5.0.1",
14 | "simplex-noise": "^4.0.1",
15 | "three": "^0.146.0"
16 | },
17 | "scripts": {
18 | "dev": "yarn start",
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject",
23 | "pretty": "prettier -w src/"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "prettier": "^2.7.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/MountainMaterial.jsx:
--------------------------------------------------------------------------------
1 | import { extend, useLoader } from "@react-three/fiber";
2 | import { shaderMaterial } from "@react-three/drei";
3 | import { TextureLoader } from "three";
4 |
5 | extend({
6 | SlopeBlendMaterial: shaderMaterial(
7 | {
8 | tFlat: undefined,
9 | tSlope: undefined,
10 | fNormal: { type: "v3", value: [], boundTo: "faces" },
11 | },
12 | `
13 | varying vec2 vUv;
14 | varying vec3 vNormal;
15 |
16 | void main() {
17 | vUv = vec2(uv.x, uv.y);
18 | vNormal = vec3(normal.x, normal.y, normal.z);
19 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
20 | }
21 | `,
22 | `
23 | uniform sampler2D tFlat;
24 | uniform sampler2D tSlope;
25 |
26 | varying vec2 vUv;
27 | varying vec3 vNormal;
28 |
29 | void main() {
30 | vec4 flatTex = texture2D(tFlat, vUv);
31 | vec4 slopeTex = texture2D(tSlope, vUv);
32 |
33 | gl_FragColor = mix(flatTex, slopeTex, smoothstep(0.0, 0.1, acos(dot(vec3(0.0, 1.0, 0.0), vNormal)) / 12.0));
34 | }
35 | `
36 | ),
37 | });
38 |
39 | const MountainMaterial = () => {
40 | const [flatTexture, slopeTexture] = useLoader(TextureLoader, [
41 | "/grass.jpg",
42 | "/rock.jpg",
43 | ]);
44 |
45 | return ;
46 | };
47 |
48 | export default MountainMaterial;
49 |
--------------------------------------------------------------------------------
/src/components/Terrain.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense, useMemo, useRef, useState } from "react";
2 | import { createNoise2D } from "simplex-noise";
3 | import { BufferAttribute } from "three";
4 | import { useControls } from "leva";
5 |
6 | import MountainMaterial from "./MountainMaterial";
7 | import WireframeMaterial from "./WireframeMaterial";
8 | import { DoubleSide } from "three";
9 | import { useEffect } from "react";
10 |
11 | const generateTerrain = (simplex, size, height, levels, scale, offset) => {
12 | const noise = (level, x, z) =>
13 | simplex(
14 | offset.x * scale + level * x * scale,
15 | offset.z * scale + level * z * scale
16 | ) /
17 | level +
18 | (level > 1 ? noise(level / 2, x, z) : 0);
19 | let lowest = 0;
20 | return [
21 | Float32Array.from({ length: size ** 2 * 3 }, (_, i) => {
22 | let v;
23 | switch (i % 3) {
24 | case 0:
25 | v = i / 3;
26 | return (offset.x + ((v % size) / size - 0.5)) * scale;
27 | case 1:
28 | v = (i - 1) / 3;
29 | const y =
30 | noise(
31 | 2 ** levels,
32 | (v % size) / size - 0.5,
33 | Math.floor(v / size) / size - 0.5
34 | ) * height;
35 | lowest = Math.min(lowest, y);
36 | return y;
37 | case 2:
38 | v = (i - 2) / 3;
39 | return (offset.z + Math.floor(v / size) / size - 0.5) * scale;
40 | default:
41 | console.error("Can't happen");
42 | return 0;
43 | }
44 | }),
45 | lowest - 0.1,
46 | ];
47 | };
48 |
49 | const Terrain = ({ seed, size, height, levels = 8, scale = 1, offset }) => {
50 | // eslint-disable-next-line react-hooks/exhaustive-deps
51 | const simplex = useMemo(() => new createNoise2D(), [seed]); // use seed to regenerate simplex noise
52 | const [lowestPoint, setLowestPoint] = useState(0);
53 | const ref = useRef();
54 | const northRef = useRef();
55 | const eastRef = useRef();
56 | const southRef = useRef();
57 | const westRef = useRef();
58 |
59 | const sides = useMemo(
60 | () => ({
61 | north: new Float32Array(size * 6),
62 | east: new Float32Array(size * 6),
63 | south: new Float32Array(size * 6),
64 | west: new Float32Array(size * 6),
65 | }),
66 | [size]
67 | );
68 |
69 | useEffect(() => {
70 | const [vertices, lowestPoint] = generateTerrain(
71 | simplex,
72 | size,
73 | height,
74 | levels,
75 | scale,
76 | offset
77 | );
78 | setLowestPoint(lowestPoint);
79 | for (let i = 0, j = 0, k = 0, l = 0; i < size ** 2; i++) {
80 | const [x, y, z] = [
81 | vertices[i * 3],
82 | vertices[i * 3 + 1],
83 | vertices[i * 3 + 2],
84 | ];
85 | if (i <= size) {
86 | sides.north[i * 6] = x;
87 | sides.north[i * 6 + 1] = y;
88 | sides.north[i * 6 + 2] = z;
89 | sides.north[i * 6 + 3] = x;
90 | sides.north[i * 6 + 4] = lowestPoint;
91 | sides.north[i * 6 + 5] = z;
92 | }
93 | if (i % size === 0) {
94 | sides.east[j * 6] = x;
95 | sides.east[j * 6 + 1] = y;
96 | sides.east[j * 6 + 2] = z;
97 | sides.east[j * 6 + 3] = x;
98 | sides.east[j * 6 + 4] = lowestPoint;
99 | sides.east[j * 6 + 5] = z;
100 | j++;
101 | }
102 | if (i % size === size - 1) {
103 | sides.south[k * 6] = x;
104 | sides.south[k * 6 + 1] = y;
105 | sides.south[k * 6 + 2] = z;
106 | sides.south[k * 6 + 3] = x;
107 | sides.south[k * 6 + 4] = lowestPoint;
108 | sides.south[k * 6 + 5] = z;
109 | k++;
110 | }
111 | if (i >= size ** 2 - size) {
112 | sides.west[l * 6] = x;
113 | sides.west[l * 6 + 1] = y;
114 | sides.west[l * 6 + 2] = z;
115 | sides.west[l * 6 + 3] = x;
116 | sides.west[l * 6 + 4] = lowestPoint;
117 | sides.west[l * 6 + 5] = z;
118 | l++;
119 | }
120 | }
121 | ref.current.setAttribute("position", new BufferAttribute(vertices, 3));
122 | ref.current.elementsNeedUpdate = true;
123 | ref.current.computeVertexNormals();
124 | northRef.current.setAttribute(
125 | "position",
126 | new BufferAttribute(sides.north, 3)
127 | );
128 | northRef.current.elementsNeedUpdate = true;
129 | eastRef.current.setAttribute(
130 | "position",
131 | new BufferAttribute(sides.east, 3)
132 | );
133 | eastRef.current.elementsNeedUpdate = true;
134 | southRef.current.setAttribute(
135 | "position",
136 | new BufferAttribute(sides.south, 3)
137 | );
138 | southRef.current.elementsNeedUpdate = true;
139 | westRef.current.setAttribute(
140 | "position",
141 | new BufferAttribute(sides.west, 3)
142 | );
143 | westRef.current.elementsNeedUpdate = true;
144 | }, [size, height, levels, scale, offset, simplex, sides]);
145 |
146 | const { wireframe } = useControls({ wireframe: false });
147 |
148 | return (
149 |
150 |
151 |
152 | }>
153 | {wireframe ? : }
154 |
155 |
156 |
157 |
158 |
163 |
164 |
165 |
166 |
171 |
172 |
173 |
174 |
179 |
180 |
181 |
182 |
187 |
188 |
193 |
194 |
195 |
196 |
197 | );
198 | };
199 |
200 | export default Terrain;
201 |
--------------------------------------------------------------------------------