From 16c617d573d9f9ccabf6b691f1aa84f4a08cf100 Mon Sep 17 00:00:00 2001 From: kpal Date: Thu, 30 May 2024 10:25:15 +0100 Subject: [PATCH 1/3] added multi-camera example --- .../src/examples/camera/multi.example.mjs | 113 +++ examples/thumbnails/camera_multi_large.webp | Bin 0 -> 3966 bytes examples/thumbnails/camera_multi_small.webp | Bin 0 -> 416 bytes scripts/camera/multi-camera.js | 753 ++++++++++++++++++ 4 files changed, 866 insertions(+) create mode 100644 examples/src/examples/camera/multi.example.mjs create mode 100644 examples/thumbnails/camera_multi_large.webp create mode 100644 examples/thumbnails/camera_multi_small.webp create mode 100644 scripts/camera/multi-camera.js diff --git a/examples/src/examples/camera/multi.example.mjs b/examples/src/examples/camera/multi.example.mjs new file mode 100644 index 00000000000..48a3df02dd7 --- /dev/null +++ b/examples/src/examples/camera/multi.example.mjs @@ -0,0 +1,113 @@ +// @config DESCRIPTION
(WASDQE) Move
(LMB) Orbit, (RMB) Fly
(Scroll Wheel) zoom
(MMB / Hold Shift) Pan
(F) Focus
+// @config HIDDEN +import * as pc from 'playcanvas'; +import { deviceType, rootPath } from 'examples/utils'; + +const canvas = document.getElementById('application-canvas'); +if (!(canvas instanceof HTMLCanvasElement)) { + throw new Error('No canvas found'); +} +window.focus(); + +const gfxOptions = { + deviceTypes: [deviceType], + glslangUrl: rootPath + '/static/lib/glslang/glslang.js', + twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js' +}; + +const assets = { + helipad: new pc.Asset( + 'helipad-env-atlas', + 'texture', + { url: rootPath + '/static/assets/cubemaps/helipad-env-atlas.png' }, + { type: pc.TEXTURETYPE_RGBP, mipmaps: false } + ), + script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/multi-camera.js' }), + statue: new pc.Asset('statue', 'container', { url: rootPath + '/static/assets/models/statue.glb' }) +}; + +const device = await pc.createGraphicsDevice(canvas, gfxOptions); +device.maxPixelRatio = Math.min(window.devicePixelRatio, 2); + +const createOptions = new pc.AppOptions(); +createOptions.graphicsDevice = device; + +createOptions.componentSystems = [ + pc.RenderComponentSystem, + pc.CameraComponentSystem, + pc.LightComponentSystem, + pc.ScriptComponentSystem +]; +createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler]; + +const app = new pc.AppBase(canvas); +app.init(createOptions); + +// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size +app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW); +app.setCanvasResolution(pc.RESOLUTION_AUTO); + +// Ensure canvas is resized when window changes size +const resize = () => app.resizeCanvas(); +window.addEventListener('resize', resize); +app.on('destroy', () => { + window.removeEventListener('resize', resize); +}); + +await new Promise((resolve) => { + new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve); +}); + +/** + * @param {pc.Entity} focus - The entity to focus the camera on. + * @returns {pc.Entity} The multi-camera entity. + */ +function createMultiCamera(focus) { + const camera = new pc.Entity(); + camera.addComponent('camera'); + + const multiCamera = new pc.Entity(); + multiCamera.addComponent('script'); + if (!multiCamera.script) { + throw new Error('Script component not found'); + } + + /** @type {any} */ + const script = multiCamera.script.create('multiCamera', { + attributes: { + camera + } + }); + + // wait until after canvas resized to focus on entity + const resize = new ResizeObserver(() => { + script.focusOnEntity(focus, true); + resize.disconnect(); + }); + resize.observe(canvas ?? document.body); + + return multiCamera; +} + +app.start(); + +app.scene.ambientLight.set(0.4, 0.4, 0.4); + +app.scene.skyboxMip = 1; +app.scene.skyboxIntensity = 0.4; +app.scene.envAtlas = assets.helipad.resource; + +// Create a directional light +const light = new pc.Entity(); +light.addComponent('light'); +light.setLocalEulerAngles(45, 30, 0); +app.root.addChild(light); + +const statue = assets.statue.resource.instantiateRenderEntity(); +statue.setLocalPosition(0, -0.5, 0); +app.root.addChild(statue); + +const multiCamera = createMultiCamera(statue); +app.root.addChild(multiCamera); + +export { app }; diff --git a/examples/thumbnails/camera_multi_large.webp b/examples/thumbnails/camera_multi_large.webp new file mode 100644 index 0000000000000000000000000000000000000000..7ea680395b5ffa8ed4c45ad2cb87984c6b3c289b GIT binary patch literal 3966 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 Y)VEU5XlYAN`huqI8Tdd7jRFgR09(nkfdBvi literal 0 HcmV?d00001 diff --git a/examples/thumbnails/camera_multi_small.webp b/examples/thumbnails/camera_multi_small.webp new file mode 100644 index 0000000000000000000000000000000000000000..af47f8c3719421d77990c535788a55976d92eb8e GIT binary patch literal 416 zcmV;R0bl-7Nk&GP0RRA3MM6+kP&gor0RRBd2>_h|DnI}*06uLnlSQN>A(dPPd_V@o zvVdcg$CIakQmiiL-~RD6>WzC)dDOvb{yJP`ynsy8o?bMly~BEK_&oALa2`@&1q1Bp z_&zUN)|N1N)<6Ks)!Ge+s5Q>xBH-eRcjVt6;D^K~&eRn5G^GIk&+wI=WI!M8Sv;gW zBy2LWda1WHdh*<Ad-=w77n%7ecCnOVmNTlmoR4+x7I6noAj*h@ zGrAVdTpLeatNBFY6vm}tmB45-dJYT+Ym3ns`w6Nv5?O#Ukcur9$!Ud3;W^D3TUF4+ zI=>+waR(2x9ho3FuU$DEhyJEv=s>I)I&GjP7Ep^$M1s){DJ`x9_XL6+vd_DCoND?J ziOFc|u~=&xSHJ$Fbd;fOI3sIy`4w1$?QhmFcrRzHKwPN)0DGhYFCS98;yI{P0P-j5 K|Nff^0000ymb_*F literal 0 HcmV?d00001 diff --git a/scripts/camera/multi-camera.js b/scripts/camera/multi-camera.js new file mode 100644 index 00000000000..b8ea90b0f59 --- /dev/null +++ b/scripts/camera/multi-camera.js @@ -0,0 +1,753 @@ +(() => { + const { createScript, BoundingBox, Vec2, Vec3, Ray, Plane, math } = pc; + + const tmpVa = new Vec2(); + const tmpV1 = new Vec3(); + const tmpV2 = new Vec3(); + const tmpR1 = new Ray(); + const tmpP1 = new Plane(); + + const PASSIVE = { passive: false }; + + const LOOK_MAX_ANGLE = 90; + + function calcEntityAABB(bbox, entity) { + bbox.center.set(0, 0, 0); + bbox.halfExtents.set(0, 0, 0); + entity.findComponents('render').forEach((render) => { + render.meshInstances.forEach((mi) => { + bbox.add(mi.aabb); + }); + }); + return bbox; + } + + class BaseCamera { + /** + * @type {Entity} + */ + entity; + + /** + * @type {HTMLElement} + */ + target = document.documentElement; + + /** + * @type {number} + */ + sceneSize = 100; + + /** + * @type {number} + */ + lookSensitivity = 0.2; + + /** + * @type {number} + */ + lookDamping = 0.97; + + /** + * @type {number} + */ + moveDamping = 0.98; + + /** + * @type {Entity} + * @protected + */ + _camera = null; + + /** + * @type {Vec3} + * @protected + */ + _origin = new Vec3(0, 1, 0); + + /** + * @type {Vec3} + * @protected + */ + _position = new Vec3(); + + /** + * @type {Vec2} + * @protected + */ + _dir = new Vec2(); + + /** + * @type {Vec3} + * @protected + */ + _angles = new Vec3(); + + /** + * @param {Entity} entity - The entity to attach the camera to. + * @param {HTMLElement} target - The target element to listen for pointer events. + * @param {Record} options - The options for the camera. + */ + constructor(entity, target, options = {}) { + this.entity = entity; + this.target = target; + this.sceneSize = options.sceneSize ?? this.sceneSize; + this.lookSensitivity = options.lookSensitivity ?? this.lookSensitivity; + this.lookDamping = options.lookDamping ?? this.lookDamping; + this.moveDamping = options.moveDamping ?? this.moveDamping; + + this._onPointerDown = this._onPointerDown.bind(this); + this._onPointerMove = this._onPointerMove.bind(this); + this._onPointerUp = this._onPointerUp.bind(this); + } + + /** + * @param {number} dt - The delta time in seconds. + * @private + */ + _smoothLook(dt) { + const lerpRate = 1 - Math.pow(this.lookDamping, dt * 1000); + this._angles.x = math.lerp(this._angles.x, this._dir.x, lerpRate); + this._angles.y = math.lerp(this._angles.y, this._dir.y, lerpRate); + this.entity.setEulerAngles(this._angles); + } + + /** + * @param {number} dt - The delta time in seconds. + * @private + */ + _smoothMove(dt) { + this._position.lerp(this._position, this._origin, 1 - Math.pow(this.moveDamping, dt * 1000)); + this.entity.setPosition(this._position); + } + + /** + * @param {MouseEvent} event - The mouse event. + * @private + */ + _onContextMenu(event) { + event.preventDefault(); + } + + /** + * @param {PointerEvent} event - The pointer event. + * @protected + * @abstract + */ + _onPointerDown(event) {} + + /** + * @param {PointerEvent} event - The pointer move event. + * @protected + * @abstract + */ + _onPointerMove(event) {} + + /** + * @param {PointerEvent} event - The pointer event. + * @protected + * @abstract + */ + _onPointerUp(event) {} + + /** + * @param {PointerEvent} event - The pointer move event. + * @protected + */ + _look(event) { + if (event.target !== this.target) { + return; + } + const movementX = event.movementX || 0; + const movementY = event.movementY || 0; + this._dir.x = math.clamp(this._dir.x - movementY * this.lookSensitivity, -LOOK_MAX_ANGLE, LOOK_MAX_ANGLE); + this._dir.y -= movementX * this.lookSensitivity; + } + + /** + * @param {Entity} camera - The camera entity to attach. + */ + attach(camera) { + this._camera = camera; + this._camera.setLocalEulerAngles(0, 0, 0); + + window.addEventListener('pointerdown', this._onPointerDown); + window.addEventListener('pointermove', this._onPointerMove); + window.addEventListener('pointerup', this._onPointerUp); + window.addEventListener('contextmenu', this._onContextMenu); + + this.entity.addChild(camera); + } + + detach() { + window.removeEventListener('pointermove', this._onPointerMove); + window.removeEventListener('pointerdown', this._onPointerDown); + window.removeEventListener('pointerup', this._onPointerUp); + window.removeEventListener('contextmenu', this._onContextMenu); + + this.entity.removeChild(this._camera); + this._camera = null; + + this._dir.x = this._angles.x; + this._dir.y = this._angles.y; + + this._origin.copy(this._position); + } + + /** + * @param {number} dt - The delta time in seconds. + */ + update(dt) { + if (!this._camera) { + return; + } + + this._smoothLook(dt); + this._smoothMove(dt); + } + } + + class MultiCamera extends BaseCamera { + /** + * @type {number} + */ + focusFov = 75; + + /** + * @type {number} + */ + lookSensitivity = 0.2; + + /** + * @type {number} + */ + lookDamping = 0.97; + + /** + * @type {number} + */ + moveDamping = 0.98; + + /** + * @type {number} + */ + pinchSpeed = 5; + + /** + * @type {number} + */ + wheelSpeed = 0.005; + + /** + * @type {number} + */ + zoomMin = 0.001; + + /** + * @type {number} + */ + zoomMax = 10; + + /** + * @type {number} + */ + zoomScaleMin = 0.01; + + /** + * @type {number} + */ + moveSpeed = 2; + + /** + * @type {number} + */ + sprintSpeed = 4; + + /** + * @type {number} + */ + crouchSpeed = 1; + + /** + * @type {number} + * @private + */ + _zoom = 0; + + /** + * @type {number} + * @private + */ + _cameraDist = 0; + + /** + * @type {Map} + * @private + */ + _pointerEvents = new Map(); + + /** + * @type {number} + * @private + */ + _lastPinchDist = -1; + + /** + * @type {Vec2} + * @private + */ + _lastPosition = new Vec2(); + + /** + * @type {boolean} + */ + _panning = false; + + /** + * @type {boolean} + */ + _flying = false; + + /** + * @type {Record} + * @private + */ + _key = { + forward: false, + backward: false, + left: false, + right: false, + up: false, + down: false, + sprint: false, + crouch: false + }; + + /** + * @param {Entity} entity - The entity to attach the camera to. + * @param {HTMLElement} target - The target element to listen for pointer events. + * @param {Record} options - The options for the camera. + */ + constructor(entity, target, options = {}) { + super(entity, target, options); + + this.pinchSpeed = options.pinchSpeed ?? this.pinchSpeed; + this.wheelSpeed = options.wheelSpeed ?? this.wheelSpeed; + this.zoomMin = options.zoomMin ?? this.zoomMin; + this.zoomMax = options.zoomMax ?? this.zoomMax; + this.moveSpeed = options.moveSpeed ?? this.moveSpeed; + this.sprintSpeed = options.sprintSpeed ?? this.sprintSpeed; + this.crouchSpeed = options.crouchSpeed ?? this.crouchSpeed; + + this._onWheel = this._onWheel.bind(this); + this._onKeyDown = this._onKeyDown.bind(this); + this._onKeyUp = this._onKeyUp.bind(this); + } + + /** + * @param {PointerEvent} event - The pointer event. + * @protected + */ + _onPointerDown(event) { + if (!this._camera) { + return; + } + this._pointerEvents.set(event.pointerId, event); + if (this._pointerEvents.size === 2) { + this._lastPinchDist = this._getPinchDist(); + this._getMidPoint(this._lastPosition); + this._panning = true; + } + if (event.shiftKey || event.button === 1) { + this._lastPosition.set(event.clientX, event.clientY); + this._panning = true; + } + if (event.button === 2) { + this._zoom = this._cameraDist; + this._origin.copy(this._camera.getPosition()); + this._position.copy(this._origin); + this._camera.setLocalPosition(0, 0, 0); + this._flying = true; + } + } + + /** + * @param {PointerEvent} event - The pointer event. + * @protected + */ + _onPointerMove(event) { + if (this._pointerEvents.size === 0) { + return; + } + + this._pointerEvents.set(event.pointerId, event); + + if (this._pointerEvents.size === 1) { + if (this._panning) { + // mouse pan + this._handlePan(tmpVa.set(event.clientX, event.clientY)); + } else { + super._look(event); + } + return; + } + + if (this._pointerEvents.size === 2) { + // touch pan + this._handlePan(this._getMidPoint(tmpVa)); + + // pinch zoom + const pinchDist = this._getPinchDist(); + if (this._lastPinchDist > 0) { + this._handleZoom((this._lastPinchDist - pinchDist) * this.pinchSpeed); + } + this._lastPinchDist = pinchDist; + } + } + + /** + * @param {PointerEvent} event - The pointer event. + * @protected + */ + _onPointerUp(event) { + this._pointerEvents.delete(event.pointerId); + if (this._pointerEvents.size < 2) { + this._lastPinchDist = -1; + this._panning = false; + } + if (this._panning) { + this._panning = false; + } + if (this._flying) { + tmpV1.copy(this.entity.forward).mulScalar(this._zoom); + this._origin.add(tmpV1); + this._position.add(tmpV1); + this._flying = false; + } + } + + /** + * @param {WheelEvent} event - The wheel event. + * @private + */ + _onWheel(event) { + event.preventDefault(); + this._handleZoom(event.deltaY); + } + + /** + * @param {KeyboardEvent} event - The keyboard event. + * @private + */ + _onKeyDown(event) { + event.stopPropagation(); + switch (event.key.toLowerCase()) { + case 'w': + this._key.forward = true; + break; + case 's': + this._key.backward = true; + break; + case 'a': + this._key.left = true; + break; + case 'd': + this._key.right = true; + break; + case 'q': + this._key.up = true; + break; + case 'e': + this._key.down = true; + break; + case 'shift': + this._key.sprint = true; + break; + case 'control': + this._key.crouch = true; + break; + } + } + + /** + * @param {KeyboardEvent} event - The keyboard event. + * @private + */ + _onKeyUp(event) { + event.stopPropagation(); + switch (event.key.toLowerCase()) { + case 'w': + this._key.forward = false; + break; + case 's': + this._key.backward = false; + break; + case 'a': + this._key.left = false; + break; + case 'd': + this._key.right = false; + break; + case 'q': + this._key.up = false; + break; + case 'e': + this._key.down = false; + break; + case 'shift': + this._key.sprint = false; + break; + case 'control': + this._key.crouch = false; + break; + } + } + + /** + * @param {number} dt - The time delta. + * @private + */ + _handleMove(dt) { + tmpV1.set(0, 0, 0); + if (this._key.forward) { + tmpV1.add(this.entity.forward); + } + if (this._key.backward) { + tmpV1.sub(this.entity.forward); + } + if (this._key.left) { + tmpV1.sub(this.entity.right); + } + if (this._key.right) { + tmpV1.add(this.entity.right); + } + if (this._key.up) { + tmpV1.add(this.entity.up); + } + if (this._key.down) { + tmpV1.sub(this.entity.up); + } + tmpV1.normalize(); + const speed = this._key.crouch ? this.crouchSpeed : this._key.sprint ? this.sprintSpeed : this.moveSpeed; + tmpV1.mulScalar(this.sceneSize * speed * dt); + this._origin.add(tmpV1); + } + + /** + * @param {Vec2} out - The output vector. + * @returns {Vec2} The mid point. + * @private + */ + _getMidPoint(out) { + const [a, b] = this._pointerEvents.values(); + const dx = a.clientX - b.clientX; + const dy = a.clientY - b.clientY; + return out.set(b.clientX + dx * 0.5, b.clientY + dy * 0.5); + } + + /** + * @returns {number} The pinch distance. + * @private + */ + _getPinchDist() { + const [a, b] = this._pointerEvents.values(); + const dx = a.clientX - b.clientX; + const dy = a.clientY - b.clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * @param {Vec2} pos - The position. + * @param {Vec3} point - The output point. + * @private + */ + _screenToWorldPan(pos, point) { + const mouseW = this._camera.camera.screenToWorld(pos.x, pos.y, 1); + const cameraPos = this._camera.getPosition(); + + const focusDirScaled = tmpV1.copy(this.entity.forward).mulScalar(this._zoom); + const focalPos = tmpV2.add2(cameraPos, focusDirScaled); + const planeNormal = focusDirScaled.mulScalar(-1).normalize(); + + const plane = tmpP1.setFromPointNormal(focalPos, planeNormal); + const ray = tmpR1.set(cameraPos, mouseW.sub(cameraPos).normalize()); + + plane.intersectsRay(ray, point); + } + + /** + * @param {Vec2} pos - The position. + * @private + */ + _handlePan(pos) { + const start = new Vec3(); + const end = new Vec3(); + + this._screenToWorldPan(this._lastPosition, start); + this._screenToWorldPan(pos, end); + + tmpV1.sub2(start, end); + this._origin.add(tmpV1); + + this._lastPosition.copy(pos); + } + + /** + * @param {number} delta - The delta. + * @private + */ + _handleZoom(delta) { + const min = this._camera.camera.nearClip + this.zoomMin * this.sceneSize; + const max = this.zoomMax * this.sceneSize; + const scale = math.clamp(this._zoom / (max - min), this.zoomScaleMin, 1); + this._zoom += delta * this.wheelSpeed * this.sceneSize * scale; + this._zoom = math.clamp(this._zoom, min, max); + } + + /** + * @returns {number} The zoom. + * @private + */ + _calcZoom() { + const camera = this._camera.camera; + const d1 = Math.tan(0.5 * this.focusFov * math.DEG_TO_RAD); + const d2 = Math.tan(0.5 * camera.fov * math.DEG_TO_RAD); + + const scale = (d1 / d2) * (1 / camera.aspectRatio); + return scale * this.sceneSize + this.sceneSize; + } + + /** + * @param {Vec3} point - The point to focus on. + * @param {Vec3} [start] - The start point. + * @param {boolean} [snap] - Whether to snap the focus. + */ + focus(point, start, snap = false) { + if (!this._camera) { + return; + } + + this._origin.copy(point); + if (snap) { + this._position.copy(point); + } + this._camera.setPosition(start); + this._camera.setLocalEulerAngles(0, 0, 0); + + if (!start) { + return; + } + + tmpV1.sub2(start, point); + const elev = Math.atan2(tmpV1.y, tmpV1.z) * math.RAD_TO_DEG; + const azim = Math.atan2(tmpV1.x, tmpV1.z) * math.RAD_TO_DEG; + this._dir.set(-elev, -azim); + if (snap) { + this._angles.copy(this._dir); + } + + this._zoom = tmpV1.length(); + } + + /** + * @param {Entity} entity - The entity to focus on. + * @param {boolean} [snap] - Whether to snap the focus. + */ + focusOnEntity(entity, snap = false) { + const bbox = calcEntityAABB(new BoundingBox(), entity); + this.sceneSize = bbox.halfExtents.length(); + this.focus(bbox.center, undefined, snap); + this._zoom = this._calcZoom(); + if (snap) { + this._cameraDist = this._zoom; + } + } + + /** + * @param {Entity} camera - The camera entity to attach. + */ + attach(camera) { + super.attach(camera); + this._camera.setPosition(0, 0, 0); + this._camera.setLocalEulerAngles(0, 0, 0); + + window.addEventListener('wheel', this._onWheel, PASSIVE); + window.addEventListener('keydown', this._onKeyDown, false); + window.addEventListener('keyup', this._onKeyUp, false); + } + + detach() { + super.detach(); + + window.removeEventListener('wheel', this._onWheel, PASSIVE); + window.removeEventListener('keydown', this._onKeyDown, false); + window.removeEventListener('keyup', this._onKeyUp, false); + + this._pointerEvents.clear(); + this._lastPinchDist = -1; + this._panning = false; + this._key = { + forward: false, + backward: false, + left: false, + right: false, + up: false, + down: false, + sprint: false, + crouch: false + }; + } + + /** + * @param {number} dt - The delta time in seconds. + */ + update(dt) { + if (!this._camera) { + return; + } + + if (!this._flying) { + this._cameraDist = math.lerp(this._cameraDist, this._zoom, 1 - Math.pow(this.moveDamping, dt * 1000)); + this._camera.setLocalPosition(0, 0, this._cameraDist); + } + + this._handleMove(dt); + + super.update(dt); + } + } + + const MultiCameraScript = createScript('multiCamera'); + + MultiCameraScript.attributes.add('camera', { type: 'entity' }); + + MultiCameraScript.prototype.initialize = function () { + this.multiCamera = new MultiCamera(this.entity, this.app.graphicsDevice.canvas, { + name: 'multi-camera' + }); + this.multiCamera.attach(this.camera); + + this._onKeyDown = this._onKeyDown.bind(this); + window.addEventListener('keydown', this._onKeyDown, false); + + this.on('destroy', () => { + this.multiCamera.detach(); + window.removeEventListener('keydown', this._onKeyDown, false); + }); + + }; + + MultiCameraScript.prototype._onKeyDown = function (event) { + if (event.key === 'f') { + this.focusOnEntity(this.focus); + } + }; + + MultiCameraScript.prototype.focusOnEntity = function (entity, snap = false) { + this.multiCamera.focusOnEntity(entity, snap); + }; + + MultiCameraScript.prototype.update = function (dt) { + this.multiCamera.update(dt); + }; +})(); From 6e0a52e7e28234fd57fcd4f2bc1dd589d3a87144 Mon Sep 17 00:00:00 2001 From: kpal Date: Thu, 30 May 2024 10:25:34 +0100 Subject: [PATCH 2/3] updated fps example --- .../examples/camera/first-person.example.mjs | 58 +- scripts/camera/first-person-camera.js | 1577 ++++++++--------- 2 files changed, 819 insertions(+), 816 deletions(-) diff --git a/examples/src/examples/camera/first-person.example.mjs b/examples/src/examples/camera/first-person.example.mjs index 5a2e4c3926d..ff8c50bfa97 100644 --- a/examples/src/examples/camera/first-person.example.mjs +++ b/examples/src/examples/camera/first-person.example.mjs @@ -1,4 +1,4 @@ -// @config DESCRIPTION
WASD to move
Space to jump
Mouse to look
+// @config DESCRIPTION
(WASD) Move
(Space) Jump
(Mouse) Look
import * as pc from 'playcanvas'; import { deviceType, rootPath } from 'examples/utils'; @@ -6,6 +6,7 @@ const canvas = document.getElementById('application-canvas'); if (!(canvas instanceof HTMLCanvasElement)) { throw new Error('No canvas found'); } +window.focus(); const gfxOptions = { deviceTypes: [deviceType], @@ -13,7 +14,21 @@ const gfxOptions = { twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js' }; +const assets = { + map: new pc.Asset('map', 'container', { url: rootPath + '/static/assets/models/fps-map.glb' }), + script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/first-person-camera.js' }), + ssao: new pc.Asset('ssao', 'script', { url: rootPath + '/static/scripts/posteffects/posteffect-ssao.js' }), + helipad: new pc.Asset( + 'helipad-env-atlas', + 'texture', + { url: rootPath + '/static/assets/cubemaps/helipad-env-atlas.png' }, + { type: pc.TEXTURETYPE_RGBP, mipmaps: false } + ) +}; + + const device = await pc.createGraphicsDevice(canvas, gfxOptions); + const createOptions = new pc.AppOptions(); createOptions.graphicsDevice = device; createOptions.mouse = new pc.Mouse(document.body); @@ -45,18 +60,6 @@ app.on('destroy', () => { window.removeEventListener('resize', resize); }); -const assets = { - map: new pc.Asset('map', 'container', { url: rootPath + '/static/assets/models/fps-map.glb' }), - script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/first-person-camera.js' }), - ssao: new pc.Asset('ssao', 'script', { url: rootPath + '/static/scripts/posteffects/posteffect-ssao.js' }), - helipad: new pc.Asset( - 'helipad-env-atlas', - 'texture', - { url: rootPath + '/static/assets/cubemaps/helipad-env-atlas.png' }, - { type: pc.TEXTURETYPE_RGBP, mipmaps: false } - ) -}; - pc.WasmModule.setConfig('Ammo', { glueUrl: rootPath + '/static/lib/ammo/ammo.wasm.js', wasmUrl: rootPath + '/static/lib/ammo/ammo.wasm.wasm', @@ -67,6 +70,10 @@ await new Promise((resolve) => { pc.WasmModule.getInstance('Ammo', () => resolve(true)); }); +await new Promise((resolve) => { + new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve); +}); + function createLevel() { const entity = new pc.Entity(); @@ -157,24 +164,21 @@ function createCharacterController() { return entity; } -const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); -assetListLoader.load(() => { - app.start(); +app.start(); - app.scene.ambientLight.set(0.2, 0.2, 0.2); +app.scene.ambientLight.set(0.2, 0.2, 0.2); - app.scene.skyboxMip = 1; - app.scene.envAtlas = assets.helipad.resource; +app.scene.skyboxMip = 1; +app.scene.envAtlas = assets.helipad.resource; - // Increase gravity for more natural jumping - app.systems.rigidbody?.gravity.set(0, -18, 0); +// Increase gravity for more natural jumping +app.systems.rigidbody?.gravity.set(0, -18, 0); - const level = createLevel(); - app.root.addChild(level); +const level = createLevel(); +app.root.addChild(level); - const characterController = createCharacterController(); - characterController.setPosition(-4, 2, 10); - app.root.addChild(characterController); -}); +const characterController = createCharacterController(); +characterController.setPosition(-4, 2, 10); +app.root.addChild(characterController); export { app }; diff --git a/scripts/camera/first-person-camera.js b/scripts/camera/first-person-camera.js index 80d0a1628f6..7a472885b37 100644 --- a/scripts/camera/first-person-camera.js +++ b/scripts/camera/first-person-camera.js @@ -1,531 +1,389 @@ -var { createScript, math, Vec2, Vec3, Mat4 } = pc; - -const LOOK_MAX_ANGLE = 90; - -const tmpV1 = new Vec3(); -const tmpV2 = new Vec3(); -const tmpM1 = new Mat4(); - -/** - * Utility function for both touch and gamepad handling of deadzones. Takes a 2-axis joystick - * position in the range -1 to 1 and applies an upper and lower radial deadzone, remapping values in - * the legal range from 0 to 1. - * - * @param {Vec2} pos - The joystick position. - * @param {Vec2} remappedPos - The remapped joystick position. - * @param {number} deadZoneLow - The lower dead zone. - * @param {number} deadZoneHigh - The upper dead zone. - */ -function applyRadialDeadZone(pos, remappedPos, deadZoneLow, deadZoneHigh) { - const magnitude = pos.length(); - - if (magnitude > deadZoneLow) { - const legalRange = 1 - deadZoneHigh - deadZoneLow; - const normalizedMag = Math.min(1, (magnitude - deadZoneLow) / legalRange); - remappedPos.copy(pos).scale(normalizedMag / magnitude); - } else { - remappedPos.set(0, 0); - } -} +(() => { + const { createScript, math, Vec2, Vec3, Mat4 } = pc; -class DesktopInput { - /** - * @type {HTMLCanvasElement} - * @private - */ - _canvas; + const LOOK_MAX_ANGLE = 90; - /** - * @type {boolean} - * @private - */ - _enabled = true; + const tmpV1 = new Vec3(); + const tmpV2 = new Vec3(); + const tmpM1 = new Mat4(); /** - * @type {AppBase} + * Utility function for both touch and gamepad handling of deadzones. Takes a 2-axis joystick + * position in the range -1 to 1 and applies an upper and lower radial deadzone, remapping values in + * the legal range from 0 to 1. + * + * @param {Vec2} pos - The joystick position. + * @param {Vec2} remappedPos - The remapped joystick position. + * @param {number} deadZoneLow - The lower dead zone. + * @param {number} deadZoneHigh - The upper dead zone. */ - app; - - /** - * @param {AppBase} app - The application. - */ - constructor(app) { - this.app = app; - this._canvas = app.graphicsDevice.canvas; - - this._onKeyDown = this._onKeyDown.bind(this); - this._onKeyUp = this._onKeyUp.bind(this); - this._onMouseDown = this._onMouseDown.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); + function applyRadialDeadZone(pos, remappedPos, deadZoneLow, deadZoneHigh) { + const magnitude = pos.length(); - this.enabled = true; - } - - set enabled(val) { - this._enabled = val; - - if (val) { - window.addEventListener('keydown', this._onKeyDown); - window.addEventListener('keyup', this._onKeyUp); - window.addEventListener('mousedown', this._onMouseDown); - window.addEventListener('mousemove', this._onMouseMove); + if (magnitude > deadZoneLow) { + const legalRange = 1 - deadZoneHigh - deadZoneLow; + const normalizedMag = Math.min(1, (magnitude - deadZoneLow) / legalRange); + remappedPos.copy(pos).scale(normalizedMag / magnitude); } else { - window.removeEventListener('keydown', this._onKeyDown); - window.removeEventListener('keyup', this._onKeyUp); - window.removeEventListener('mousedown', this._onMouseDown); - window.removeEventListener('mousemove', this._onMouseMove); + remappedPos.set(0, 0); } } - get enabled() { - return this._enabled; - } - - /** - * @param {string} key - The key pressed. - * @param {number} val - The key value. - * @private - */ - _handleKey(key, val) { - switch (key.toLowerCase()) { - case 'w': - case 'arrowup': - this.app.fire('cc:move:forward', val); - break; - case 's': - case 'arrowdown': - this.app.fire('cc:move:backward', val); - break; - case 'a': - case 'arrowleft': - this.app.fire('cc:move:left', val); - break; - case 'd': - case 'arrowright': - this.app.fire('cc:move:right', val); - break; - case ' ': - this.app.fire('cc:jump', !!val); - break; - case 'shift': - this.app.fire('cc:sprint', !!val); - break; + class DesktopInput { + /** + * @type {HTMLCanvasElement} + * @private + */ + _canvas; + + /** + * @type {boolean} + * @private + */ + _enabled = true; + + /** + * @type {AppBase} + */ + app; + + /** + * @param {AppBase} app - The application. + */ + constructor(app) { + this.app = app; + this._canvas = app.graphicsDevice.canvas; + + this._onKeyDown = this._onKeyDown.bind(this); + this._onKeyUp = this._onKeyUp.bind(this); + this._onMouseDown = this._onMouseDown.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + + this.enabled = true; } - } - /** - * @param {KeyboardEvent} e - The keyboard event. - * @private - */ - _onKeyDown(e) { - if (document.pointerLockElement !== this._canvas) { - return; + set enabled(val) { + this._enabled = val; + + if (val) { + window.addEventListener('keydown', this._onKeyDown); + window.addEventListener('keyup', this._onKeyUp); + window.addEventListener('mousedown', this._onMouseDown); + window.addEventListener('mousemove', this._onMouseMove); + } else { + window.removeEventListener('keydown', this._onKeyDown); + window.removeEventListener('keyup', this._onKeyUp); + window.removeEventListener('mousedown', this._onMouseDown); + window.removeEventListener('mousemove', this._onMouseMove); + } } - if (e.repeat) { - return; + get enabled() { + return this._enabled; } - this._handleKey(e.key, 1); - } - /** - * @param {KeyboardEvent} e - The keyboard event. - * @private - */ - _onKeyUp(e) { - if (e.repeat) { - return; + /** + * @param {string} key - The key pressed. + * @param {number} val - The key value. + * @private + */ + _handleKey(key, val) { + switch (key.toLowerCase()) { + case 'w': + case 'arrowup': + this.app.fire('cc:move:forward', val); + break; + case 's': + case 'arrowdown': + this.app.fire('cc:move:backward', val); + break; + case 'a': + case 'arrowleft': + this.app.fire('cc:move:left', val); + break; + case 'd': + case 'arrowright': + this.app.fire('cc:move:right', val); + break; + case ' ': + this.app.fire('cc:jump', !!val); + break; + case 'shift': + this.app.fire('cc:sprint', !!val); + break; + } } - this._handleKey(e.key, 0); - } - _onMouseDown(e) { - if (document.pointerLockElement !== this._canvas) { - this._canvas.requestPointerLock(); - } - } + /** + * @param {KeyboardEvent} e - The keyboard event. + * @private + */ + _onKeyDown(e) { + if (document.pointerLockElement !== this._canvas) { + return; + } - /** - * @param {MouseEvent} e - The mouse event. - * @private - */ - _onMouseMove(e) { - if (document.pointerLockElement !== this._canvas) { - return; + if (e.repeat) { + return; + } + this._handleKey(e.key, 1); } - const movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0; - const movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0; - - this.app.fire('cc:look', movementX, movementY); - } - - destroy() { - this.enabled = false; - } -} - -class MobileInput { - /** - * @type {GraphicsDevice} - * @private - */ - _device; - - /** - * @type {HTMLCanvasElement} - * @private - */ - _canvas; - - /** - * @type {boolean} - * @private - */ - _enabled = true; - - /** - * @type {number} - * @private - */ - _lastRightTap = 0; - - /** - * @type {number} - * @private - */ - _jumpTimeout; - - /** - * @type {Vec2} - * @private - */ - _remappedPos = new Vec2(); - - /** - * @type {{ identifier: number, center: Vec2; pos: Vec2 }} - * @private - */ - _leftStick = { - identifier: -1, - center: new Vec2(), - pos: new Vec2() - }; - - /** - * @type {{ identifier: number, center: Vec2; pos: Vec2 }} - * @private - */ - _rightStick = { - identifier: -1, - center: new Vec2(), - pos: new Vec2() - }; - - /** - * @type {AppBase} - */ - app; - - /** - * @type {number} - */ - deadZone = 0.3; - - /** - * @type {number} - */ - turnSpeed = 30; + /** + * @param {KeyboardEvent} e - The keyboard event. + * @private + */ + _onKeyUp(e) { + if (e.repeat) { + return; + } + this._handleKey(e.key, 0); + } - /** - * @type {number} - */ - radius = 50; + _onMouseDown(e) { + if (document.pointerLockElement !== this._canvas) { + this._canvas.requestPointerLock(); + } + } - /** - * @type {number} - */ - _doubleTapInterval = 300; + /** + * @param {MouseEvent} e - The mouse event. + * @private + */ + _onMouseMove(e) { + if (document.pointerLockElement !== this._canvas) { + return; + } - /** - * @param {AppBase} app - The application. - */ - constructor(app) { - this.app = app; - this._device = app.graphicsDevice; - this._canvas = app.graphicsDevice.canvas; + const movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0; + const movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0; - this._onTouchStart = this._onTouchStart.bind(this); - this._onTouchMove = this._onTouchMove.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); + this.app.fire('cc:look', movementX, movementY); + } - this.enabled = true; + destroy() { + this.enabled = false; + } } - set enabled(val) { - this._enabled = val; - if (val) { - this._canvas.addEventListener('touchstart', this._onTouchStart, false); - this._canvas.addEventListener('touchmove', this._onTouchMove, false); - this._canvas.addEventListener('touchend', this._onTouchEnd, false); - } else { - this._canvas.removeEventListener('touchstart', this._onTouchStart, false); - this._canvas.removeEventListener('touchmove', this._onTouchMove, false); - this._canvas.removeEventListener('touchend', this._onTouchEnd, false); + class MobileInput { + /** + * @type {GraphicsDevice} + * @private + */ + _device; + + /** + * @type {HTMLCanvasElement} + * @private + */ + _canvas; + + /** + * @type {boolean} + * @private + */ + _enabled = true; + + /** + * @type {number} + * @private + */ + _lastRightTap = 0; + + /** + * @type {number} + * @private + */ + _jumpTimeout; + + /** + * @type {Vec2} + * @private + */ + _remappedPos = new Vec2(); + + /** + * @type {{ identifier: number, center: Vec2; pos: Vec2 }} + * @private + */ + _leftStick = { + identifier: -1, + center: new Vec2(), + pos: new Vec2() + }; + + /** + * @type {{ identifier: number, center: Vec2; pos: Vec2 }} + * @private + */ + _rightStick = { + identifier: -1, + center: new Vec2(), + pos: new Vec2() + }; + + /** + * @type {AppBase} + */ + app; + + /** + * @type {number} + */ + deadZone = 0.3; + + /** + * @type {number} + */ + turnSpeed = 30; + + /** + * @type {number} + */ + radius = 50; + + /** + * @type {number} + */ + _doubleTapInterval = 300; + + /** + * @param {AppBase} app - The application. + */ + constructor(app) { + this.app = app; + this._device = app.graphicsDevice; + this._canvas = app.graphicsDevice.canvas; + + this._onTouchStart = this._onTouchStart.bind(this); + this._onTouchMove = this._onTouchMove.bind(this); + this._onTouchEnd = this._onTouchEnd.bind(this); + + this.enabled = true; } - } - get enabled() { - return this._enabled; - } + set enabled(val) { + this._enabled = val; + if (val) { + this._canvas.addEventListener('touchstart', this._onTouchStart, false); + this._canvas.addEventListener('touchmove', this._onTouchMove, false); + this._canvas.addEventListener('touchend', this._onTouchEnd, false); + } else { + this._canvas.removeEventListener('touchstart', this._onTouchStart, false); + this._canvas.removeEventListener('touchmove', this._onTouchMove, false); + this._canvas.removeEventListener('touchend', this._onTouchEnd, false); + } + } - /** - * @private - * @param {TouchEvent} e - The touch event. - */ - _onTouchStart(e) { - e.preventDefault(); - - const xFactor = this._device.width / this._canvas.clientWidth; - const yFactor = this._device.height / this._canvas.clientHeight; - - const touches = e.changedTouches; - for (let i = 0; i < touches.length; i++) { - const touch = touches[i]; - - if (touch.pageX <= this._canvas.clientWidth / 2 && this._leftStick.identifier === -1) { - // If the user touches the left half of the screen, create a left virtual joystick... - this._leftStick.identifier = touch.identifier; - this._leftStick.center.set(touch.pageX, touch.pageY); - this._leftStick.pos.set(0, 0); - this.app.fire('leftjoystick:enable', touch.pageX * xFactor, touch.pageY * yFactor); - } else if (touch.pageX > this._canvas.clientWidth / 2 && this._rightStick.identifier === -1) { - // ...otherwise create a right virtual joystick - this._rightStick.identifier = touch.identifier; - this._rightStick.center.set(touch.pageX, touch.pageY); - this._rightStick.pos.set(0, 0); - this.app.fire('rightjoystick:enable', touch.pageX * xFactor, touch.pageY * yFactor); - - // See how long since the last tap of the right virtual joystick to detect a double tap (jump) - const now = Date.now(); - if (now - this._lastRightTap < this._doubleTapInterval) { - if (this._jumpTimeout) { - clearTimeout(this._jumpTimeout); + get enabled() { + return this._enabled; + } + + /** + * @private + * @param {TouchEvent} e - The touch event. + */ + _onTouchStart(e) { + e.preventDefault(); + + const xFactor = this._device.width / this._canvas.clientWidth; + const yFactor = this._device.height / this._canvas.clientHeight; + + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + + if (touch.pageX <= this._canvas.clientWidth / 2 && this._leftStick.identifier === -1) { + // If the user touches the left half of the screen, create a left virtual joystick... + this._leftStick.identifier = touch.identifier; + this._leftStick.center.set(touch.pageX, touch.pageY); + this._leftStick.pos.set(0, 0); + this.app.fire('leftjoystick:enable', touch.pageX * xFactor, touch.pageY * yFactor); + } else if (touch.pageX > this._canvas.clientWidth / 2 && this._rightStick.identifier === -1) { + // ...otherwise create a right virtual joystick + this._rightStick.identifier = touch.identifier; + this._rightStick.center.set(touch.pageX, touch.pageY); + this._rightStick.pos.set(0, 0); + this.app.fire('rightjoystick:enable', touch.pageX * xFactor, touch.pageY * yFactor); + + // See how long since the last tap of the right virtual joystick to detect a double tap (jump) + const now = Date.now(); + if (now - this._lastRightTap < this._doubleTapInterval) { + if (this._jumpTimeout) { + clearTimeout(this._jumpTimeout); + } + this.app.fire('cc:jump', true); + this._jumpTimeout = setTimeout(() => this.app.fire('cc:jump', false), 50); } - this.app.fire('cc:jump', true); - this._jumpTimeout = setTimeout(() => this.app.fire('cc:jump', false), 50); + this._lastRightTap = now; } - this._lastRightTap = now; } } - } - /** - * @private - * @param {TouchEvent} e - The touch event. - */ - _onTouchMove(e) { - e.preventDefault(); - - const xFactor = this._device.width / this._canvas.clientWidth; - const yFactor = this._device.height / this._canvas.clientHeight; - - const touches = e.changedTouches; - for (let i = 0; i < touches.length; i++) { - const touch = touches[i]; - - // Update the current positions of the two virtual joysticks - if (touch.identifier === this._leftStick.identifier) { - this._leftStick.pos.set(touch.pageX, touch.pageY); - this._leftStick.pos.sub(this._leftStick.center); - this._leftStick.pos.scale(1 / this.radius); - this.app.fire('leftjoystick:move', touch.pageX * xFactor, touch.pageY * yFactor); - } else if (touch.identifier === this._rightStick.identifier) { - this._rightStick.pos.set(touch.pageX, touch.pageY); - this._rightStick.pos.sub(this._rightStick.center); - this._rightStick.pos.scale(1 / this.radius); - this.app.fire('rightjoystick:move', touch.pageX * xFactor, touch.pageY * yFactor); + /** + * @private + * @param {TouchEvent} e - The touch event. + */ + _onTouchMove(e) { + e.preventDefault(); + + const xFactor = this._device.width / this._canvas.clientWidth; + const yFactor = this._device.height / this._canvas.clientHeight; + + const touches = e.changedTouches; + for (let i = 0; i < touches.length; i++) { + const touch = touches[i]; + + // Update the current positions of the two virtual joysticks + if (touch.identifier === this._leftStick.identifier) { + this._leftStick.pos.set(touch.pageX, touch.pageY); + this._leftStick.pos.sub(this._leftStick.center); + this._leftStick.pos.scale(1 / this.radius); + this.app.fire('leftjoystick:move', touch.pageX * xFactor, touch.pageY * yFactor); + } else if (touch.identifier === this._rightStick.identifier) { + this._rightStick.pos.set(touch.pageX, touch.pageY); + this._rightStick.pos.sub(this._rightStick.center); + this._rightStick.pos.scale(1 / this.radius); + this.app.fire('rightjoystick:move', touch.pageX * xFactor, touch.pageY * yFactor); + } } } - } - /** - * @private - * @param {TouchEvent} e - The touch event. - */ - _onTouchEnd(e) { - e.preventDefault(); - - var touches = e.changedTouches; - for (var i = 0; i < touches.length; i++) { - var touch = touches[i]; - - // If this touch is one of the sticks, get rid of it... - if (touch.identifier === this._leftStick.identifier) { - this._leftStick.identifier = -1; - this.app.fire('cc:move:forward', 0); - this.app.fire('cc:move:backward', 0); - this.app.fire('cc:move:left', 0); - this.app.fire('cc:move:right', 0); - this.app.fire('leftjoystick:disable'); - } else if (touch.identifier === this._rightStick.identifier) { - this._rightStick.identifier = -1; - this.app.fire('rightjoystick:disable'); - } - } - } + /** + * @private + * @param {TouchEvent} e - The touch event. + */ + _onTouchEnd(e) { + e.preventDefault(); - /** - * @param {number} dt - The delta time. - */ - update(dt) { - // Moving - if (this._leftStick.identifier !== -1) { - // Apply a lower radial dead zone. We don't need an upper zone like with a real joypad - applyRadialDeadZone(this._leftStick.pos, this._remappedPos, this.deadZone, 0); - - const forward = -this._remappedPos.y; - if (this._lastForward !== forward) { - if (forward > 0) { - this.app.fire('cc:move:forward', Math.abs(forward)); - this.app.fire('cc:move:backward', 0); - } - if (forward < 0) { - this.app.fire('cc:move:forward', 0); - this.app.fire('cc:move:backward', Math.abs(forward)); - } - if (forward === 0) { + var touches = e.changedTouches; + for (var i = 0; i < touches.length; i++) { + var touch = touches[i]; + + // If this touch is one of the sticks, get rid of it... + if (touch.identifier === this._leftStick.identifier) { + this._leftStick.identifier = -1; this.app.fire('cc:move:forward', 0); this.app.fire('cc:move:backward', 0); - } - this._lastForward = forward; - } - - const strafe = this._remappedPos.x; - if (this._lastStrafe !== strafe) { - if (strafe > 0) { - this.app.fire('cc:move:left', 0); - this.app.fire('cc:move:right', Math.abs(strafe)); - } - if (strafe < 0) { - this.app.fire('cc:move:left', Math.abs(strafe)); - this.app.fire('cc:move:right', 0); - } - if (strafe === 0) { this.app.fire('cc:move:left', 0); this.app.fire('cc:move:right', 0); + this.app.fire('leftjoystick:disable'); + } else if (touch.identifier === this._rightStick.identifier) { + this._rightStick.identifier = -1; + this.app.fire('rightjoystick:disable'); } - this._lastStrafe = strafe; } } - // Looking - if (this._rightStick.identifier !== -1) { - // Apply a lower radial dead zone. We don't need an upper zone like with a real joypad - applyRadialDeadZone(this._rightStick.pos, this._remappedPos, this.deadZone, 0); - - const movX = this._remappedPos.x * this.turnSpeed; - const movY = this._remappedPos.y * this.turnSpeed; - this.app.fire('cc:look', movX, movY); - } - } - - destroy() { - this.enabled = false; - } -} - -class GamePadInput { - /** - * @type {number} - * @private - */ - _jumpTimeout; - - /** - * @type {number} - * @private - */ - _lastForward = 0; - - /** - * @type {number} - * @private - */ - _lastStrafe = 0; - - /** - * @type {boolean} - * @private - */ - _lastJump = false; - - /** - * @type {Vec2} - * @private - */ - _remappedPos = new Vec2(); - - /** - * @type {{ center: Vec2; pos: Vec2 }} - * @private - */ - _leftStick = { - center: new Vec2(), - pos: new Vec2() - }; - - /** - * @type {{ center: Vec2; pos: Vec2 }} - * @private - */ - _rightStick = { - center: new Vec2(), - pos: new Vec2() - }; - - /** - * @type {AppBase} - */ - app; - - /** - * @type {number} - */ - deadZoneLow = 0.1; - - /** - * @type {number} - */ - deadZoneHigh = 0.1; - - /** - * @type {number} - */ - turnSpeed = 30; - - /** - * @param {AppBase} app - The application. - */ - constructor(app) { - this.app = app; - } - - /** - * @param {number} dt - The delta time. - */ - update(dt) { - const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; - - for (let i = 0; i < gamepads.length; i++) { - const gamepad = gamepads[i]; - - // Only proceed if we have at least 2 sticks - if (gamepad && gamepad.mapping === 'standard' && gamepad.axes.length >= 4) { - // Moving (left stick) - this._leftStick.pos.set(gamepad.axes[0], gamepad.axes[1]); - applyRadialDeadZone(this._leftStick.pos, this._remappedPos, this.deadZoneLow, this.deadZoneHigh); - + /** + * @param {number} dt - The delta time. + */ + update(dt) { + // Moving + if (this._leftStick.identifier !== -1) { + // Apply a lower radial dead zone. We don't need an upper zone like with a real joypad + applyRadialDeadZone(this._leftStick.pos, this._remappedPos, this.deadZone, 0); const forward = -this._remappedPos.y; if (this._lastForward !== forward) { @@ -560,347 +418,488 @@ class GamePadInput { } this._lastStrafe = strafe; } + } - // Looking (right stick) - this._rightStick.pos.set(gamepad.axes[2], gamepad.axes[3]); - applyRadialDeadZone(this._rightStick.pos, this._remappedPos, this.deadZoneLow, this.deadZoneHigh); + // Looking + if (this._rightStick.identifier !== -1) { + // Apply a lower radial dead zone. We don't need an upper zone like with a real joypad + applyRadialDeadZone(this._rightStick.pos, this._remappedPos, this.deadZone, 0); const movX = this._remappedPos.x * this.turnSpeed; const movY = this._remappedPos.y * this.turnSpeed; this.app.fire('cc:look', movX, movY); - - // Jumping (bottom button of right cluster) - if (gamepad.buttons[0].pressed && !this._lastJump) { - if (this._jumpTimeout) { - clearTimeout(this._jumpTimeout); - } - this.app.fire('cc:jump', true); - this._jumpTimeout = setTimeout(() => this.app.fire('cc:jump', false), 50); - } - this._lastJump = gamepad.buttons[0].pressed; } } - } - - destroy() { + destroy() { + this.enabled = false; + } } -} - -class CharacterController { - /** - * @type {Entity} - * @private - */ - _camera; - - /** - * @type {RigidBodyComponent} - * @private - */ - _rigidbody; - /** - * @type {boolean} - * @private - */ - _jumping = false; + class GamePadInput { + /** + * @type {number} + * @private + */ + _jumpTimeout; + + /** + * @type {number} + * @private + */ + _lastForward = 0; + + /** + * @type {number} + * @private + */ + _lastStrafe = 0; + + /** + * @type {boolean} + * @private + */ + _lastJump = false; + + /** + * @type {Vec2} + * @private + */ + _remappedPos = new Vec2(); + + /** + * @type {{ center: Vec2; pos: Vec2 }} + * @private + */ + _leftStick = { + center: new Vec2(), + pos: new Vec2() + }; + + /** + * @type {{ center: Vec2; pos: Vec2 }} + * @private + */ + _rightStick = { + center: new Vec2(), + pos: new Vec2() + }; + + /** + * @type {AppBase} + */ + app; + + /** + * @type {number} + */ + deadZoneLow = 0.1; + + /** + * @type {number} + */ + deadZoneHigh = 0.1; + + /** + * @type {number} + */ + turnSpeed = 30; + + /** + * @param {AppBase} app - The application. + */ + constructor(app) { + this.app = app; + } - /** - * @type {AppBase} - */ - app; + /** + * @param {number} dt - The delta time. + */ + update(dt) { + const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; + + for (let i = 0; i < gamepads.length; i++) { + const gamepad = gamepads[i]; + + // Only proceed if we have at least 2 sticks + if (gamepad && gamepad.mapping === 'standard' && gamepad.axes.length >= 4) { + // Moving (left stick) + this._leftStick.pos.set(gamepad.axes[0], gamepad.axes[1]); + applyRadialDeadZone(this._leftStick.pos, this._remappedPos, this.deadZoneLow, this.deadZoneHigh); + + const forward = -this._remappedPos.y; + if (this._lastForward !== forward) { + if (forward > 0) { + this.app.fire('cc:move:forward', Math.abs(forward)); + this.app.fire('cc:move:backward', 0); + } + if (forward < 0) { + this.app.fire('cc:move:forward', 0); + this.app.fire('cc:move:backward', Math.abs(forward)); + } + if (forward === 0) { + this.app.fire('cc:move:forward', 0); + this.app.fire('cc:move:backward', 0); + } + this._lastForward = forward; + } - /** - * @type {Entity} - */ - entity; + const strafe = this._remappedPos.x; + if (this._lastStrafe !== strafe) { + if (strafe > 0) { + this.app.fire('cc:move:left', 0); + this.app.fire('cc:move:right', Math.abs(strafe)); + } + if (strafe < 0) { + this.app.fire('cc:move:left', Math.abs(strafe)); + this.app.fire('cc:move:right', 0); + } + if (strafe === 0) { + this.app.fire('cc:move:left', 0); + this.app.fire('cc:move:right', 0); + } + this._lastStrafe = strafe; + } - /** - * @type {Vec2} - */ - look = new Vec2(); + // Looking (right stick) + this._rightStick.pos.set(gamepad.axes[2], gamepad.axes[3]); + applyRadialDeadZone(this._rightStick.pos, this._remappedPos, this.deadZoneLow, this.deadZoneHigh); + + const movX = this._remappedPos.x * this.turnSpeed; + const movY = this._remappedPos.y * this.turnSpeed; + this.app.fire('cc:look', movX, movY); + + // Jumping (bottom button of right cluster) + if (gamepad.buttons[0].pressed && !this._lastJump) { + if (this._jumpTimeout) { + clearTimeout(this._jumpTimeout); + } + this.app.fire('cc:jump', true); + this._jumpTimeout = setTimeout(() => this.app.fire('cc:jump', false), 50); + } + this._lastJump = gamepad.buttons[0].pressed; + } + } + } - /** - * @type {Record} - */ - controls = { - forward: 0, - backward: 0, - left: 0, - right: 0, - jump: false, - sprint: false - }; + destroy() {} + } - /** - * @type {number} - */ - lookSens = 0.08; + class CharacterController { + /** + * @type {Entity} + * @private + */ + _camera; + + /** + * @type {RigidBodyComponent} + * @private + */ + _rigidbody; + + /** + * @type {boolean} + * @private + */ + _jumping = false; + + /** + * @type {AppBase} + */ + app; + + /** + * @type {Entity} + */ + entity; + + /** + * @type {Vec2} + */ + look = new Vec2(); + + /** + * @type {Record} + */ + controls = { + forward: 0, + backward: 0, + left: 0, + right: 0, + jump: false, + sprint: false + }; + + /** + * @type {number} + */ + lookSens = 0.08; + + /** + * @type {number} + */ + speedGround = 50; + + /** + * @type {number} + */ + speedAir = 5; + + /** + * @type {number} + */ + sprintMult = 1.5; + + /** + * @type {number} + */ + velocityDampingGround = 0.99; + + /** + * @type {number} + */ + velocityDampingAir = 0.99925; + + /** + * @type {number} + */ + jumpForce = 600; + + /** + * @param {AppBase} app - The application. + * @param {Entity} camera - The camera entity. + * @param {Entity} entity - The controller entity. + */ + constructor(app, camera, entity) { + this.app = app; + this.entity = entity; + + if (!camera) { + throw new Error('No camera entity found'); + } + this._camera = camera; + if (!entity.rigidbody) { + throw new Error('No rigidbody component found'); + } + this._rigidbody = entity.rigidbody; + + this.app.on('cc:look', (movX, movY) => { + this.look.x = math.clamp(this.look.x - movY * this.lookSens, -LOOK_MAX_ANGLE, LOOK_MAX_ANGLE); + this.look.y -= movX * this.lookSens; + }); + this.app.on('cc:move:forward', (val) => { + this.controls.forward = val; + }); + this.app.on('cc:move:backward', (val) => { + this.controls.backward = val; + }); + this.app.on('cc:move:left', (val) => { + this.controls.left = val; + }); + this.app.on('cc:move:right', (val) => { + this.controls.right = val; + }); + this.app.on('cc:jump', (state) => { + this.controls.jump = state; + }); + this.app.on('cc:sprint', (state) => { + this.controls.sprint = state; + }); + } - /** - * @type {number} - */ - speedGround = 50; + /** + * @private + */ + _checkIfGrounded() { + const start = this.entity.getPosition(); + const end = tmpV1.copy(start).add(Vec3.DOWN); + end.y -= 0.1; + this._grounded = !!this._rigidbody.system.raycastFirst(start, end); + } - /** - * @type {number} - */ - speedAir = 5; + /** + * @private + */ + _jump() { + if (this._rigidbody.linearVelocity.y < 0) { + this._jumping = false; + } + if (this.controls.jump && !this._jumping && this._grounded) { + this._jumping = true; + this._rigidbody.applyImpulse(0, this.jumpForce, 0); + } + } - /** - * @type {number} - */ - sprintMult = 1.5; + /** + * @private + */ + _look() { + this._camera.setLocalEulerAngles(this.look.x, this.look.y, 0); + } - /** - * @type {number} - */ - velocityDampingGround = 0.99; + /** + * @param {number} dt - The delta time. + */ + _move(dt) { + tmpM1.setFromAxisAngle(Vec3.UP, this.look.y); + const dir = tmpV1.set(0, 0, 0); + if (this.controls.forward) { + dir.add(tmpV2.set(0, 0, -this.controls.forward)); + } + if (this.controls.backward) { + dir.add(tmpV2.set(0, 0, this.controls.backward)); + } + if (this.controls.left) { + dir.add(tmpV2.set(-this.controls.left, 0, 0)); + } + if (this.controls.right) { + dir.add(tmpV2.set(this.controls.right, 0, 0)); + } + tmpM1.transformVector(dir, dir); - /** - * @type {number} - */ - velocityDampingAir = 0.99925; + let speed = this._grounded ? this.speedGround : this.speedAir; + if (this.controls.sprint) { + speed *= this.sprintMult; + } - /** - * @type {number} - */ - jumpForce = 600; + const accel = dir.mulScalar(speed * dt); + const velocity = this._rigidbody.linearVelocity.add(accel); - /** - * @param {AppBase} app - The application. - * @param {Entity} camera - The camera entity. - * @param {Entity} entity - The controller entity. - */ - constructor(app, camera, entity) { - this.app = app; - this.entity = entity; + const damping = this._grounded ? this.velocityDampingGround : this.velocityDampingAir; + const mult = Math.pow(damping, dt * 1e3); + velocity.x *= mult; + velocity.z *= mult; - if (!camera) { - throw new Error('No camera entity found'); + this._rigidbody.linearVelocity = velocity; } - this._camera = camera; - if (!entity.rigidbody) { - throw new Error('No rigidbody component found'); + + /** + * @param {number} dt - The delta time. + */ + update(dt) { + this._checkIfGrounded(); + this._jump(); + this._look(); + this._move(dt); } - this._rigidbody = entity.rigidbody; - - this.app.on('cc:look', (movX, movY) => { - this.look.x = math.clamp(this.look.x - movY * this.lookSens, -LOOK_MAX_ANGLE, LOOK_MAX_ANGLE); - this.look.y -= movX * this.lookSens; - }); - this.app.on('cc:move:forward', (val) => { - this.controls.forward = val; - }); - this.app.on('cc:move:backward', (val) => { - this.controls.backward = val; - }); - this.app.on('cc:move:left', (val) => { - this.controls.left = val; - }); - this.app.on('cc:move:right', (val) => { - this.controls.right = val; - }); - this.app.on('cc:jump', (state) => { - this.controls.jump = state; - }); - this.app.on('cc:sprint', (state) => { - this.controls.sprint = state; - }); } - /** - * @private - */ - _checkIfGrounded() { - const start = this.entity.getPosition(); - const end = tmpV1.copy(start).add(Vec3.DOWN); - end.y -= 0.1; - this._grounded = !!this._rigidbody.system.raycastFirst(start, end); - } + // SCRIPTS - /** - * @private - */ - _jump() { - if (this._rigidbody.linearVelocity.y < 0) { - this._jumping = false; - } - if (this.controls.jump && !this._jumping && this._grounded) { - this._jumping = true; - this._rigidbody.applyImpulse(0, this.jumpForce, 0); - } - } + const DesktopInputScript = createScript('desktopInput'); - /** - * @private - */ - _look() { - this._camera.setLocalEulerAngles(this.look.x, this.look.y, 0); - } + DesktopInputScript.prototype.initialize = function () { + this.input = new DesktopInput(this.app); + this.on('enable', () => (this.input.enabled = true)); + this.on('disable', () => (this.input.enabled = false)); + this.on('destroy', () => this.input.destroy()); + }; - /** - * @param {number} dt - The delta time. - */ - _move(dt) { - tmpM1.setFromAxisAngle(Vec3.UP, this.look.y); - const dir = tmpV1.set(0, 0, 0); - if (this.controls.forward) { - dir.add(tmpV2.set(0, 0, -this.controls.forward)); - } - if (this.controls.backward) { - dir.add(tmpV2.set(0, 0, this.controls.backward)); - } - if (this.controls.left) { - dir.add(tmpV2.set(-this.controls.left, 0, 0)); - } - if (this.controls.right) { - dir.add(tmpV2.set(this.controls.right, 0, 0)); - } - tmpM1.transformVector(dir, dir); + const MobileInputScript = createScript('mobileInput'); + + MobileInputScript.attributes.add('deadZone', { + title: 'Dead Zone', + description: 'Radial thickness of inner dead zone of the virtual joysticks. This dead zone ensures the virtual joysticks report a value of 0 even if a touch deviates a small amount from the initial touch.', + type: 'number', + min: 0, + max: 0.4, + default: 0.3 + }); + MobileInputScript.attributes.add('turnSpeed', { + title: 'Turn Speed', + description: 'Maximum turn speed in degrees per second', + type: 'number', + default: 30 + }); + MobileInputScript.attributes.add('radius', { + title: 'Radius', + description: 'The radius of the virtual joystick in CSS pixels.', + type: 'number', + default: 50 + }); + MobileInputScript.attributes.add('_doubleTapInterval', { + title: 'Double Tap Interval', + description: 'The time in milliseconds between two taps of the right virtual joystick for a double tap to register. A double tap will trigger a cc:jump.', + type: 'number', + default: 300 + }); + + MobileInputScript.prototype.initialize = function () { + this.input = new MobileInput(this.app); + this.input.deadZone = this.deadZone; + this.input.turnSpeed = this.turnSpeed; + this.input.radius = this.radius; + this.input._doubleTapInterval = this._doubleTapInterval; + this.on('enable', () => (this.input.enabled = true)); + this.on('disable', () => (this.input.enabled = false)); + this.on('destroy', () => this.input.destroy()); + }; - let speed = this._grounded ? this.speedGround : this.speedAir; - if (this.controls.sprint) { - speed *= this.sprintMult; - } + MobileInputScript.prototype.update = function (dt) { + this.input.update(dt); + }; - const accel = dir.mulScalar(speed * dt); - const velocity = this._rigidbody.linearVelocity.add(accel); + const GamePadInputScript = createScript('gamePadInput'); + + GamePadInputScript.attributes.add('deadZoneLow', { + title: 'Low Dead Zone', + description: "Radial thickness of inner dead zone of pad's joysticks. This dead zone ensures that all pads report a value of 0 for each joystick axis when untouched.", + type: 'number', + min: 0, + max: 0.4, + default: 0.1 + }); + GamePadInputScript.attributes.add('deadZoneHigh', { + title: 'High Dead Zone', + description: "Radial thickness of outer dead zone of pad's joysticks. This dead zone ensures that all pads can reach the -1 and 1 limits of each joystick axis.", + type: 'number', + min: 0, + max: 0.4, + default: 0.1 + }); + GamePadInputScript.attributes.add('turnSpeed', { + title: 'Turn Speed', + description: 'Maximum turn speed in degrees per second', + type: 'number', + default: 30 + }); + + GamePadInputScript.prototype.initialize = function () { + this.input = new GamePadInput(this.app); + this.input.deadZoneLow = this.deadZoneLow; + this.input.deadZoneHigh = this.deadZoneHigh; + this.input.turnSpeed = this.turnSpeed; + this.on('destroy', () => this.input.destroy()); + }; - const damping = this._grounded ? this.velocityDampingGround : this.velocityDampingAir; - const mult = Math.pow(damping, dt * 1e3); - velocity.x *= mult; - velocity.z *= mult; + GamePadInputScript.prototype.update = function (dt) { + this.input.update(dt); + }; - this._rigidbody.linearVelocity = velocity; - } + const CharacterControllerScript = createScript('characterController'); + + CharacterControllerScript.attributes.add('camera', { type: 'entity' }); + CharacterControllerScript.attributes.add('lookSens', { type: 'number', default: 0.08 }); + CharacterControllerScript.attributes.add('speedGround', { type: 'number', default: 50 }); + CharacterControllerScript.attributes.add('speedAir', { type: 'number', default: 5 }); + CharacterControllerScript.attributes.add('sprintMult', { type: 'number', default: 1.5 }); + CharacterControllerScript.attributes.add('velocityDampingGround', { type: 'number', default: 0.99 }); + CharacterControllerScript.attributes.add('velocityDampingAir', { type: 'number', default: 0.99925 }); + CharacterControllerScript.attributes.add('jumpForce', { type: 'number', default: 600 }); + + CharacterControllerScript.prototype.initialize = function () { + this.controller = new CharacterController(this.app, this.camera, this.entity); + this.controller.lookSens = this.lookSens; + this.controller.speedGround = this.speedGround; + this.controller.speedAir = this.speedAir; + this.controller.sprintMult = this.sprintMult; + this.controller.velocityDampingGround = this.velocityDampingGround; + this.controller.velocityDampingAir = this.velocityDampingAir; + this.controller.jumpForce = this.jumpForce; + }; - /** - * @param {number} dt - The delta time. - */ - update(dt) { - this._checkIfGrounded(); - this._jump(); - this._look(); - this._move(dt); - } -} - -// SCRIPTS - -const DesktopInputScript = createScript('desktopInput'); - -DesktopInputScript.prototype.initialize = function () { - this.input = new DesktopInput(this.app); - this.on('enable', () => (this.input.enabled = true)); - this.on('disable', () => (this.input.enabled = false)); - this.on('destroy', () => this.input.destroy()); -}; - -const MobileInputScript = createScript('mobileInput'); - -MobileInputScript.attributes.add('deadZone', { - title: 'Dead Zone', - description: 'Radial thickness of inner dead zone of the virtual joysticks. This dead zone ensures the virtual joysticks report a value of 0 even if a touch deviates a small amount from the initial touch.', - type: 'number', - min: 0, - max: 0.4, - default: 0.3 -}); -MobileInputScript.attributes.add('turnSpeed', { - title: 'Turn Speed', - description: 'Maximum turn speed in degrees per second', - type: 'number', - default: 30 -}); -MobileInputScript.attributes.add('radius', { - title: 'Radius', - description: 'The radius of the virtual joystick in CSS pixels.', - type: 'number', - default: 50 -}); -MobileInputScript.attributes.add('_doubleTapInterval', { - title: 'Double Tap Interval', - description: 'The time in milliseconds between two taps of the right virtual joystick for a double tap to register. A double tap will trigger a cc:jump.', - type: 'number', - default: 300 -}); - -MobileInputScript.prototype.initialize = function () { - this.input = new MobileInput(this.app); - this.input.deadZone = this.deadZone; - this.input.turnSpeed = this.turnSpeed; - this.input.radius = this.radius; - this.input._doubleTapInterval = this._doubleTapInterval; - this.on('enable', () => (this.input.enabled = true)); - this.on('disable', () => (this.input.enabled = false)); - this.on('destroy', () => this.input.destroy()); -}; - -MobileInputScript.prototype.update = function (dt) { - this.input.update(dt); -}; - -const GamePadInputScript = createScript('gamePadInput'); - -GamePadInputScript.attributes.add('deadZoneLow', { - title: 'Low Dead Zone', - description: "Radial thickness of inner dead zone of pad's joysticks. This dead zone ensures that all pads report a value of 0 for each joystick axis when untouched.", - type: 'number', - min: 0, - max: 0.4, - default: 0.1 -}); -GamePadInputScript.attributes.add('deadZoneHigh', { - title: 'High Dead Zone', - description: "Radial thickness of outer dead zone of pad's joysticks. This dead zone ensures that all pads can reach the -1 and 1 limits of each joystick axis.", - type: 'number', - min: 0, - max: 0.4, - default: 0.1 -}); -GamePadInputScript.attributes.add('turnSpeed', { - title: 'Turn Speed', - description: 'Maximum turn speed in degrees per second', - type: 'number', - default: 30 -}); - -GamePadInputScript.prototype.initialize = function () { - this.input = new GamePadInput(this.app); - this.input.deadZoneLow = this.deadZoneLow; - this.input.deadZoneHigh = this.deadZoneHigh; - this.input.turnSpeed = this.turnSpeed; - this.on('destroy', () => this.input.destroy()); -}; - -GamePadInputScript.prototype.update = function (dt) { - this.input.update(dt); -}; - -const CCScript = createScript('characterController'); - -CCScript.attributes.add('camera', { type: 'entity' }); -CCScript.attributes.add('lookSens', { type: 'number', default: 0.08 }); -CCScript.attributes.add('speedGround', { type: 'number', default: 50 }); -CCScript.attributes.add('speedAir', { type: 'number', default: 5 }); -CCScript.attributes.add('sprintMult', { type: 'number', default: 1.5 }); -CCScript.attributes.add('velocityDampingGround', { type: 'number', default: 0.99 }); -CCScript.attributes.add('velocityDampingAir', { type: 'number', default: 0.99925 }); -CCScript.attributes.add('jumpForce', { type: 'number', default: 600 }); - -CCScript.prototype.initialize = function () { - this.controller = new CharacterController(this.app, this.camera, this.entity); - this.controller.lookSens = this.lookSens; - this.controller.speedGround = this.speedGround; - this.controller.speedAir = this.speedAir; - this.controller.sprintMult = this.sprintMult; - this.controller.velocityDampingGround = this.velocityDampingGround; - this.controller.velocityDampingAir = this.velocityDampingAir; - this.controller.jumpForce = this.jumpForce; -}; - -CCScript.prototype.update = function (dt) { - this.controller.update(dt); -}; + CharacterControllerScript.prototype.update = function (dt) { + this.controller.update(dt); + }; +})(); From b289e609cebcbcab69fd0dbdaef45cb6b57d4e34 Mon Sep 17 00:00:00 2001 From: kpal Date: Thu, 30 May 2024 10:27:25 +0100 Subject: [PATCH 3/3] fixed focus issue --- scripts/camera/multi-camera.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/camera/multi-camera.js b/scripts/camera/multi-camera.js index b8ea90b0f59..768ea1e07a7 100644 --- a/scripts/camera/multi-camera.js +++ b/scripts/camera/multi-camera.js @@ -738,13 +738,14 @@ }; MultiCameraScript.prototype._onKeyDown = function (event) { - if (event.key === 'f') { + if (event.key === 'f' && this.focus) { this.focusOnEntity(this.focus); } }; MultiCameraScript.prototype.focusOnEntity = function (entity, snap = false) { - this.multiCamera.focusOnEntity(entity, snap); + this.focus = entity; + this.multiCamera.focusOnEntity(this.focus, snap); }; MultiCameraScript.prototype.update = function (dt) {