188 lines
6.7 KiB
JavaScript
188 lines
6.7 KiB
JavaScript
/*
|
|
*
|
|
* The p5.RoverCam library - First-Person 3D CameraControl for p5.js and WEBGL.
|
|
*
|
|
* Copyright © 2020 by p5.RoverCam authors
|
|
*
|
|
* Source: https://github.com/freshfork/p5.RoverCam
|
|
*
|
|
* MIT License: https://opensource.org/licenses/MIT
|
|
*
|
|
*
|
|
* explanatory note:
|
|
*
|
|
* p5.RoverCam is a derivative of the QueasyCam Library by Josh Castle,
|
|
* ported to JavaScript for p5.js from github.com/jrc03c/queasycam
|
|
*
|
|
* updates
|
|
* 20200628 incorporate pointerLock and overridable controller method
|
|
* 20200629 add support for switching between multiple cameras
|
|
* 20200701 v1.1.0 fix registerMethod and allow for p5js instance mode
|
|
*/
|
|
|
|
// First-person camera control
|
|
// Mouse:
|
|
// left/right : yaw
|
|
// up/down : pitch
|
|
// click : enter/leave pointerLock
|
|
|
|
// Keys: a/d : yaw or left/right if pointerLock
|
|
// w/s : forward/backward
|
|
// e/q : up/down
|
|
|
|
class RoverCam {
|
|
constructor(instance) {
|
|
this.sensitivity = 0.02;
|
|
this.friction = 0.8;
|
|
this.speed = 0.1;
|
|
this.reset();
|
|
this.active = true; // use the setActive method
|
|
this.enableControl = true; // used to enable/disable controls
|
|
if (instance !== undefined) this.p5 = instance;
|
|
else this.p5 = p5.instance;
|
|
if (this.p5 !== null)
|
|
this.p5.registerMethod('post', () => {
|
|
if (this.active) this.draw();
|
|
});
|
|
}
|
|
|
|
// Application can override the following method
|
|
controller() { // default behavior
|
|
if (!this.enableControl) return;
|
|
if (RoverCam.pointerLock) {
|
|
this.yaw(this.p5.movedX * this.sensitivity / 10); // mouse left/right
|
|
this.pitch(this.p5.movedY * this.sensitivity / 10); // mouse up/down
|
|
if (this.p5.keyIsDown(65) || this.p5.keyIsDown(this.p5.LEFT_ARROW)) this.moveY(this.speed); // a
|
|
if (this.p5.keyIsDown(68) || this.p5.keyIsDown(this.p5.RIGHT_ARROW)) this.moveY(-this.speed); // d
|
|
} else { // otherwise yaw/pitch with keys
|
|
if (this.p5.keyIsDown(65) || this.p5.keyIsDown(this.p5.LEFT_ARROW)) this.yaw(-this.sensitivity); // a
|
|
if (this.p5.keyIsDown(68) || this.p5.keyIsDown(this.p5.RIGHT_ARROW)) this.yaw(this.sensitivity); // d
|
|
if (this.p5.keyIsDown(82)) this.pitch(-this.sensitivity); // r
|
|
if (this.p5.keyIsDown(70)) this.pitch(this.sensitivity); // f
|
|
}
|
|
if (this.p5.keyIsDown(87) || this.p5.keyIsDown(this.p5.UP_ARROW)) this.moveX(this.speed); // w
|
|
if (this.p5.keyIsDown(83) || this.p5.keyIsDown(this.p5.DOWN_ARROW)) this.moveX(-this.speed); // s
|
|
if (this.p5.keyIsDown(69)) this.moveZ(this.speed); // e
|
|
if (this.p5.keyIsDown(81)) this.moveZ(-this.speed); // q
|
|
|
|
if (this.p5.keyIsDown(107) || this.p5.keyIsDown(187)) this.fov(-this.sensitivity / 10); // +
|
|
if (this.p5.keyIsDown(109) || this.p5.keyIsDown(189)) this.fov(this.sensitivity / 10); // -
|
|
|
|
// test roll TBD
|
|
//if(this.p5.keyIsDown(90)) this.roll(this.sensitivity); // z
|
|
//if(this.p5.keyIsDown(67)) this.roll(-this.sensitivity); // c
|
|
}
|
|
|
|
// Primitive internal camera control methods
|
|
moveX(speed) {
|
|
this.velocity.add(p5.Vector.mult(this.forward, speed));
|
|
}
|
|
moveY(speed) {
|
|
this.velocity.add(p5.Vector.mult(this.right, speed));
|
|
}
|
|
moveZ(speed) {
|
|
this.velocity.add(p5.Vector.mult(this.up, -speed));
|
|
}
|
|
yaw(angle) {
|
|
this.pan += angle;
|
|
}
|
|
pitch(angle) {
|
|
this.tilt += angle;
|
|
this.tilt = this.clamp(this.tilt, -Math.PI / 2.01, Math.PI / 2.01);
|
|
if (this.tilt == Math.PI / 2.0) this.tilt += 0.001;
|
|
}
|
|
roll(angle) { // TBD: useful for flight sim or sloped racetracks
|
|
this.rot += angle;
|
|
}
|
|
fov(angle) {
|
|
this.fovy += angle;
|
|
this.width = 0; // trigger a perspective call in the draw loop
|
|
}
|
|
reset() {
|
|
this.pan = 0.0;
|
|
this.tilt = 0.0;
|
|
this.rot = 0.0;
|
|
this.fovy = 1.0;
|
|
this.width = 0; // trigger a perspective call in the draw loop
|
|
this.height = 0;
|
|
this.position = new p5.Vector(0, 0, 0);
|
|
this.velocity = new p5.Vector(0, 0, 0);
|
|
this.up = new p5.Vector(0, 1, 0);
|
|
this.right = new p5.Vector(1, 0, 0);
|
|
this.forward = new p5.Vector(0, 0, 1);
|
|
}
|
|
setActive(active) { // method to switch between multiple cameras
|
|
this.active = active;
|
|
if (active) this.width = 0; // trigger a perspective call in the draw loop
|
|
}
|
|
setState(state) { // state object can have fov,active,rotation,position
|
|
if (state.fov !== undefined) {
|
|
this.fovy = state.fov;
|
|
this.width = 0; // trigger a perspective call in the draw loop;
|
|
}
|
|
if (state.active !== undefined) this.active = state.active;
|
|
if (state.rotation !== undefined) {
|
|
this.pan = state.rotation[0];
|
|
this.tilt = state.rotation[1];
|
|
this.rot = state.rotation[2];
|
|
}
|
|
if (state.position !== undefined) this.position = new p5.Vector(state.position[0], state.position[1], state.position[2]);
|
|
}
|
|
|
|
// This method is called after the main p5.js draw loop
|
|
draw() {
|
|
if (this.p5.width !== this.width || this.p5.height !== this.height) {
|
|
this.p5.perspective(this.fovy, this.p5.width / this.p5.height, 0.01, 1000.0);
|
|
this.width = this.p5.width;
|
|
this.height = this.p5.height;
|
|
}
|
|
|
|
// Call the potentially overridden controller method
|
|
this.controller();
|
|
|
|
this.forward = new p5.Vector(Math.cos(this.pan), Math.tan(this.tilt), Math.sin(this.pan));
|
|
this.forward.normalize();
|
|
this.right = new p5.Vector(Math.cos(this.pan - Math.PI / 2.0), 0, Math.sin(this.pan - Math.PI / 2.0));
|
|
// TBD: handle roll command (using this.rot)
|
|
|
|
this.velocity.mult(this.friction);
|
|
this.position.add(this.velocity);
|
|
let center = p5.Vector.add(this.position, this.forward);
|
|
this.p5.camera(this.position.x, this.position.y, this.position.z, center.x, center.y, center.z, this.up.x, this.up.y, this.up.z);
|
|
}
|
|
|
|
clamp(aNumber, aMin, aMax) {
|
|
return (aNumber > aMax ? aMax :
|
|
aNumber < aMin ? aMin :
|
|
aNumber);
|
|
}
|
|
}
|
|
RoverCam.version = "1.1.0";
|
|
// Optional pointerLock applies to all RoverCam instances
|
|
RoverCam.pointerLock = false;
|
|
RoverCam.usePointerLock = (instance) => {
|
|
if (instance === undefined) instance = p5.instance;
|
|
if (instance === null) return;
|
|
RoverCam.canvas = instance._renderer.elt;
|
|
// ffd8 - click into pointerlock example based on:
|
|
// https://p5js.org/reference/#/p5/exitPointerLock
|
|
document.addEventListener('click', () => {
|
|
if (!RoverCam.pointerLock) {
|
|
RoverCam.pointerLock = true;
|
|
instance.requestPointerLock();
|
|
} else {
|
|
instance.exitPointerLock();
|
|
RoverCam.pointerLock = false;
|
|
}
|
|
}, false);
|
|
document.addEventListener('pointerlockchange', RoverCam.onPointerlockChange, false);
|
|
}
|
|
// handle exit from pointerLock when user presses ESCAPE
|
|
RoverCam.onPointerlockChange = () => {
|
|
if (document.pointerLockElement !== RoverCam.canvas &&
|
|
document.mozPointerLockElement !== RoverCam.canvas) RoverCam.pointerLock = false;
|
|
}
|
|
p5.prototype.createRoverCam = function() {
|
|
return new RoverCam(this);
|
|
}
|