diff --git a/src/cameras/PerspectiveCamera.js b/src/cameras/PerspectiveCamera.js new file mode 100644 index 0000000..624c084 --- /dev/null +++ b/src/cameras/PerspectiveCamera.js @@ -0,0 +1,14 @@ +import * as THREE from 'three'; +import * as MathUtils from 'three/src/math/MathUtils.js'; + +export function getCamera(){ + /* Function to create and place the Camera object. is freecam is true, it will enable freecam controls for easier debug*/ + const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); + + camera.position.x = 0;// <-----> + camera.position.y = -4;// ^^ / vv + camera.position.z = 4;// ^-----v + camera.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), MathUtils.degToRad(60)); + + return camera; +} \ No newline at end of file diff --git a/src/cameras/freecam.js b/src/cameras/freecam.js deleted file mode 100644 index 983953c..0000000 --- a/src/cameras/freecam.js +++ /dev/null @@ -1,17 +0,0 @@ -//https://medium.com/geekculture/how-to-control-three-js-camera-like-a-pro-a8575a717a2 - -import * as THREE from 'three'; -import { FlyControls } from 'three/examples/jsm/controls/FlyControls'; - -let controls; -export function enableFreeCam(camera, renderer){ - controls = new FlyControls( camera, renderer.domElement ); - controls.movementSpeed = 5; - controls.rollSpeed = Math.PI / 48; - controls.autoForward = false; - controls.dragToLook = true; -} - -export function updateFreeCamKeys(){ - controls.update(0.01) -} \ No newline at end of file diff --git a/src/main.js b/src/main.js index c2513e0..a203e05 100644 --- a/src/main.js +++ b/src/main.js @@ -1,128 +1,46 @@ import * as THREE from 'three'; -import * as MathUtils from 'three/src/math/MathUtils.js'; -import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; -import {createGround} from './objects/ground.js'; -import {alignGround} from './utils/placement.js'; -import * as Freecam from './cameras/freecam.js'; +import { getCamera } from './cameras/PerspectiveCamera.js'; +import {addAssetsOnScene} from './utils/addAssetsOnScene.js'; +import * as PlayerMovement from './utils/playerMovement.js'; +import * as Animations from './utils/animateMesh.js'; let Clock = new THREE.Clock(); -console.log("OK") +/* --------------------- */ +/* | SCENE CREATION | */ +/* --------------------- */ const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0EB1D2); -const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); +const camera = getCamera(); const renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); -//Freecam -Freecam.enableFreeCam(camera, renderer); - -const ground = createGround(); -scene.add(ground); - -const loader = new GLTFLoader(); -let mixer, clip, action, playerModel; - -loader.load( './src/objects/models/player.gltf', function ( gltf ) { - playerModel = gltf; - gltf.scene.scale.set(0.2,0.2,0.2); - gltf.scene.rotateOnWorldAxis(new THREE.Vector3(1,0,0), MathUtils.degToRad(90)); - alignGround(ground, gltf.scene); - scene.add( gltf.scene ); - - mixer = new THREE.AnimationMixer( gltf.scene ); - clip = THREE.AnimationClip.findByName( gltf.animations, 'Walk' ); - action = mixer.clipAction( clip ); - -}, undefined, function ( error ) { - - console.error( error ); - -} ); - -scene.add(new THREE.AmbientLight(0x404040, 50)); - -camera.position.x = 0;// <-----> -camera.position.y = -4;// ^^ / vv -camera.position.z = 4;// ^-----v -camera.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), MathUtils.degToRad(60)); - -/* ----- -* -MOVEMENT ------------*/ - -const PLAYER_SPEED = 0.015; -const playerDirection = new THREE.Vector3(0, 0, 0); -const playerRotation = new THREE.Quaternion(); +//Here, we create/load every asset used, and add it in the scene +let assets = await addAssetsOnScene(scene); +/* --------------------- */ +/* | MOVEMENT | */ +/* --------------------- */ // Events to listen keyboard inputs -document.addEventListener('keydown', handleKeyDown); -document.addEventListener('keyup', handleKeyUp); +document.addEventListener('keydown', PlayerMovement.handleKeyDown); +document.addEventListener('keyup', PlayerMovement.handleKeyUp); -//Managing keys -function handleKeyDown(event) { - action.play(); - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - playerDirection.y = 1; // Up - break; - case 'ArrowDown': - case 'KeyS': - playerDirection.y = -1; // Back - break; - case 'ArrowLeft': - case 'KeyA': - playerDirection.x = -1; // Left - break; - case 'ArrowRight': - case 'KeyD': - playerDirection.x = 1; // Right - break; - } -} +/* --------------------- */ +/* | ANIMATIONS | */ +/* --------------------- */ +//We can add an animationMixer on objects, and define ready to play actions on objects +//https://threejs.org/docs/#api/en/animation/AnimationAction +Animations.addAnimationMixerOnMesh(assets.player, ["Walk"]); -function handleKeyUp(event) { - action.stop(); - switch (event.code) { - case 'ArrowUp': - case 'KeyW': - case 'ArrowDown': - case 'KeyS': - playerDirection.y = 0; // Arrêter le mouvement vertical - break; - case 'ArrowLeft': - case 'KeyA': - case 'ArrowRight': - case 'KeyD': - playerDirection.x = 0; // Arrêter le mouvement horizontal - break; - } -} - -function updatePlayerPosition() { - const delta = playerDirection.clone().multiplyScalar(PLAYER_SPEED); - playerModel.scene.position.add(delta); - camera.position.add(delta); -const ROTATION_SPEED = 0.05; // Vitesse de rotation - if (playerDirection.lengthSq() > 0) { - const angle = Math.atan2(playerDirection.x, -playerDirection.y); - const targetRotation = new THREE.Quaternion(); - targetRotation.setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle); - playerRotation.slerp(targetRotation, ROTATION_SPEED); - playerModel.scene.setRotationFromQuaternion(playerRotation); - playerModel.scene.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), MathUtils.degToRad(90)); - } -} function animate() { requestAnimationFrame( animate ); - if(mixer) mixer.update( Clock.getDelta()*1.4 ); //If mixer ready - updatePlayerPosition(); - //Freecam.updateFreeCamKeys()//Freecam - renderer.render( scene, camera ); + PlayerMovement.updatePlayerPositionAnimation(assets.player, camera);//Updating player position and rotation + Animations.updateAnimations(Clock.getDelta());//Playing animations + + renderer.render( scene, camera );//Calculating and redering new frame } animate(); diff --git a/src/utils/addAssetsOnScene.js b/src/utils/addAssetsOnScene.js new file mode 100644 index 0000000..3237b8b --- /dev/null +++ b/src/utils/addAssetsOnScene.js @@ -0,0 +1,37 @@ +import * as THREE from 'three'; +import * as MathUtils from 'three/src/math/MathUtils.js'; +import { createGround } from '../objects/ground.js'; +import {loadModel} from './import.js'; + + +function alignGround(ground, MovingObject){ + /*Function used to place an object on top of another object. In fact, this is used only to place correctly objects on the ground*/ + const boundingBoxGround = new THREE.Box3().setFromObject(ground); + const boundingBoxMovingObject = new THREE.Box3().setFromObject(MovingObject); + + const MovingObjectHeight = boundingBoxMovingObject.max.z - boundingBoxMovingObject.min.z; + MovingObject.position.y = MovingObjectHeight/2 + boundingBoxGround.max.z; +} + +export async function addAssetsOnScene(scene){ + /*This function takes the scene as arg, and is used to load everything needed on the terrain. + Player, trees, fences, and everything displayed on the screen is added by this function, at the right place + + Will return an object containing created objects*/ + + scene.add(new THREE.AmbientLight(0x404040, 50));//Light, required to see + + const ground = createGround(); + scene.add(ground); + + const player = await loadModel('src/objects/models/player.gltf');//Loading player + player.scene.scale.set(0.2,0.2,0.2); + alignGround(ground, player.scene); + player.scene.rotateOnWorldAxis(new THREE.Vector3(1,0,0), MathUtils.degToRad(90));//Must be standing up + scene.add( player.scene ); + + return { + ground: ground, + player: player, + }; +} \ No newline at end of file diff --git a/src/utils/animateMesh.js b/src/utils/animateMesh.js new file mode 100644 index 0000000..4f97b26 --- /dev/null +++ b/src/utils/animateMesh.js @@ -0,0 +1,28 @@ +import * as THREE from 'three'; + +const ANIMATION_SPEED_MULTIPLIER = 1.4; +let mixersToUpdateEachFrame = []; + +export function addAnimationMixerOnMesh(mesh, actions){ + //See https://threejs.org/docs/#manual/en/introduction/Animation-system + /*Function used to add an animationMixer on a mesh, so we can easily start or stop animations + Takes the animated mesh, and a list of actions names that should become animated */ + + const mixer = new THREE.AnimationMixer(mesh.scene);//We create a new mixer to manage every animations on this mesh + + mesh.animationMixer = mixer;//We add this mixer on the mesh + mesh.animationActions = {};//And create an object that will hold actions + + actions.forEach(element => { + const clip = THREE.AnimationClip.findByName( mesh.animations, element );//For each given action name, we get a clip + mesh.animationActions[element] = mixer.clipAction(clip);//and save it as an action + }); + + mixersToUpdateEachFrame.push(mixer); +} + +export function updateAnimations(deltaSeconds){ + mixersToUpdateEachFrame.forEach(element => { + element.update(deltaSeconds * ANIMATION_SPEED_MULTIPLIER); + }); +}; \ No newline at end of file diff --git a/src/utils/import.js b/src/utils/import.js new file mode 100644 index 0000000..2991eeb --- /dev/null +++ b/src/utils/import.js @@ -0,0 +1,18 @@ +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +const loader = new GLTFLoader(); + +export function loadModel(path) { + return new Promise((resolve, reject) => { + loader.load( + path, + (gltf) => { + resolve(gltf); + }, + undefined, + (error) => { + reject(error); + } + ); + }); +} diff --git a/src/utils/placement.js b/src/utils/placement.js deleted file mode 100644 index c0be3d2..0000000 --- a/src/utils/placement.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as THREE from 'three'; - -export function alignGround(ground, MovingObject){ - const boundingBoxGround = new THREE.Box3().setFromObject(ground); - const boundingBoxMovingObject = new THREE.Box3().setFromObject(MovingObject); - - const MovingObjectHeight = boundingBoxMovingObject.max.z - boundingBoxMovingObject.min.z; - MovingObject.position.y = MovingObjectHeight/2 + boundingBoxGround.max.z; -} \ No newline at end of file diff --git a/src/utils/playerMovement.js b/src/utils/playerMovement.js new file mode 100644 index 0000000..52f68ad --- /dev/null +++ b/src/utils/playerMovement.js @@ -0,0 +1,69 @@ +import * as THREE from 'three'; +import * as MathUtils from 'three/src/math/MathUtils.js'; + +const PLAYER_SPEED = 0.015; +const ROTATION_SPEED = 0.05; // Vitesse de rotation +const playerDirection = new THREE.Vector3(0, 0, 0); +const playerRotation = new THREE.Quaternion(); + +//Managing keys +export function handleKeyDown(event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + playerDirection.y = 1; // Up + break; + case 'ArrowDown': + case 'KeyS': + playerDirection.y = -1; // Back + break; + case 'ArrowLeft': + case 'KeyA': + playerDirection.x = -1; // Left + break; + case 'ArrowRight': + case 'KeyD': + playerDirection.x = 1; // Right + break; + } + } + + export function handleKeyUp(event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + case 'ArrowDown': + case 'KeyS': + playerDirection.y = 0; // Arrêter le mouvement vertical + break; + case 'ArrowLeft': + case 'KeyA': + case 'ArrowRight': + case 'KeyD': + playerDirection.x = 0; // Arrêter le mouvement horizontal + break; + } + } + + export function updatePlayerPositionAnimation(player, camera) { + /* Function called in the main loop + Used to calculate the position and orientation of the player + Also used to start and stop walking animation */ + const delta = playerDirection.clone().multiplyScalar(PLAYER_SPEED); + player.scene.position.add(delta); + camera.position.add(delta);//The camera must follow the player + + if (playerDirection.length() > 0) {//If a movement key is pressed ( or something in playerDirection ) + if(!player.animationActions.Walk.isRunning()) player.animationActions.Walk.play();//Playing walk animation + + const angle = Math.atan2(playerDirection.x, -playerDirection.y);//Get the angle between center (0;0) and the coordinates, which are directly provided by the pressed keys + const targetRotation = new THREE.Quaternion(); + + targetRotation.setFromAxisAngle(new THREE.Vector3(0, 1, 0), angle); + playerRotation.slerp(targetRotation, ROTATION_SPEED);//Slowly moving from playerRotation to targetRotation + player.scene.setRotationFromQuaternion(playerRotation); + player.scene.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), MathUtils.degToRad(90));//Used to force the player to be standing up instead of sleeping + }else{ + if(player.animationActions.Walk.isRunning()) player.animationActions.Walk.stop();//Not walking, we can stop this animation + } + } \ No newline at end of file