├── 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 | camera.lookAt(0, 0, 0)} 14 | camera={{ position: [1, 1, 1] }} 15 | > 16 | 17 | 18 | 19 | 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 | --------------------------------------------------------------------------------