Skip to content

nadiamariduena/firstpersoncontrol-threejs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

First Person Control and PointerLockControls 👾👾👾



DIFFERENCES


  • FirstPersonControls is actually an alternative implementation of FlyControls. Meaning you can use WASD, Arrow Keys and Mouse to freely transform the camera in 3D space.

  • PointerLockControls is different since it uses the Pointer Lock API. That means the mouse is captured (the cursor is hidden during the capturing) and only mouse movements (so the actual delta values) transform the camera when looking around. This is the behavior you usually know from First-Person games.

Probably a third person control, using some collision


Simple Third Person Camera (using Three.js/JavaScript)

Physics | Collision



DEBUT 🐙

First Person Control

Start by importing the code from the node modules, just like in the past tests:
import { FirstPersonControls } from "three/examples/jsm/controls/FirstPersonControls";
preview of what you are importing (inside the link)
  • ALSO... to understand a bit this code, check out the beginners app we created in class

Beginners commands videgame | DCI fbw_26


import * as THREE from "three";

THREE.FirstPersonControls = function (object, domElement) {
  if (domElement === undefined) {
    console.warn(
      'THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.'
    );
    domElement = document;
  }

  this.object = object;
  this.domElement = domElement;

  // API

  this.enabled = true;

  this.movementSpeed = 1.0;
  this.lookSpeed = 0.005;

  this.lookVertical = true;
  this.autoForward = false;

  this.activeLook = true;

  this.heightSpeed = false;
  this.heightCoef = 1.0;
  this.heightMin = 0.0;
  this.heightMax = 1.0;

  this.constrainVertical = false;
  this.verticalMin = 0;
  this.verticalMax = Math.PI;

  this.mouseDragOn = false;

  // internals

  this.autoSpeedFactor = 0.0;

  this.mouseX = 0;
  this.mouseY = 0;

  this.moveForward = false;
  this.moveBackward = false;
  this.moveLeft = false;
  this.moveRight = false;

  this.viewHalfX = 0;
  this.viewHalfY = 0;

  // private variables

  var lat = 0;
  var lon = 0;

  var lookDirection = new THREE.Vector3();
  var spherical = new THREE.Spherical();
  var target = new THREE.Vector3();

  //

  if (this.domElement !== document) {
    this.domElement.setAttribute("tabindex", -1);
  }

  //

  this.handleResize = function () {
    if (this.domElement === document) {
      this.viewHalfX = window.innerWidth / 2;
      this.viewHalfY = window.innerHeight / 2;
    } else {
      this.viewHalfX = this.domElement.offsetWidth / 2;
      this.viewHalfY = this.domElement.offsetHeight / 2;
    }
  };

  this.onMouseDown = function (event) {
    if (this.domElement !== document) {
      this.domElement.focus();
    }

    event.preventDefault();
    event.stopPropagation();

    if (this.activeLook) {
      switch (event.button) {
        case 0:
          this.moveForward = true;
          break;
        case 2:
          this.moveBackward = true;
          break;
      }
    }

    this.mouseDragOn = true;
  };

  this.onMouseUp = function (event) {
    event.preventDefault();
    event.stopPropagation();

    if (this.activeLook) {
      switch (event.button) {
        case 0:
          this.moveForward = false;
          break;
        case 2:
          this.moveBackward = false;
          break;
      }
    }

    this.mouseDragOn = false;
  };

  this.onMouseMove = function (event) {
    if (this.domElement === document) {
      this.mouseX = event.pageX - this.viewHalfX;
      this.mouseY = event.pageY - this.viewHalfY;
    } else {
      this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
      this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY;
    }
  };
  //
  //----------------------------
  //      commands
  //----------------------------
  //
  //
  this.onKeyDown = function (event) {
    //event.preventDefault();

    switch (event.code) {
      case "ArrowUp":
      case "KeyW":
        this.moveForward = true;
        break;

      case "ArrowLeft":
      case "KeyA":
        this.moveLeft = true;
        break;

      case "ArrowDown":
      case "KeyS":
        this.moveBackward = true;
        break;

      case "ArrowRight":
      case "KeyD":
        this.moveRight = true;
        break;

      case "KeyR":
        this.moveUp = true;
        break;
      case "KeyF":
        this.moveDown = true;
        break;
    }
  };
  //
  //
  //
  //

  this.onKeyUp = function (event) {
    switch (event.code) {
      case "ArrowUp":
      case "KeyW":
        this.moveForward = false;
        break;

      case "ArrowLeft":
      case "KeyA":
        this.moveLeft = false;
        break;

      case "ArrowDown":
      case "KeyS":
        this.moveBackward = false;
        break;

      case "ArrowRight":
      case "KeyD":
        this.moveRight = false;
        break;

      case "KeyR":
        this.moveUp = false;
        break;
      case "KeyF":
        this.moveDown = false;
        break;
    }
  };

  this.lookAt = function (x, y, z) {
    if (x.isVector3) {
      target.copy(x);
    } else {
      target.set(x, y, z);
    }

    this.object.lookAt(target);

    setOrientation(this);

    return this;
  };

  this.update = (function () {
    var targetPosition = new THREE.Vector3();

    return function update(delta) {
      if (this.enabled === false) return;

      if (this.heightSpeed) {
        var y = THREE.MathUtils.clamp(
          this.object.position.y,
          this.heightMin,
          this.heightMax
        );
        var heightDelta = y - this.heightMin;

        this.autoSpeedFactor = delta * (heightDelta * this.heightCoef);
      } else {
        this.autoSpeedFactor = 0.0;
      }

      var actualMoveSpeed = delta * this.movementSpeed;

      if (this.moveForward || (this.autoForward && !this.moveBackward))
        this.object.translateZ(-(actualMoveSpeed + this.autoSpeedFactor));
      if (this.moveBackward) this.object.translateZ(actualMoveSpeed);

      if (this.moveLeft) this.object.translateX(-actualMoveSpeed);
      if (this.moveRight) this.object.translateX(actualMoveSpeed);

      if (this.moveUp) this.object.translateY(actualMoveSpeed);
      if (this.moveDown) this.object.translateY(-actualMoveSpeed);

      var actualLookSpeed = delta * this.lookSpeed;

      if (!this.activeLook) {
        actualLookSpeed = 0;
      }

      var verticalLookRatio = 1;

      if (this.constrainVertical) {
        verticalLookRatio = Math.PI / (this.verticalMax - this.verticalMin);
      }

      lon -= this.mouseX * actualLookSpeed;
      if (this.lookVertical)
        lat -= this.mouseY * actualLookSpeed * verticalLookRatio;

      lat = Math.max(-85, Math.min(85, lat));

      var phi = THREE.MathUtils.degToRad(90 - lat);
      var theta = THREE.MathUtils.degToRad(lon);

      if (this.constrainVertical) {
        phi = THREE.MathUtils.mapLinear(
          phi,
          0,
          Math.PI,
          this.verticalMin,
          this.verticalMax
        );
      }

      var position = this.object.position;

      targetPosition.setFromSphericalCoords(1, phi, theta).add(position);

      this.object.lookAt(targetPosition);
    };
  })();

  function contextmenu(event) {
    event.preventDefault();
  }
  /*



                        DISPOSE method ?

                        The dispose is related to the Memory life Cycle


                        When the DocumentViewer is disposed, it will call the dispose methods of all its parts (View, Thumbnails, Bookmarks, Text and Annotations).

Disposing a document viewer will first clear the current document by using SetDocument(null), this will abort all workers and wait for them to exit gracefully. Hence, a slight delay might happen when calling this method.

When the document viewer is disposed, it will also delete all the UI controls created and remove them from their parent containers.

https://www.leadtools.com/help/sdk/v20/dh/javascript/doxui/documentviewer-dispose.html






*/
  this.dispose = function () {
    this.domElement.removeEventListener("contextmenu", contextmenu);
    this.domElement.removeEventListener("mousedown", _onMouseDown);
    this.domElement.removeEventListener("mousemove", _onMouseMove);
    this.domElement.removeEventListener("mouseup", _onMouseUp);

    window.removeEventListener("keydown", _onKeyDown);
    window.removeEventListener("keyup", _onKeyUp);
  };

  var _onMouseMove = bind(this, this.onMouseMove);
  var _onMouseDown = bind(this, this.onMouseDown);
  var _onMouseUp = bind(this, this.onMouseUp);
  var _onKeyDown = bind(this, this.onKeyDown);
  var _onKeyUp = bind(this, this.onKeyUp);

  this.domElement.addEventListener("contextmenu", contextmenu);
  this.domElement.addEventListener("mousemove", _onMouseMove);
  this.domElement.addEventListener("mousedown", _onMouseDown);
  this.domElement.addEventListener("mouseup", _onMouseUp);

  window.addEventListener("keydown", _onKeyDown);
  window.addEventListener("keyup", _onKeyUp);

  function bind(scope, fn) {
    return function () {
      fn.apply(scope, arguments);
    };
  }

  function setOrientation(controls) {
    var quaternion = controls.object.quaternion;

    lookDirection.set(0, 0, -10).applyQuaternion(quaternion);
    spherical.setFromVector3(lookDirection);

    lat = 90 - THREE.MathUtils.radToDeg(spherical.phi);
    lon = THREE.MathUtils.radToDeg(spherical.theta);
  }

  this.handleResize();

  setOrientation(this);
};



COMMON ERROR

  • Since I had no idea how to start with this, I presumed I could simply import the folders from the node modules like I did before, but after watching the tutorial I realized it wasn't required.
the error
    ./src/components/3dScenes/TropicFirstPerson.js
Attempted import error: 'FirstPersonControls' is not exported from 'three' (imported as 'THREE').

the solution
//
// instead of this:
this.cameraControlsFirstPerson = new THREE.FirstPersonControls();
//
//
//
//
//add this: (remove the THREE)

this.cameraControlsFirstPerson = new FirstPersonControls();


🌵

NOW REPLACE the old settings from the the purple rain camera for the following:

this.camera.position.x = 40;
this.camera.position.y = 40;
this.camera.position.z = 40; // origin: 50
//
this.camera.lookAt(this.scene.position); //we are looking at the center of the scene(depends of what yoou have in the camera position)


HIDE the old Orbits controls and the zoom distance

this.cameraControlsFirstPerson = new FirstPersonControls(
  this.camera,
  this.eleModelBlOne
);

this.cameraControlsFirstPerson.lookSpeed = 0.05;
this.cameraControlsFirstPerson.movementSpeed = 10;
//

//
//
//-------------- before the first person controls , we had this:
// OrbitControls allow a camera to orbit around the object
// https://threejs.org/docs/#examples/controls/OrbitControls
// this.controls = new OrbitControls(this.camera, this.eleModelBlOne);
//  ------------- **
//
//
//
//  to limit zoom distance so that the User
//  dont zoom out of the specified range

// this.controls.maxDistance = 70;
DELETE most of the models (leave just 1)


Now add the delta to make it work

  • I still dont get what is the concrete logic of the delta, i know that here it s updating something and causing a loop like, but I am confused about the "why " of its use.I just know that if you use the delta with the clock, your animation will never stop from doing what you told it to do, ex: if you want an object to move from top to bottom without ending and with a certain tempo, it will do it.
//
    //
    // ------------------ clock
    this.stop = 0;
    this.stepy = 0;
    this.clock = new THREE.Clock();

    //
    //
    //
  };

  // 3
  startAnimationLoop = () => {
    this.stop += 0.005;
    this.stepy += 0.00005;
    this.delta = this.clock.getDelta();
    //
    //
    this.cameraControlsFirstPerson.update(this.delta);
    //
    this.renderer.render(this.scene, this.camera);
    this.requestID = window.requestAnimationFrame(this.startAnimationLoop);
  };
  /*

First result

  • Here (in the image) I am clicking to get near the red object.

  • to turn, i just have to mouse over the direction i want.

  • but its not nice to test it on this object, tomorrow i will create another model where it will be much more easier to check the progress.




FIGURING a way to prevent the camera going up and down like so:

  • I found out you can change the following inside the node modules:

// API

//*******  here **********
//
// this will prevent the camera from looking up and down
this.lookVertical = true; //
// type npm start again, as refreshing will no update the changes
//



(UPDATE) Not really but its much easier to understand!
I WILL TRY another code that seems to me more of a (beginner level) than the one i am trying, so this code below is the code i ve been studying until now:
import React, { Component } from "react";
import * as THREE from "three";
//
// import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { FirstPersonControls } from "three/examples/jsm/controls/FirstPersonControls";
// import { FirstPersonControls } from "firstpersoncontrols.js";
//
//

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";

//
//

const style = {
  height: 600, // we can control scene size by setting container dimensions
};
//

//
//
class TropicalVoid extends Component {
  componentDidMount() {
    this.sceneSetup();
    this.addCustomSceneObjects();
    this.startAnimationLoop();

    //
    window.addEventListener("resize", this.handleWindowResize);
  }
  //
  //
  componentWillUnmount() {
    window.removeEventListener("resize", this.handleWindowResize);
    window.cancelAnimationFrame(this.requestID);
    // right now with the first person control,
    // we dont need this dispose as it s already included inside the three folder, check the read me, in the
    // beginning you will find a copy of the code inside the threejs that I am using.
    // this.controls.dispose();
  }
  /*



  */
  // 1
  sceneSetup = () => {
    // background color scene
    // this.lemonChiffon = "rgb(240, 224, 190)";

    const width = this.eleModelBlOne.clientWidth;
    const height = this.eleModelBlOne.clientHeight;
    //

    this.scene = new THREE.Scene();
    //
    //
    this.camera = new THREE.PerspectiveCamera(
      55, // fov = field of view
      width / height, // aspect ratio
      1, // near plane
      30000 // far plane
    );
    /*
    
    
    original
    
    this.camera = new THREE.PerspectiveCamera(
      //
      25, // fov = field of view
      width / height, // aspect ratio
      0.1, // near plane
      1000 // far plane
    );
    
    
    
    
    
    
    
    
    */
    //----------------------------------
    //           AXES HELPER
    //----------------------------------
    // This block of code is just to help you to see where you are at in the scene
    // if i add the this.axes, it s going to clash with the dispose inside the
    //
    // const axes = new THREE.AxesHelper(50); //origin 50
    // this.scene.add(axes);
    //
    this.camera.position.x = 12;
    this.camera.position.y = 0;
    this.camera.position.z = 0; // origin: 50
    //
    this.camera.lookAt(this.scene.position); //we are looking at the center of the scene(depends of what yoou have in the camera position)

    //
    //
    this.renderer = new THREE.WebGL1Renderer({
      // set the transparency of the scene, otherwise its black
      // alpha: true,
      // will make the edges smooth
      antialias: true,
    });
    //
    //
    //

    //

    //
    //
    // *************************************
    //
    //
    //-------------- before the first person controls , we had this:
    // OrbitControls allow a camera to orbit around the object
    // https://threejs.org/docs/#examples/controls/OrbitControls
    // this.controls = new OrbitControls(this.camera, this.eleModelBlOne);
    //  ------------- **
    //
    //
    //
    //  to limit zoom distance so that the User
    //  dont zoom out of the specified range

    // this.controls.maxDistance = 70;

    //
    //
    //
    this.renderer.setSize(width, height);
    // BG color from the scene
    // this.renderer.setClearColor(this.lemonChiffon);
    this.renderer.shadowMap.enabled = true;
    // here you append it to the jsx
    this.eleModelBlOne.appendChild(this.renderer.domElement); // mount using React ref

    this.cameraControlsFirstPerson = new FirstPersonControls(
      this.camera,
      this.eleModelBlOne
    );
    // ------------ with this 2 lines , the camera will move really fast, its better to hide it while testing it
    this.cameraControlsFirstPerson.lookSpeed = 0.05; // the speed when you move (without clicking)
    // lookSpeed = 0.02; slow
    // lookSpeed = 1; extremely fast
    this.cameraControlsFirstPerson.movementSpeed = 8; //the speed when you click

    // this.cameraControlsFirstPerson.maxPolarAngle = Math.PI / 2;

    //
    //
    //
    //
  };
  //
  //
  //
  //

  /*






  */
  // 2
  addCustomSceneObjects = () => {
    //----------------------------------
    //         BLENDER  MODELS
    //----------------------------------
    //
    const loader = new GLTFLoader();
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath("myDecoder/");
    loader.setDRACOLoader(dracoLoader);

    //
    // terrain_grosso_moon.-Normalize-4_.glb
    // 49,4Kb
    loader.load("./models/palmera-sun-palmeras2_retoucheeeggg.glb", (gltf) => {
      this.meshy = gltf.scene;

      gltf.scene.traverse((model) => {
        if (model.material) model.material.metalness = 0.08;

        model.receiveShadow = true;
        model.scale.set(2, 2, 2);
        // model.rotation.y = 1;
        // model.rotation.x += -0;
        // model.rotation.y += 0;
        //
        model.position.x = 0;
        model.position.y = -0.4;
        model.position.z = 0;
        //
        //
        //
      });

      this.scene.add(gltf.scene);
    });
    //
    // Add PLANE  w , h , segments
    const planeGeometry = new THREE.PlaneGeometry(500, 500, 100, 55);
    const planeMaterial = new THREE.MeshLambertMaterial({
      color: 0xdddddd,
      wireframe: true,
    });
    // var planeMaterial = new THREE.MeshLambertMaterial((color: 0xff0000));
    this.plane = new THREE.Mesh(planeGeometry, planeMaterial);
    //
    this.plane.rotation.x = -0.5 * Math.PI;
    this.plane.position.y = -1;
    //
    //
    // *** RECEIVE SHADOW
    // related to the light and the shadow
    this.plane.receiveShadow = true;
    this.scene.add(this.plane);
    //
    //
    // var box = new THREE.Box3();
    // box.setFromObject(this.plane);
    // //
    // //
    // if (this.camera.position.x > box.max.x) {
    //   this.camera.position.x = box.max.x;
    // }

    // if (this.camera.position.x < box.min.x) {
    //   this.camera.position.x = box.max.x;
    // }

    // if (this.camera.position.z > box.max.z) {
    //   this.camera.position.z = box.max.z;
    // }

    // if (this.camera.position.z < box.min.z) {
    //   this.camera.position.z = box.max.z;
    // }

    /*
    
    
    
    
    
    
    */
    //---------------------
    //   Directional Light
    //---------------------
    //
    // //
    this.renderer.outputEncoding = THREE.sRGBEncoding;
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.autoUpdate = true;
    this.renderer.gammaFactor = 2.2;

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(5, -1, 100);

    // position as follow , the light comes from x:-1000, comes from: y and the last comes from : z
    directionalLight.position.set(1000, 1000, 1000);
    directionalLight.castShadow = true;
    directionalLight.shadow.camera = new THREE.OrthographicCamera(
      -100,
      200,
      -200,
      200,
      0.5,
      5000
    );
    // //
    this.scene.add(directionalLight);
    // The light points to the flat ground
    // this.directionalLight.target = this.plane;  //dont add this
    //
    //
    //THIS LIGHT IS ON THE BOTTOM
    //---------------------
    //     spotLight FF5733
    //---------------------
    //
    //
    //
    //
    // With the light you can see the colors you added to each geometry in the materials
    this.spotLight = new THREE.SpotLight(0xffffff, 0.5); //intensity:   0.5);
    // spotLight.position.set( 0 , 10 , 0 );
    this.spotLight.position.set(5, -50, 0); //x, y , z   original (5, -50, 0);
    // (2, 32, 32); with this settings the light will be on the front
    this.spotLight.castShadow = true;
    //
    // this will remove the shadows
    this.spotLight.visible = true;
    //
    this.scene.add(this.spotLight);
    // //
    //

    /*



 
 */
    //
    //
    // ------------------ clock
    this.stop = 0;
    this.stepy = 0;

    this.clock = new THREE.Clock();

    //
    //
    //
  };

  // 3
  startAnimationLoop = () => {
    this.stop += 0.005;

    this.stepy += 0.00005;
    this.delta = this.clock.getDelta();
    //
    //
    this.cameraControlsFirstPerson.update(this.delta);
    //
    this.renderer.render(this.scene, this.camera);
    this.requestID = window.requestAnimationFrame(this.startAnimationLoop);
  };
  /*



  */
  handleWindowResize = () => {
    const width = this.eleModelBlOne.clientWidth;
    const height = this.eleModelBlOne.clientHeight;
    //
    // updated renderer
    this.renderer.setSize(width, height);
    // updated **camera** aspect ratio
    this.camera.aspect = width / height;
    // That is the Three.js optimization: you can group multiple camera changes into a block with only one
    this.camera.updateProjectionMatrix();
  };
  /*


  */
  render() {
    return (
      <div className="scene-oblivion">
        <div
          className="modelBleOne"
          style={style}
          ref={(ref) => (this.eleModelBlOne = ref)}
        ></div>
      </div>
    );
  }
}

export default TropicalVoid;



I WILL CONTINUE the "Pointer Lock Controls" in another respository:

Pointer Lock Controls