I’m extremely new to Javascript and Three.js (and to posting on stackOverflow). I’m attempting to implement a first person camera using three.js by converting the example implementation of PointerLockControls.js found here:
PointerLockControls example
My issue is that the error ‘TypeError: Cannot read properties of undefined (reading ‘lock’)
at HTMLDivElement.’ is thrown – caused by line 204 of my code (full code below) which reads:
// initialise locks:
const blocker = document.getElementById( 'blocker' );
const instructions = document.getElementById( 'instructions' );
instructions.addEventListener( 'click', function () {
// LINE 204: ERROR
this.controls_.lock;
} );
The example defines this lock as:
instructions.addEventListener( 'click', function () {
controls.lock();
} );
Previously, I analogously defined
instructions.addEventListener( 'click', function () {
this.controls_.lock();
} );
where PointerCamera.lock was defined as
lock() {
this.controls.lock();
}
I’ve attempted to redefine my lock functions using const = function () {...}, which led to the type error occurring in the PointerLockControls.js file.
Any help would be greatly appreciated!
Full Javascript:
import * as THREE from '../three.js-r134/three.js-r134/build/three.module.js';
import { FirstPersonControls } from '../three.js-r134/three.js-r134/examples/jsm/controls/FirstPersonControls.js';
import { PointerLockControls } from '../three.js-r134/three.js-r134/examples/jsm/controls/PointerLockControls.js';
// class for handling the camera
// camera implemented using the example: https://threejs.org/examples/?q=pointerlock#misc_controls_pointerlock
// listener is domElement used to listen for mouse/touch events
class PointerCamera {
constructor(camera, dElement, objects) {
this.camera = camera;
this.dElement = dElement
this.objects = objects;
this.raycaster = null;
this.moveForward = false;
this.moveBackward = false;
this.moveLeft = false;
this.moveRight = false;
this.canJump = false;
this.controls = null;
//minHeight initialised to initial value of y-coordinate of camera
this.minHeight = camera.position.y;
// this.prevTime
// may not need this if we pass it to the update from update in the main render
this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.vertex = new THREE.Vector3();
this.color = new THREE.Color();
// initialise movement
this.dElement.addEventListener( 'keydown', this.onKeyDown);
this.dElement.addEventListener( 'keyup', this.onKeyUp);
this.initControls();
}
initControls() {
this.controls = new PointerLockControls(this.camera, this.dElement);
// locks
this.lock = function () {
this.controls.lock();
}
this.unlock = function () {
this.controls.unlock();
}
}
changeMinHeight(newHeight) {
const oldHeight = this.minHeight;
this.minHeight = newHeight;
return oldHeight;
}
// keydown function
onKeyDown( event ) {
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 'Space':
if ( canJump === true ) velocity.y += 350;
this.canJump = false;
break;
}
}
//keyupfunction
//if domElement doesn't work, try passing full document to the class.
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;
}
}
update(timeElapsedS) {
if (this.controls.isLocked === true) {
// to adjust vertical acceleration, need to calculate whether on the ground or not.
const delta = timeElapsedS; // time elapsed in seconds
// may need to double check this hits 0.
this.velocity.x -= this.velocity.x * 10 * delta;
this.velocity.z -= this.velocity.z * 10 * delta;
// simulate gravity:
this.velocity.y -= 9.8 * 100 * delta; // 100.0 = mass of player
this.direction.z = Number(this.moveForward) - Number(this.moveBackward);
this.direction.x = Number(this.moveRight) - Number(this.moveLeft);
this.direction.normalize(); // ensures consistent movement in all directions
if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * 400 * delta;
if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * 400 * delta;
// add object interaction with ray casting here
// update camera position:
this.controls.moveRight( - this.velocity.x * delta);
this.controls.moveForward( - this.velocity.z * delta);
this.controls.getObject().position.y += ( this.velocity.y * delta ); // new behavior
// detect if camera falls below minimum:
if ( controls.getObject().position.y < 10 ) {
this.velocity.y = 0;
this.controls.getObject().position.y = 10;
this.canJump = true;
}
}
}
}
class FirstPersonCameraDemo {
constructor() {
this.initialize_();
}
initialize_() {
this.initializeRenderer_();
this.initializeLights_();
this.initializeScene_();
this.initializeDemo_();
this.previousRAF_ = null;
this.raf_();
this.onWindowResize_();
}
initializeDemo_() {
this.controls_ = new PointerCamera(this.camera_, document.body, []);
this.controls_.controls.lookSpeed = 0.8;
this.controls_.controls.movementSpeed = 5;
this.controls_.controls.heightCoef = 0;
// const controlLock = function () {this.controls_.lock};
// initialise locks:
const blocker = document.getElementById( 'blocker' );
const instructions = document.getElementById( 'instructions' );
instructions.addEventListener( 'click', function () {
this.controls_.lock;
} );
this.controls_.controls.addEventListener( 'lock', function () {
instructions.style.display = 'none';
blocker.style.display = 'none';
} );
this.controls_.controls.addEventListener( 'unlock', function () {
blocker.style.display = 'block';
instructions.style.display = '';
} );
this.scene_.add(this.controls_.controls.getObject());
}
initializeRenderer_() {
this.threejs_ = new THREE.WebGLRenderer({
antialias: false,
});
this.threejs_.shadowMap.enabled = true;
this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
this.threejs_.setPixelRatio(window.devicePixelRatio);
this.threejs_.setSize(window.innerWidth, window.innerHeight);
this.threejs_.physicallyCorrectLights = true;
this.threejs_.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(this.threejs_.domElement);
window.addEventListener('resize', () => {
this.onWindowResize_();
}, false);
// initialise CAMERA
this.minHeight = 10;
const fov = 60;
const aspect = window.innerWidth / window.innerHeight;
const near = 1.0;
const far = 1000.0;
this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
// minimum and default height (10 - change to variable later)
this.camera_.position.set(0, this.minHeight, 0);
this.scene_ = new THREE.Scene();
}
initializeScene_() {
const loader = new THREE.CubeTextureLoader();
const texture = loader.load([
'./bkg/bkg/red/bkg1_right1.png',
'./bkg/bkg/red/bkg1_left2.png',
'./bkg/bkg/red/bkg1_top3.png',
'./bkg/bkg/red/bkg1_bottom4.png',
'./bkg/bkg/red/bkg1_front5.png',
'./bkg/bkg/red/bkg1_back6.png'
]);
this.scene_.background = texture;
const planegeo = new THREE.PlaneGeometry(100, 100, 10, 10);
// plane.castShadow = false;
// plane.receiveShadow = true;
planegeo.rotateX(-Math.PI / 2);
const material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
const plane = new THREE.Mesh( planegeo, material );
this.scene_.add(plane);
}
initializeLights_() {
// his light:
const distance = 50.0;
const angle = Math.PI / 4.0;
const penumbra = 0.5;
const decay = 1.0;
let light = new THREE.SpotLight(
0xFFFFFF, 100.0, distance, angle, penumbra, decay);
light.castShadow = true;
light.shadow.bias = -0.00001;
light.shadow.mapSize.width = 4096;
light.shadow.mapSize.height = 4096;
light.shadow.camera.near = 1;
light.shadow.camera.far = 100;
light.position.set(25, 25, 0);
light.lookAt(0, 0, 0);
this.scene_.add(light);
const upColour = 0xFFFF80;
const downColour = 0x808080;
light = new THREE.HemisphereLight(upColour, downColour, 0.5);
light.color.setHSL( 0.6, 1, 0.6 );
light.groundColor.setHSL( 0.095, 1, 0.75 );
light.position.set(0, 4, 0);
this.scene_.add(light);
}
onWindowResize_() {
this.camera_.aspect = window.innerWidth / window.innerHeight;
this.camera_.updateProjectionMatrix();
this.threejs_.setSize(window.innerWidth, window.innerHeight);
}
raf_() {
requestAnimationFrame((t) => {
if (this.previousRAF_ === null) {
this.previousRAF_ = t;
}
this.step_(t - this.previousRAF_);
this.threejs_.autoClear = true;
this.threejs_.render(this.scene_, this.camera_);
console.log("just tried to render");
this.threejs_.autoClear = false;
this.previousRAF_ = t;
this.raf_();
});
}
step_(timeElapsed) {
// console.log("in demo step");
const timeElapsedS = timeElapsed * 0.01;
// may need to change above to 0.001
this.controls_.update(timeElapsedS);
}
}
// run
let _APP = null;
window.addEventListener('DOMContentLoaded', () => {
console.log("successful print");
_APP = new FirstPersonCameraDemo();
});
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Trying to get basic FPS control</title>
<style>
body { margin: 0; }
#blocker {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
#instructions {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="blocker">
<div id="instructions">
<p style="font-size:36px">
Click to play
</p>
<p>
Move: WASD<br/>
Jump: SPACE<br/>
Look: MOUSE
</p>
</div>
</div>
<script type="module" src="./fpsbasic.js"></script>
</body>
</html>