├── .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 | [](https://github.com/Louis3797/r3f-world-with-character/network/)
4 | [](https://github.com/Louis3797/r3f-world-with-character/stargazers/)
5 | [](https://github.com/Louis3797/r3f-world-with-character/issues/)
6 | [](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 | You need to enable JavaScript to run this app.
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 |
--------------------------------------------------------------------------------