├── .gitignore ├── LICENSE ├── README.md ├── assets └── screenshot.png ├── package.json ├── postcss.config.js ├── public ├── character │ ├── character.fbx │ ├── dance.fbx │ ├── idle.fbx │ ├── running.fbx │ └── walking.fbx ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── textures │ └── nature │ ├── BirchTree_3.fbx │ ├── BirchTree_4.fbx │ ├── BushBerries_1.fbx │ ├── CommonTree_3.fbx │ ├── CommonTree_5.fbx │ ├── Grass.fbx │ ├── Grass_2.fbx │ ├── Rock_1.fbx │ ├── Rock_5.fbx │ ├── Willow_2.fbx │ ├── Willow_5.fbx │ └── WoodLog_Moss.fbx ├── src ├── App.tsx ├── components │ ├── Character.tsx │ ├── Ground.tsx │ └── Nature.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts └── setupTests.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Louis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![GitHub forks](https://badgen.net/github/forks/Louis3797/r3f-world-with-character/)](https://github.com/Louis3797/r3f-world-with-character/network/) 4 | [![GitHub stars](https://badgen.net/github/stars/Louis3797/r3f-world-with-character/)](https://github.com/Louis3797/r3f-world-with-character/stargazers/) 5 | [![GitHub issues](https://badgen.net/github/issues/Louis3797/r3f-world-with-character/)](https://github.com/Louis3797/r3f-world-with-character/issues/) 6 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Louis3797/r3f-world-with-character//blob/main/LICENSE) 7 | 8 | 9 |
10 |
11 | 12 |

Three.js World with Character

13 | 14 |

15 | A platform with a character on it. 16 |
17 | View Demo 18 | · 19 | Report Bug 20 | · 21 | Request Feature 22 |

23 |
24 | 25 | ![screenshot][screenshot] 26 | 27 | 28 | 29 | ## Table of Content 30 | 49 | 50 | 51 | ## About The Project 52 | 53 | A 3D world with nature objects and a character that can move around using your keyboard. 54 | Let the character walk, run or dance. 55 | 56 | w: Forwards 57 |
58 | a: Left 59 |
60 | s: Backwards 61 |
62 | d: Right 63 |
64 | e: Dance 65 |
66 | shift + (w,a,s,d): Run 67 | 68 | ### Built With 69 | 70 | * [TypeScript](https://www.typescriptlang.org/) 71 | * [React.js](https://reactjs.org/) 72 | * [TailwindCss](https://tailwindcss.com/) 73 | * [Three.js](https://threejs.org/) 74 | * [React-Three-Fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) 75 | * [Three-React Drei](https://github.com/pmndrs/drei) 76 | 77 | ### Features 78 | 79 | - 3D 80 | - Nature Objects 81 | - Simplex Noise Floor 82 | - 3D Character 83 | - Animations: Idle, Walk, Run, Dance 84 | - Movement with w, a, s, d, shift 85 | - Dance with e 86 | - Third Person Camera 87 | 88 | 89 | ## Getting Started 90 | 91 | This project uses yarn as packet manager. 92 | 93 | ### Installation 94 | 95 | 1. Clone the repo 96 | 97 | ```sh 98 | git clone https://github.com/Louis3797/r3f-world-with-character.git 99 | ``` 100 | 101 | 2. Install packages 102 | 103 | ```sh 104 | cd r3f-world-with-character 105 | yarn install 106 | ``` 107 | 108 | 3. Run on dev 109 | 110 | ```sh 111 | yarn start 112 | ``` 113 | 114 | See the [open issues](https://github.com/Louis3797/r3f-world-with-character/issues) for a full list of proposed features (and known issues). 115 | 116 | 117 | ## Usage 118 | 119 | 120 | 121 | ## Roadmap 122 | 123 | * [ ] Add collisions 124 | * [ ] Check height of Floor and move Character up if its higher than y: 1 125 | 126 | 127 | ## License 128 | 129 | Distributed under the MIT License. See `LICENSE.txt` for more information. 130 | 131 | 132 | ## Contact 133 | 134 | Louis 135 | 136 | Project Link: [r3f-world-with-character](https://github.com/Louis3797/r3f-world-with-character/) 137 | 138 | 139 | 140 | 141 | [screenshot]: assets/screenshot.png 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/assets/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-world", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-three/drei": "^8.20.2", 7 | "@react-three/fiber": "^8.0.0-beta.9", 8 | "@testing-library/jest-dom": "^5.14.1", 9 | "@testing-library/react": "^12.0.0", 10 | "@testing-library/user-event": "^13.2.1", 11 | "@types/jest": "^27.0.1", 12 | "@types/node": "^16.7.13", 13 | "@types/react": "^17.0.20", 14 | "@types/react-dom": "^17.0.9", 15 | "@types/three": "^0.139.0", 16 | "react": "^18.0.0", 17 | "react-dom": "^18.0.0", 18 | "react-scripts": "5.0.0", 19 | "simplex-noise": "^3.0.1", 20 | "three": "^0.139.0", 21 | "typescript": "^4.4.2", 22 | "web-vitals": "^2.1.0" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "autoprefixer": "^10.4.4", 50 | "postcss": "^8.4.12", 51 | "tailwindcss": "^3.0.23" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/character/character.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/character/character.fbx -------------------------------------------------------------------------------- /public/character/dance.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/character/dance.fbx -------------------------------------------------------------------------------- /public/character/idle.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/character/idle.fbx -------------------------------------------------------------------------------- /public/character/running.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/character/running.fbx -------------------------------------------------------------------------------- /public/character/walking.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/character/walking.fbx -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/textures/nature/BirchTree_3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/BirchTree_3.fbx -------------------------------------------------------------------------------- /public/textures/nature/BirchTree_4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/BirchTree_4.fbx -------------------------------------------------------------------------------- /public/textures/nature/BushBerries_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/BushBerries_1.fbx -------------------------------------------------------------------------------- /public/textures/nature/CommonTree_3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/CommonTree_3.fbx -------------------------------------------------------------------------------- /public/textures/nature/CommonTree_5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/CommonTree_5.fbx -------------------------------------------------------------------------------- /public/textures/nature/Grass.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Grass.fbx -------------------------------------------------------------------------------- /public/textures/nature/Grass_2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Grass_2.fbx -------------------------------------------------------------------------------- /public/textures/nature/Rock_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Rock_1.fbx -------------------------------------------------------------------------------- /public/textures/nature/Rock_5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Rock_5.fbx -------------------------------------------------------------------------------- /public/textures/nature/Willow_2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Willow_2.fbx -------------------------------------------------------------------------------- /public/textures/nature/Willow_5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/Willow_5.fbx -------------------------------------------------------------------------------- /public/textures/nature/WoodLog_Moss.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Louis3797/r3f-world-with-character/55b2ea988135bf44ff602a2a45259df8616901ab/public/textures/nature/WoodLog_Moss.fbx -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Loader, OrbitControls, softShadows } from "@react-three/drei"; 3 | import { Canvas } from "@react-three/fiber"; 4 | import Ground from "./components/Ground"; 5 | import Character from "./components/Character"; 6 | import * as THREE from "three"; 7 | import Nature from "./components/Nature"; 8 | 9 | softShadows(); 10 | function App() { 11 | const hemiLight = new THREE.HemisphereLight(0xffffff, 0xfffffff, 0.6); 12 | hemiLight.color.setHSL(0.6, 1, 0.6); 13 | hemiLight.groundColor.setHSL(0.095, 1, 0.75); 14 | 15 | const fov = 60; 16 | const aspect = 1920 / 1080; 17 | const near = 1.0; 18 | const far = 1000.0; 19 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 20 | camera.position.set(25, 10, 25); 21 | 22 | const light = new THREE.DirectionalLight(0xffffff, 1.0); 23 | light.position.set(-100, 100, 100); 24 | light.target.position.set(0, 0, 0); 25 | light.castShadow = true; 26 | light.shadow.bias = -0.001; 27 | light.shadow.mapSize.width = 4096; 28 | light.shadow.mapSize.height = 4096; 29 | light.shadow.camera.near = 0.1; 30 | light.shadow.camera.far = 500.0; 31 | light.shadow.camera.near = 0.5; 32 | light.shadow.camera.far = 500.0; 33 | light.shadow.camera.left = 50; 34 | light.shadow.camera.right = -50; 35 | light.shadow.camera.top = 50; 36 | light.shadow.camera.bottom = -50; 37 | 38 | return ( 39 |
40 | 41 | / 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | `Loading ${p.toFixed(2)}%`} 55 | initialState={(active) => active} 56 | /> 57 |
58 | ); 59 | } 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /src/components/Character.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, useLoader } from "@react-three/fiber"; 2 | import React, { useCallback, useEffect, useRef } from "react"; 3 | import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; 4 | import { useFBX } from "@react-three/drei"; 5 | import * as THREE from "three"; 6 | 7 | import { Mesh } from "three"; 8 | 9 | interface Animations { 10 | [name: string]: { 11 | clip: THREE.AnimationAction; 12 | }; 13 | } 14 | 15 | interface CharacterProps { 16 | camera: THREE.PerspectiveCamera; 17 | } 18 | const Character: React.FC = ({ camera }) => { 19 | const character = useRef(null!); 20 | 21 | const activeAnimation: { 22 | forward: boolean; 23 | backward: boolean; 24 | left: boolean; 25 | right: boolean; 26 | run: boolean; 27 | dance: boolean; 28 | } = { 29 | forward: false, 30 | backward: false, 31 | left: false, 32 | right: false, 33 | run: false, 34 | dance: false, 35 | }; 36 | 37 | const animations: Animations = {}; 38 | 39 | const currentPosition = new THREE.Vector3(); 40 | const currentLookAt = new THREE.Vector3(); 41 | const decceleration = new THREE.Vector3(-0.0005, -0.0001, -5.0); 42 | const acceleration = new THREE.Vector3(1, 0.125, 100.0); 43 | const velocity = new THREE.Vector3(0, 0, 0); 44 | 45 | const c = useLoader(FBXLoader, "./character/character.fbx"); 46 | 47 | c.scale.setScalar(0.1); 48 | c.traverse((f) => { 49 | f.castShadow = true; 50 | f.receiveShadow = true; 51 | }); 52 | 53 | const mixer = new THREE.AnimationMixer(c); 54 | 55 | const idle = useFBX("./character/idle.fbx"); 56 | 57 | animations["idle"] = { 58 | clip: mixer.clipAction(idle.animations[0]), 59 | }; 60 | 61 | const walk = useFBX("./character/walking.fbx"); 62 | 63 | animations["walk"] = { 64 | clip: mixer.clipAction(walk.animations[0]), 65 | }; 66 | 67 | const run = useFBX("./character/running.fbx"); 68 | 69 | animations["run"] = { 70 | clip: mixer.clipAction(run.animations[0]), 71 | }; 72 | 73 | const dance = useFBX("./character/dance.fbx"); 74 | 75 | animations["dance"] = { 76 | clip: mixer.clipAction(dance.animations[0]), 77 | }; 78 | 79 | // set current Action 80 | let currAction = animations["idle"].clip; 81 | 82 | let prevAction: THREE.AnimationAction; 83 | 84 | // Controll Input 85 | const handleKeyPress = useCallback((event) => { 86 | switch (event.keyCode) { 87 | case 87: //w 88 | activeAnimation.forward = true; 89 | 90 | break; 91 | 92 | case 65: //a 93 | activeAnimation.left = true; 94 | 95 | break; 96 | 97 | case 83: //s 98 | activeAnimation.backward = true; 99 | 100 | break; 101 | 102 | case 68: // d 103 | activeAnimation.right = true; 104 | 105 | break; 106 | 107 | case 69: //e dance 108 | activeAnimation.dance = true; 109 | 110 | break; 111 | case 16: // shift 112 | activeAnimation.run = true; 113 | break; 114 | } 115 | }, []); 116 | 117 | const handleKeyUp = useCallback((event) => { 118 | switch (event.keyCode) { 119 | case 87: //w 120 | activeAnimation.forward = false; 121 | break; 122 | 123 | case 65: //a 124 | activeAnimation.left = false; 125 | break; 126 | 127 | case 83: //s 128 | activeAnimation.backward = false; 129 | break; 130 | 131 | case 68: // d 132 | activeAnimation.right = false; 133 | break; 134 | 135 | case 69: //e dance 136 | activeAnimation.dance = false; 137 | break; 138 | 139 | case 16: // shift 140 | activeAnimation.run = false; 141 | break; 142 | } 143 | }, []); 144 | 145 | const calculateIdealOffset = () => { 146 | const idealOffset = new THREE.Vector3(0, 20, -30); 147 | idealOffset.applyQuaternion(character.current.quaternion); 148 | idealOffset.add(character.current.position); 149 | return idealOffset; 150 | }; 151 | 152 | const calculateIdealLookat = () => { 153 | const idealLookat = new THREE.Vector3(0, 10, 50); 154 | idealLookat.applyQuaternion(character.current.quaternion); 155 | idealLookat.add(character.current.position); 156 | return idealLookat; 157 | }; 158 | 159 | function updateCameraTarget(delta: number) { 160 | const idealOffset = calculateIdealOffset(); 161 | const idealLookat = calculateIdealLookat(); 162 | 163 | const t = 1.0 - Math.pow(0.001, delta); 164 | 165 | currentPosition.lerp(idealOffset, t); 166 | currentLookAt.lerp(idealLookat, t); 167 | 168 | camera.position.copy(currentPosition); 169 | } 170 | 171 | // movement 172 | const characterState = (delta: number) => { 173 | const newVelocity = velocity; 174 | const frameDecceleration = new THREE.Vector3( 175 | newVelocity.x * decceleration.x, 176 | newVelocity.y * decceleration.y, 177 | newVelocity.z * decceleration.z 178 | ); 179 | frameDecceleration.multiplyScalar(delta); 180 | frameDecceleration.z = 181 | Math.sign(frameDecceleration.z) * 182 | Math.min(Math.abs(frameDecceleration.z), Math.abs(newVelocity.z)); 183 | 184 | newVelocity.add(frameDecceleration); 185 | 186 | const controlObject = character.current; 187 | const _Q = new THREE.Quaternion(); 188 | const _A = new THREE.Vector3(); 189 | const _R = controlObject.quaternion.clone(); 190 | 191 | const acc = acceleration.clone(); 192 | if (activeAnimation.run) { 193 | acc.multiplyScalar(2.0); 194 | } 195 | 196 | if (currAction === animations["dance"].clip) { 197 | acc.multiplyScalar(0.0); 198 | } 199 | 200 | if (activeAnimation.forward) { 201 | newVelocity.z += acc.z * delta; 202 | } 203 | if (activeAnimation.backward) { 204 | newVelocity.z -= acc.z * delta; 205 | } 206 | if (activeAnimation.left) { 207 | _A.set(0, 1, 0); 208 | _Q.setFromAxisAngle(_A, 4.0 * Math.PI * delta * acceleration.y); 209 | _R.multiply(_Q); 210 | } 211 | if (activeAnimation.right) { 212 | _A.set(0, 1, 0); 213 | _Q.setFromAxisAngle(_A, 4.0 * -Math.PI * delta * acceleration.y); 214 | _R.multiply(_Q); 215 | } 216 | 217 | controlObject.quaternion.copy(_R); 218 | 219 | const oldPosition = new THREE.Vector3(); 220 | oldPosition.copy(controlObject.position); 221 | 222 | const forward = new THREE.Vector3(0, 0, 1); 223 | forward.applyQuaternion(controlObject.quaternion); 224 | forward.normalize(); 225 | 226 | const sideways = new THREE.Vector3(1, 0, 0); 227 | sideways.applyQuaternion(controlObject.quaternion); 228 | sideways.normalize(); 229 | 230 | sideways.multiplyScalar(newVelocity.x * delta); 231 | forward.multiplyScalar(newVelocity.z * delta); 232 | 233 | controlObject.position.add(forward); 234 | controlObject.position.add(sideways); 235 | 236 | character.current.position.copy(controlObject.position); 237 | updateCameraTarget(delta); 238 | }; 239 | 240 | useFrame((state, delta) => { 241 | prevAction = currAction; 242 | 243 | if (activeAnimation.forward) { 244 | if (activeAnimation.run) { 245 | currAction = animations["run"].clip; 246 | } else { 247 | currAction = animations["walk"].clip; 248 | } 249 | } else if (activeAnimation.left) { 250 | if (activeAnimation.run) { 251 | currAction = animations["run"].clip; 252 | } else { 253 | currAction = animations["walk"].clip; 254 | } 255 | } else if (activeAnimation.right) { 256 | if (activeAnimation.run) { 257 | currAction = animations["run"].clip; 258 | } else { 259 | currAction = animations["walk"].clip; 260 | } 261 | } else if (activeAnimation.backward) { 262 | if (activeAnimation.run) { 263 | currAction = animations["run"].clip; 264 | } else { 265 | currAction = animations["walk"].clip; 266 | } 267 | } else if (activeAnimation.dance) { 268 | currAction = animations["dance"].clip; 269 | } else { 270 | currAction = animations["idle"].clip; 271 | } 272 | 273 | if (prevAction !== currAction) { 274 | prevAction.fadeOut(0.2); 275 | 276 | if (prevAction === animations["walk"].clip) { 277 | const ratio = 278 | currAction.getClip().duration / prevAction.getClip().duration; 279 | currAction.time = prevAction.time * ratio; 280 | } 281 | 282 | currAction.reset().play(); 283 | } else { 284 | currAction.play(); 285 | } 286 | 287 | characterState(delta); 288 | const idealLookat = calculateIdealLookat(); 289 | 290 | state.camera.lookAt(idealLookat); 291 | state.camera.updateProjectionMatrix(); 292 | mixer?.update(delta); 293 | }); 294 | 295 | useEffect(() => { 296 | document.addEventListener("keydown", handleKeyPress); 297 | 298 | document.addEventListener("keyup", handleKeyUp); 299 | currAction.play(); 300 | return () => { 301 | document.removeEventListener("keydown", handleKeyPress); 302 | 303 | document.removeEventListener("keyup", handleKeyUp); 304 | }; 305 | }); 306 | 307 | return ; 308 | }; 309 | 310 | export default Character; 311 | -------------------------------------------------------------------------------- /src/components/Ground.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useMemo, useRef } from "react"; 2 | import SimplexNoise from "simplex-noise"; 3 | import * as THREE from "three"; 4 | 5 | const Ground: React.FC = () => { 6 | const simplex = useMemo(() => new SimplexNoise(), []); 7 | 8 | const terrain = useRef(null!); 9 | 10 | useLayoutEffect(() => { 11 | let pos = terrain.current.getAttribute("position"); 12 | let pa = pos.array; 13 | 14 | const hVerts = terrain.current.parameters.heightSegments + 1; 15 | const wVerts = terrain.current.parameters.widthSegments + 1; 16 | 17 | for (let j = 0; j < hVerts; j++) { 18 | for (let i = 0; i < wVerts; i++) { 19 | const ex = Math.random() * 1.3; 20 | // @ts-ignore 21 | pa[3 * (j * wVerts + i) + 2] = 22 | (simplex.noise2D(i / 100, j / 100) + 23 | simplex.noise2D((i + 200) / 50, j / 50) * Math.pow(ex, 1) + 24 | simplex.noise2D((i + 400) / 25, j / 25) * Math.pow(ex, 2) + 25 | simplex.noise2D((i + 600) / 12.5, j / 12.5) * Math.pow(ex, 3) + 26 | +(simplex.noise2D((i + 800) / 6.25, j / 6.25) * Math.pow(ex, 4))) / 27 | 2; 28 | } 29 | } 30 | 31 | pos.needsUpdate = true; 32 | 33 | terrain.current.computeVertexNormals(); 34 | }); 35 | 36 | return ( 37 | 38 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Ground; 50 | -------------------------------------------------------------------------------- /src/components/Nature.tsx: -------------------------------------------------------------------------------- 1 | import { useLoader } from "@react-three/fiber"; 2 | import React, { useMemo } from "react"; 3 | import * as THREE from "three"; 4 | import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader"; 5 | 6 | const Nature: React.FC = () => { 7 | const [ 8 | birch3, 9 | birch4, 10 | berry1, 11 | ctree3, 12 | ctree5, 13 | grass2, 14 | grass, 15 | rock1, 16 | rock5, 17 | willow2, 18 | willow5, 19 | log, 20 | ] = useLoader(FBXLoader, [ 21 | "./textures/nature/BirchTree_3.fbx", 22 | "./textures/nature/BirchTree_4.fbx", 23 | "./textures/nature/BushBerries_1.fbx", 24 | "./textures/nature/CommonTree_3.fbx", 25 | "./textures/nature/CommonTree_5.fbx", 26 | "./textures/nature/Grass_2.fbx", 27 | "./textures/nature/Grass.fbx", 28 | "./textures/nature/Rock_1.fbx", 29 | "./textures/nature/Rock_5.fbx", 30 | "./textures/nature/Willow_2.fbx", 31 | "./textures/nature/Willow_5.fbx", 32 | "./textures/nature/WoodLog_Moss.fbx", 33 | ]); 34 | 35 | birch3.scale.setScalar(0.4); 36 | birch3.traverse((o) => { 37 | o.castShadow = true; 38 | o.receiveShadow = true; 39 | }); 40 | birch4.scale.setScalar(0.3); 41 | birch4.traverse((o) => { 42 | o.castShadow = true; 43 | o.receiveShadow = true; 44 | }); 45 | berry1.scale.setScalar(0.08); 46 | berry1.traverse((o) => { 47 | o.castShadow = true; 48 | o.receiveShadow = true; 49 | }); 50 | grass2.scale.setScalar(0.05); 51 | grass2.traverse((o) => { 52 | o.castShadow = true; 53 | o.receiveShadow = true; 54 | }); 55 | grass.scale.setScalar(0.05); 56 | grass.traverse((o) => { 57 | o.castShadow = true; 58 | o.receiveShadow = true; 59 | }); 60 | rock1.scale.setScalar(0.2); 61 | rock1.traverse((o) => { 62 | o.castShadow = true; 63 | o.receiveShadow = true; 64 | }); 65 | rock5.scale.setScalar(0.2); 66 | rock5.traverse((o) => { 67 | o.castShadow = true; 68 | o.receiveShadow = true; 69 | }); 70 | willow2.scale.setScalar(0.4); 71 | willow2.traverse((o) => { 72 | o.castShadow = true; 73 | o.receiveShadow = true; 74 | }); 75 | willow5.scale.setScalar(0.5); 76 | willow5.traverse((o) => { 77 | o.castShadow = true; 78 | o.receiveShadow = true; 79 | }); 80 | log.scale.setScalar(0.1); 81 | log.traverse((o) => { 82 | o.castShadow = true; 83 | o.receiveShadow = true; 84 | }); 85 | ctree3.scale.setScalar(0.4); 86 | ctree3.traverse((o) => { 87 | o.castShadow = true; 88 | o.receiveShadow = true; 89 | }); 90 | ctree5.scale.setScalar(0.4); 91 | ctree5.traverse((o) => { 92 | o.castShadow = true; 93 | o.receiveShadow = true; 94 | }); 95 | 96 | const objects: JSX.Element[] = []; 97 | 98 | const createTrees = useMemo(() => { 99 | for (let i = 0; i < 100; i++) { 100 | const idx: number = Math.floor(Math.random() * 11) + 1; 101 | const pos = new THREE.Vector3( 102 | Math.ceil(Math.random() * 450) * (Math.round(Math.random()) ? 1 : -1), 103 | 0, 104 | Math.ceil(Math.random() * 450) * (Math.round(Math.random()) ? 1 : -1) 105 | ); 106 | 107 | const obj = ( 108 | 137 | ); 138 | 139 | objects.push(obj); 140 | } 141 | }, []); 142 | 143 | return ( 144 | 145 | {objects.map((obj: JSX.Element) => { 146 | return obj; 147 | })} 148 | 149 | ); 150 | }; 151 | 152 | export default Nature; 153 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | body { 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 7 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------