├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── screenshot01.png
├── src
├── index.html
├── index.ts
├── models
│ └── Soldier.glb
├── textures
│ └── grass
│ │ ├── Grass_005_AmbientOcclusion.jpg
│ │ ├── Grass_005_BaseColor.jpg
│ │ ├── Grass_005_Normal.jpg
│ │ └── Grass_005_Roughness.jpg
└── utils
│ ├── characterControls.ts
│ └── keydisplay.ts
├── tsconfig.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .cache/
3 | coverage/
4 | dist/*
5 | !dist/index.html
6 | node_modules/
7 | *.log
8 |
9 | # OS generated files
10 | .DS_Store
11 | .DS_Store?
12 | ._*
13 | .Spotlight-V100
14 | .Trashes
15 | ehthumbs.db
16 | Thumbs.db
17 |
18 | index.js
19 |
20 | *.module.wasm
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # threejs-rapier3d-character-terrain-movement
2 |
3 | `npm install` and `npm run start`
4 |
5 | Try the [Stackblitz](https://stackblitz.com/github/tamani-coding/threejs-rapier3d-character-terrain-movement)
6 |
7 | 
8 |
9 | ## sources
10 |
11 | Model from: https://threejs.org/examples/#webgl_animation_skinning_blending
12 | Textures from: https://3dtextures.me/
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "threejs-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "npm run build && npm run serve",
8 | "build": "webpack",
9 | "serve": "webpack serve"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "npm-run-all": "^4.1.5",
16 | "ts-loader": "^8.1.0",
17 | "typescript": "^4.2.3",
18 | "webpack": "5.71.0",
19 | "webpack-cli": "4.9.2",
20 | "webpack-dev-server": "4.7.4"
21 | },
22 | "dependencies": {
23 | "@types/dat.gui": "^0.7.7",
24 | "@types/three": "^0.139.0",
25 | "dat.gui": "^0.7.9",
26 | "three": "^0.139.2",
27 | "@dimforge/rapier3d": "0.8.0-alpha.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/screenshot01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/screenshot01.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | three.js example
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { CharacterControls, CONTROLLER_BODY_RADIUS } from './utils/characterControls';
2 | import { KeyDisplay } from './utils/keydisplay';
3 | import { RigidBody, World } from '@dimforge/rapier3d';
4 | import * as THREE from 'three';
5 | import { AmbientLight, BoxBufferGeometry, MeshPhongMaterial } from 'three';
6 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
7 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
8 |
9 | // SCENE
10 | const scene = new THREE.Scene();
11 | scene.background = new THREE.Color(0xa8def0);
12 |
13 | // CAMERA
14 | const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
15 | camera.position.y = 5;
16 | camera.position.z = 10;
17 | camera.position.x = -13;
18 |
19 | // RENDERER
20 | const renderer = new THREE.WebGLRenderer({ antialias: true });
21 | renderer.setSize(window.innerWidth, window.innerHeight);
22 | renderer.setPixelRatio(window.devicePixelRatio);
23 | renderer.shadowMap.enabled = true
24 |
25 | // ORBIT CAMERA CONTROLS
26 | const orbitControls = new OrbitControls(camera, renderer.domElement);
27 | orbitControls.enableDamping = true
28 | orbitControls.enablePan = true
29 | orbitControls.minDistance = 5
30 | orbitControls.maxDistance = 20
31 | orbitControls.maxPolarAngle = Math.PI / 2 - 0.05 // prevent camera below ground
32 | orbitControls.minPolarAngle = Math.PI / 4 // prevent top down view
33 | orbitControls.update();
34 |
35 | const dLight = new THREE.DirectionalLight('white', 0.6);
36 | dLight.position.x = 20;
37 | dLight.position.y = 30;
38 | dLight.castShadow = true;
39 | dLight.shadow.mapSize.width = 4096;
40 | dLight.shadow.mapSize.height = 4096;
41 | const d = 35;
42 | dLight.shadow.camera.left = - d;
43 | dLight.shadow.camera.right = d;
44 | dLight.shadow.camera.top = d;
45 | dLight.shadow.camera.bottom = - d;
46 | scene.add(dLight);
47 |
48 | const aLight = new THREE.AmbientLight('white', 0.4);
49 | scene.add(aLight);
50 |
51 | // ANIMATE
52 | document.body.appendChild(renderer.domElement);
53 |
54 | // RESIZE HANDLER
55 | function onWindowResize() {
56 | camera.aspect = window.innerWidth / window.innerHeight;
57 | camera.updateProjectionMatrix();
58 | renderer.setSize(window.innerWidth, window.innerHeight);
59 | }
60 | window.addEventListener('resize', onWindowResize);
61 |
62 | function loadTexture(path: string): THREE.Texture {
63 | const texture = new THREE.TextureLoader().load(path);
64 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
65 | texture.repeat.x = 10;
66 | texture.repeat.y = 10;
67 | return texture;
68 | }
69 |
70 |
71 | // MODEL WITH ANIMATIONS
72 | var characterControls: CharacterControls
73 |
74 | import('@dimforge/rapier3d').then(RAPIER => {
75 |
76 | function body(scene: THREE.Scene, world: World,
77 | bodyType: 'dynamic' | 'static' | 'kinematicPositionBased',
78 | colliderType: 'cube' | 'sphere' | 'cylinder' | 'cone', dimension: any,
79 | translation: { x: number, y: number, z: number },
80 | rotation: { x: number, y: number, z: number },
81 | color: string): { rigid: RigidBody, mesh: THREE.Mesh } {
82 |
83 | let bodyDesc
84 |
85 | if (bodyType === 'dynamic') {
86 | bodyDesc = RAPIER.RigidBodyDesc.dynamic();
87 | } else if (bodyType === 'kinematicPositionBased') {
88 | bodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
89 | } else if (bodyType === 'static') {
90 | bodyDesc = RAPIER.RigidBodyDesc.fixed();
91 | bodyDesc.setCanSleep(false);
92 | }
93 |
94 | if (translation) {
95 | bodyDesc.setTranslation(translation.x, translation.y, translation.z)
96 | }
97 | if(rotation) {
98 | const q = new THREE.Quaternion().setFromEuler(
99 | new THREE.Euler( rotation.x, rotation.y, rotation.z, 'XYZ' )
100 | )
101 | bodyDesc.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w })
102 | }
103 |
104 | let rigidBody = world.createRigidBody(bodyDesc);
105 |
106 | let collider;
107 | if (colliderType === 'cube') {
108 | collider = RAPIER.ColliderDesc.cuboid(dimension.hx, dimension.hy, dimension.hz);
109 | } else if (colliderType === 'sphere') {
110 | collider = RAPIER.ColliderDesc.ball(dimension.radius);
111 | } else if (colliderType === 'cylinder') {
112 | collider = RAPIER.ColliderDesc.cylinder(dimension.hh, dimension.radius);
113 | } else if (colliderType === 'cone') {
114 | collider = RAPIER.ColliderDesc.cone(dimension.hh, dimension.radius);
115 | // cone center of mass is at bottom
116 | collider.centerOfMass = {x:0, y:0, z:0}
117 | }
118 | world.createCollider(collider, rigidBody.handle);
119 |
120 | let bufferGeometry;
121 | if (colliderType === 'cube') {
122 | bufferGeometry = new BoxBufferGeometry(dimension.hx * 2, dimension.hy * 2, dimension.hz * 2);
123 | } else if (colliderType === 'sphere') {
124 | bufferGeometry = new THREE.SphereBufferGeometry(dimension.radius, 32, 32);
125 | } else if (colliderType === 'cylinder') {
126 | bufferGeometry = new THREE.CylinderBufferGeometry(dimension.radius,
127 | dimension.radius, dimension.hh * 2, 32, 32);
128 | } else if (colliderType === 'cone') {
129 | bufferGeometry = new THREE.ConeBufferGeometry(dimension.radius, dimension.hh * 2,
130 | 32, 32);
131 | }
132 |
133 | const threeMesh = new THREE.Mesh(bufferGeometry, new MeshPhongMaterial({ color: color }));
134 | threeMesh.castShadow = true;
135 | threeMesh.receiveShadow = true;
136 | scene.add(threeMesh);
137 |
138 | return { rigid: rigidBody, mesh: threeMesh };
139 | }
140 |
141 | function generateTerrain(nsubdivs: number, scale: { x: number, y: number, z: number }) {
142 | let heights: number[] = []
143 |
144 | // three plane
145 | const threeFloor = new THREE.Mesh(
146 | new THREE.PlaneBufferGeometry(scale.x, scale.z, nsubdivs, nsubdivs),
147 | new THREE.MeshStandardMaterial({
148 | map: loadTexture('/textures/grass/Grass_005_BaseColor.jpg'),
149 | normalMap: loadTexture('/textures/grass/Grass_005_Normal.jpg'),
150 | aoMap: loadTexture('/textures/grass/Grass_005_AmbientOcclusion.jpg'),
151 | roughnessMap: loadTexture('/textures/grass/Grass_005_Roughness.jpg'),
152 | roughness: 0.6
153 | }));
154 | threeFloor.rotateX(- Math.PI / 2);
155 | threeFloor.receiveShadow = true;
156 | threeFloor.castShadow = true;
157 | scene.add(threeFloor);
158 |
159 | // add height data to plane
160 | const vertices = threeFloor.geometry.attributes.position.array;
161 | const dx = scale.x / nsubdivs;
162 | const dy = scale.z / nsubdivs;
163 | // store height data in map column-row map
164 | const columsRows = new Map();
165 | for (let i = 0; i < vertices.length; i += 3) {
166 | // translate into colum / row indices
167 | let row = Math.floor(Math.abs((vertices as any)[i] + (scale.x / 2)) / dx);
168 | let column = Math.floor(Math.abs((vertices as any)[i + 1] - (scale.z / 2)) / dy);
169 | // generate height for this column & row
170 | const randomHeight = Math.random();
171 | (vertices as any)[i + 2] = scale.y * randomHeight;
172 | // store height
173 | if (!columsRows.get(column)) {
174 | columsRows.set(column, new Map());
175 | }
176 | columsRows.get(column).set(row, randomHeight);
177 | }
178 | threeFloor.geometry.computeVertexNormals();
179 |
180 | // store height data into column-major-order matrix array
181 | for (let i = 0; i <= nsubdivs; ++i) {
182 | for (let j = 0; j <= nsubdivs; ++j) {
183 | heights.push(columsRows.get(j).get(i));
184 | }
185 | }
186 |
187 | let groundBodyDesc = RAPIER.RigidBodyDesc.fixed();
188 | let groundBody = world.createRigidBody(groundBodyDesc);
189 | let groundCollider = RAPIER.ColliderDesc.heightfield(
190 | nsubdivs, nsubdivs, new Float32Array(heights), scale
191 | );
192 | world.createCollider(groundCollider, groundBody.handle);
193 | }
194 |
195 | // Use the RAPIER module here.
196 | let gravity = { x: 0.0, y: -9.81, z: 0.0 };
197 | let world = new RAPIER.World(gravity);
198 |
199 | // Bodys
200 | const bodys: { rigid: RigidBody, mesh: THREE.Mesh }[] = []
201 |
202 | // Create Ground.
203 | let nsubdivs = 20;
204 | let scale = new RAPIER.Vector3(70.0, 3.0, 70.0);
205 | generateTerrain(nsubdivs, scale);
206 |
207 | const staticB = body(scene, world, 'static', 'cube',
208 | { hx: 10, hy: 0.8, hz: 10 }, { x: scale.x / 2, y: 2.5, z: 0 },
209 | { x: 0, y: 0, z: 0.3 }, 'pink');
210 | bodys.push(staticB);
211 |
212 | const cubeBody = body(scene, world, 'dynamic', 'cube',
213 | { hx: 0.5, hy: 0.5, hz: 0.5 }, { x: 0, y: 15, z: 0 },
214 | { x: 0, y: 0.4, z: 0.7 }, 'orange');
215 | bodys.push(cubeBody);
216 |
217 | const sphereBody = body(scene, world, 'dynamic', 'sphere',
218 | { radius: 0.7 }, { x: 4, y: 15, z: 2 },
219 | { x: 0, y: 1, z: 0 }, 'blue');
220 | bodys.push(sphereBody);
221 |
222 | const sphereBody2 = body(scene, world, 'dynamic', 'sphere',
223 | { radius: 0.7 }, { x: 0, y: 15, z: 0 },
224 | { x: 0, y: 1, z: 0 }, 'red');
225 | bodys.push(sphereBody2);
226 |
227 | const cylinderBody = body(scene, world, 'dynamic', 'cylinder',
228 | { hh: 1.0, radius: 0.7 }, { x: -7, y: 15, z: 8 },
229 | { x: 0, y: 1, z: 0 }, 'green');
230 | bodys.push(cylinderBody);
231 |
232 | const coneBody = body(scene, world, 'dynamic', 'cone',
233 | { hh: 1.0, radius: 1 }, { x: 7, y: 15, z: -8 },
234 | { x: 0, y: 1, z: 0 }, 'purple');
235 | bodys.push(coneBody);
236 |
237 | // character controller
238 | new GLTFLoader().load('models/Soldier.glb', function (gltf) {
239 | const model = gltf.scene;
240 | model.traverse(function (object: any) {
241 | if (object.isMesh) object.castShadow = true;
242 | });
243 | scene.add(model);
244 |
245 | const gltfAnimations: THREE.AnimationClip[] = gltf.animations;
246 | const mixer = new THREE.AnimationMixer(model);
247 | const animationsMap: Map = new Map()
248 | gltfAnimations.filter(a => a.name != 'TPose').forEach((a: THREE.AnimationClip) => {
249 | animationsMap.set(a.name, mixer.clipAction(a))
250 | })
251 |
252 |
253 | // RIGID BODY
254 | let bodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation(-1, 3, 1)
255 | let rigidBody = world.createRigidBody(bodyDesc);
256 | let dynamicCollider = RAPIER.ColliderDesc.ball(CONTROLLER_BODY_RADIUS);
257 | world.createCollider(dynamicCollider, rigidBody.handle);
258 |
259 | characterControls = new CharacterControls(model, mixer,
260 | animationsMap, orbitControls,
261 | camera, 'Idle',
262 | new RAPIER.Ray(
263 | { x: 0, y: 0, z: 0 },
264 | { x: 0, y: -1, z: 0}
265 | ), rigidBody)
266 | });
267 |
268 | const clock = new THREE.Clock();
269 | // Game loop.
270 | let gameLoop = () => {
271 | let deltaTime = clock.getDelta();
272 |
273 | if (characterControls) {
274 | characterControls.update(world, deltaTime, keysPressed);
275 | }
276 |
277 | // Step the simulation forward.
278 | world.step();
279 | // update 3d world with physical world
280 | bodys.forEach(body => {
281 | let position = body.rigid.translation();
282 | let rotation = body.rigid.rotation();
283 |
284 | body.mesh.position.x = position.x
285 | body.mesh.position.y = position.y
286 | body.mesh.position.z = position.z
287 |
288 | body.mesh.setRotationFromQuaternion(
289 | new THREE.Quaternion(rotation.x,
290 | rotation.y,
291 | rotation.z,
292 | rotation.w));
293 | });
294 |
295 | orbitControls.update()
296 | renderer.render(scene, camera);
297 |
298 | setTimeout(gameLoop, 16);
299 | };
300 |
301 | gameLoop();
302 | })
303 |
304 |
305 | const keysPressed: any = {}
306 | const keyDisplayQueue = new KeyDisplay();
307 | document.addEventListener('keydown', (event) => {
308 | keyDisplayQueue.down(event.key)
309 | if (event.shiftKey && characterControls) {
310 | characterControls.switchRunToggle()
311 | }
312 | keysPressed[event.key.toLowerCase()] = true
313 | }, false);
314 | document.addEventListener('keyup', (event) => {
315 | keyDisplayQueue.up(event.key);
316 | keysPressed[event.key.toLowerCase()] = false
317 | }, false);
--------------------------------------------------------------------------------
/src/models/Soldier.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/src/models/Soldier.glb
--------------------------------------------------------------------------------
/src/textures/grass/Grass_005_AmbientOcclusion.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/src/textures/grass/Grass_005_AmbientOcclusion.jpg
--------------------------------------------------------------------------------
/src/textures/grass/Grass_005_BaseColor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/src/textures/grass/Grass_005_BaseColor.jpg
--------------------------------------------------------------------------------
/src/textures/grass/Grass_005_Normal.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/src/textures/grass/Grass_005_Normal.jpg
--------------------------------------------------------------------------------
/src/textures/grass/Grass_005_Roughness.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tamani-coding/threejs-rapier3d-character-terrain-movement/f5be8e84696660b930a1d7c7f6fa28d910dcb81e/src/textures/grass/Grass_005_Roughness.jpg
--------------------------------------------------------------------------------
/src/utils/characterControls.ts:
--------------------------------------------------------------------------------
1 | import { Ray, RigidBody, World } from '@dimforge/rapier3d';
2 | import * as THREE from 'three'
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
4 | import { A, D, DIRECTIONS, S, W } from './keydisplay'
5 |
6 | export const CONTROLLER_BODY_RADIUS = 0.28;
7 |
8 | export class CharacterControls {
9 |
10 | model: THREE.Group
11 | mixer: THREE.AnimationMixer
12 | animationsMap: Map = new Map() // Walk, Run, Idle
13 | orbitControl: OrbitControls
14 | camera: THREE.Camera
15 |
16 | // state
17 | toggleRun: boolean = true
18 | currentAction: string
19 |
20 | // temporary data
21 | walkDirection = new THREE.Vector3()
22 | rotateAngle = new THREE.Vector3(0, 1, 0)
23 | rotateQuarternion: THREE.Quaternion = new THREE.Quaternion()
24 | cameraTarget = new THREE.Vector3()
25 | storedFall = 0
26 |
27 | // constants
28 | fadeDuration: number = 0.2
29 | runVelocity = 5
30 | walkVelocity = 2
31 |
32 | ray: Ray
33 | rigidBody: RigidBody
34 | lerp = (x: number, y: number, a: number) => x * (1 - a) + y * a;
35 |
36 | constructor(model: THREE.Group,
37 | mixer: THREE.AnimationMixer, animationsMap: Map,
38 | orbitControl: OrbitControls, camera: THREE.Camera,
39 | currentAction: string,
40 | ray: Ray, rigidBody: RigidBody) {
41 | this.model = model
42 | this.mixer = mixer
43 | this.animationsMap = animationsMap
44 | this.currentAction = currentAction
45 | this.animationsMap.forEach((value, key) => {
46 | if (key == currentAction) {
47 | value.play()
48 | }
49 | })
50 |
51 | this.ray = ray
52 | this.rigidBody = rigidBody
53 |
54 | this.orbitControl = orbitControl
55 | this.camera = camera
56 | this.updateCameraTarget(new THREE.Vector3(0,1,5))
57 | }
58 |
59 | public switchRunToggle() {
60 | this.toggleRun = !this.toggleRun
61 | }
62 |
63 | public update(world: World, delta: number, keysPressed: any) {
64 | const directionPressed = DIRECTIONS.some(key => keysPressed[key] == true)
65 |
66 | var play = '';
67 | if (directionPressed && this.toggleRun) {
68 | play = 'Run'
69 | } else if (directionPressed) {
70 | play = 'Walk'
71 | } else {
72 | play = 'Idle'
73 | }
74 |
75 | if (this.currentAction != play) {
76 | const toPlay = this.animationsMap.get(play)
77 | const current = this.animationsMap.get(this.currentAction)
78 |
79 | current.fadeOut(this.fadeDuration)
80 | toPlay.reset().fadeIn(this.fadeDuration).play();
81 |
82 | this.currentAction = play
83 | }
84 |
85 | this.mixer.update(delta)
86 |
87 | this.walkDirection.x = this.walkDirection.y = this.walkDirection.z = 0
88 |
89 | let velocity = 0
90 | if (this.currentAction == 'Run' || this.currentAction == 'Walk') {
91 | // calculate towards camera direction
92 | var angleYCameraDirection = Math.atan2(
93 | (this.camera.position.x - this.model.position.x),
94 | (this.camera.position.z - this.model.position.z))
95 | // diagonal movement angle offset
96 | var directionOffset = this.directionOffset(keysPressed)
97 |
98 | // rotate model
99 | this.rotateQuarternion.setFromAxisAngle(this.rotateAngle, angleYCameraDirection + directionOffset)
100 | this.model.quaternion.rotateTowards(this.rotateQuarternion, 0.2)
101 |
102 | // calculate direction
103 | this.camera.getWorldDirection(this.walkDirection)
104 | this.walkDirection.y = 0
105 | this.walkDirection.normalize()
106 | this.walkDirection.applyAxisAngle(this.rotateAngle, directionOffset)
107 |
108 | // run/walk velocity
109 | velocity = this.currentAction == 'Run' ? this.runVelocity : this.walkVelocity
110 | }
111 |
112 | const translation = this.rigidBody.translation();
113 | if (translation.y < -1) {
114 | // don't fall below ground
115 | this.rigidBody.setNextKinematicTranslation( {
116 | x: 0,
117 | y: 10,
118 | z: 0
119 | });
120 | } else {
121 | const cameraPositionOffset = this.camera.position.sub(this.model.position);
122 | // update model and camera
123 | this.model.position.x = translation.x
124 | this.model.position.y = translation.y
125 | this.model.position.z = translation.z
126 | this.updateCameraTarget(cameraPositionOffset)
127 |
128 | this.walkDirection.y += this.lerp(this.storedFall, -9.81 * delta, 0.10)
129 | this.storedFall = this.walkDirection.y
130 | this.ray.origin.x = translation.x
131 | this.ray.origin.y = translation.y
132 | this.ray.origin.z = translation.z
133 | let hit = world.castRay(this.ray, 0.5, false, 0xfffffffff);
134 | if (hit) {
135 | const point = this.ray.pointAt(hit.toi);
136 | let diff = translation.y - ( point.y + CONTROLLER_BODY_RADIUS);
137 | if (diff < 0.0) {
138 | this.storedFall = 0
139 | this.walkDirection.y = this.lerp(0, Math.abs(diff), 0.5)
140 | }
141 | }
142 |
143 | this.walkDirection.x = this.walkDirection.x * velocity * delta
144 | this.walkDirection.z = this.walkDirection.z * velocity * delta
145 |
146 | this.rigidBody.setNextKinematicTranslation( {
147 | x: translation.x + this.walkDirection.x,
148 | y: translation.y + this.walkDirection.y,
149 | z: translation.z + this.walkDirection.z
150 | });
151 | }
152 | }
153 |
154 | private updateCameraTarget(offset: THREE.Vector3) {
155 | // move camera
156 | const rigidTranslation = this.rigidBody.translation();
157 | this.camera.position.x = rigidTranslation.x + offset.x
158 | this.camera.position.y = rigidTranslation.y + offset.y
159 | this.camera.position.z = rigidTranslation.z + offset.z
160 |
161 | // update camera target
162 | this.cameraTarget.x = rigidTranslation.x
163 | this.cameraTarget.y = rigidTranslation.y + 1
164 | this.cameraTarget.z = rigidTranslation.z
165 | this.orbitControl.target = this.cameraTarget
166 | }
167 |
168 | private directionOffset(keysPressed: any) {
169 | var directionOffset = 0 // w
170 |
171 | if (keysPressed[W]) {
172 | if (keysPressed[A]) {
173 | directionOffset = Math.PI / 4 // w+a
174 | } else if (keysPressed[D]) {
175 | directionOffset = - Math.PI / 4 // w+d
176 | }
177 | } else if (keysPressed[S]) {
178 | if (keysPressed[A]) {
179 | directionOffset = Math.PI / 4 + Math.PI / 2 // s+a
180 | } else if (keysPressed[D]) {
181 | directionOffset = -Math.PI / 4 - Math.PI / 2 // s+d
182 | } else {
183 | directionOffset = Math.PI // s
184 | }
185 | } else if (keysPressed[A]) {
186 | directionOffset = Math.PI / 2 // a
187 | } else if (keysPressed[D]) {
188 | directionOffset = - Math.PI / 2 // d
189 | }
190 |
191 | return directionOffset
192 | }
193 |
194 | }
--------------------------------------------------------------------------------
/src/utils/keydisplay.ts:
--------------------------------------------------------------------------------
1 | export const W = 'w'
2 | export const A = 'a'
3 | export const S = 's'
4 | export const D = 'd'
5 | export const SHIFT = 'shift'
6 | export const DIRECTIONS = [W, A, S, D]
7 |
8 | export class KeyDisplay {
9 |
10 | map: Map = new Map()
11 |
12 | constructor() {
13 | const w: HTMLDivElement = document.createElement("div")
14 | const a: HTMLDivElement = document.createElement("div")
15 | const s: HTMLDivElement = document.createElement("div")
16 | const d: HTMLDivElement = document.createElement("div")
17 | const shift: HTMLDivElement = document.createElement("div")
18 |
19 | this.map.set(W, w)
20 | this.map.set(A, a)
21 | this.map.set(S, s)
22 | this.map.set(D, d)
23 | this.map.set(SHIFT, shift)
24 |
25 | this.map.forEach( (v, k) => {
26 | v.style.color = 'blue'
27 | v.style.fontSize = '50px'
28 | v.style.fontWeight = '800'
29 | v.style.position = 'absolute'
30 | v.textContent = k
31 | })
32 |
33 | this.updatePosition()
34 |
35 | this.map.forEach( (v, _) => {
36 | document.body.append(v)
37 | })
38 | }
39 |
40 | public updatePosition() {
41 | this.map.get(W).style.top = `${window.innerHeight - 150}px`
42 | this.map.get(A).style.top = `${window.innerHeight - 100}px`
43 | this.map.get(S).style.top = `${window.innerHeight - 100}px`
44 | this.map.get(D).style.top = `${window.innerHeight - 100}px`
45 | this.map.get(SHIFT).style.top = `${window.innerHeight - 100}px`
46 |
47 | this.map.get(W).style.left = `${300}px`
48 | this.map.get(A).style.left = `${200}px`
49 | this.map.get(S).style.left = `${300}px`
50 | this.map.get(D).style.left = `${400}px`
51 | this.map.get(SHIFT).style.left = `${50}px`
52 | }
53 |
54 | public down (key: string) {
55 | if (this.map.get(key.toLowerCase())) {
56 | this.map.get(key.toLowerCase()).style.color = 'red'
57 | }
58 | }
59 |
60 | public up (key: string) {
61 | if (this.map.get(key.toLowerCase())) {
62 | this.map.get(key.toLowerCase()).style.color = 'blue'
63 | }
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "noImplicitAny": true,
6 | "module": "CommonJS",
7 | "target": "es5",
8 | "allowJs": true
9 | }
10 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const src = path.resolve(__dirname, 'src');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | entry: './src/index.ts',
8 | devtool: 'inline-source-map',
9 | module: {
10 | rules: [
11 | {
12 | test: /\.tsx?$/,
13 | use: 'ts-loader',
14 | exclude: /node_modules/
15 | }
16 | ]
17 | },
18 | resolve: {
19 | extensions: ['.tsx', '.ts', '.js']
20 | },
21 | output: {
22 | filename: 'index.js',
23 | path: src
24 | },
25 | devServer: {
26 | static: src,
27 | },
28 | experiments: {
29 | asyncWebAssembly: true,
30 | },
31 | }
--------------------------------------------------------------------------------